mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Lens] Move editorFrame state to redux (#100858)
Co-authored-by: Joe Reuter <johannes.reuter@elastic.co> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: dej611 <dej611@gmail.com>
This commit is contained in:
parent
65ff74ff5a
commit
6e3df60aba
67 changed files with 2754 additions and 3379 deletions
|
@ -260,7 +260,6 @@ export const App = (props: {
|
|||
color
|
||||
) as unknown) as LensEmbeddableInput
|
||||
}
|
||||
isVisible={isSaveModalVisible}
|
||||
onSave={() => {}}
|
||||
onClose={() => setIsSaveModalVisible(false)}
|
||||
/>
|
||||
|
|
|
@ -13,7 +13,13 @@ import { App } from './app';
|
|||
import { LensAppProps, LensAppServices } from './types';
|
||||
import { EditorFrameInstance, EditorFrameProps } from '../types';
|
||||
import { Document } from '../persistence';
|
||||
import { makeDefaultServices, mountWithProvider } from '../mocks';
|
||||
import {
|
||||
createMockDatasource,
|
||||
createMockVisualization,
|
||||
DatasourceMock,
|
||||
makeDefaultServices,
|
||||
mountWithProvider,
|
||||
} from '../mocks';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import {
|
||||
SavedObjectSaveModal,
|
||||
|
@ -25,7 +31,6 @@ import {
|
|||
FilterManager,
|
||||
IFieldType,
|
||||
IIndexPattern,
|
||||
IndexPattern,
|
||||
Query,
|
||||
} from '../../../../../src/plugins/data/public';
|
||||
import { TopNavMenuData } from '../../../../../src/plugins/navigation/public';
|
||||
|
@ -60,17 +65,41 @@ jest.mock('lodash', () => {
|
|||
|
||||
// const navigationStartMock = navigationPluginMock.createStartContract();
|
||||
|
||||
function createMockFrame(): jest.Mocked<EditorFrameInstance> {
|
||||
return {
|
||||
EditorFrameContainer: jest.fn((props: EditorFrameProps) => <div />),
|
||||
};
|
||||
}
|
||||
|
||||
const sessionIdSubject = new Subject<string>();
|
||||
|
||||
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 />),
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
};
|
||||
}
|
||||
|
||||
const navMenuItems = {
|
||||
expectedSaveButton: { emphasize: true, testId: 'lnsApp_saveButton' },
|
||||
|
@ -86,17 +115,19 @@ describe('Lens App', () => {
|
|||
redirectToOrigin: jest.fn(),
|
||||
onAppLeave: jest.fn(),
|
||||
setHeaderActionMenu: jest.fn(),
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
};
|
||||
}
|
||||
|
||||
async function mountWith({
|
||||
props = makeDefaultProps(),
|
||||
services = makeDefaultServices(sessionIdSubject),
|
||||
storePreloadedState,
|
||||
preloadedState,
|
||||
}: {
|
||||
props?: jest.Mocked<LensAppProps>;
|
||||
services?: jest.Mocked<LensAppServices>;
|
||||
storePreloadedState?: Partial<LensAppState>;
|
||||
preloadedState?: Partial<LensAppState>;
|
||||
}) {
|
||||
const wrappingComponent: React.FC<{
|
||||
children: React.ReactNode;
|
||||
|
@ -110,9 +141,11 @@ describe('Lens App', () => {
|
|||
|
||||
const { instance, lensStore } = await mountWithProvider(
|
||||
<App {...props} />,
|
||||
services.data,
|
||||
storePreloadedState,
|
||||
wrappingComponent
|
||||
{
|
||||
data: services.data,
|
||||
preloadedState,
|
||||
},
|
||||
{ wrappingComponent }
|
||||
);
|
||||
|
||||
const frame = props.editorFrame as ReturnType<typeof createMockFrame>;
|
||||
|
@ -139,8 +172,6 @@ describe('Lens App', () => {
|
|||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"initialContext": undefined,
|
||||
"onError": [Function],
|
||||
"showNoDataPopover": [Function],
|
||||
},
|
||||
Object {},
|
||||
|
@ -164,7 +195,7 @@ describe('Lens App', () => {
|
|||
|
||||
instance.update();
|
||||
expect(lensStore.getState()).toEqual({
|
||||
app: expect.objectContaining({
|
||||
lens: expect.objectContaining({
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [pinnedFilter],
|
||||
resolvedDateRange: {
|
||||
|
@ -177,14 +208,6 @@ describe('Lens App', () => {
|
|||
expect(services.data.query.filterManager.getFilters).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('displays errors from the frame in a toast', async () => {
|
||||
const { instance, frame, services } = await mountWith({});
|
||||
const onError = frame.EditorFrameContainer.mock.calls[0][0].onError;
|
||||
onError({ message: 'error' });
|
||||
instance.update();
|
||||
expect(services.notifications.toasts.addDanger).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('breadcrumbs', () => {
|
||||
const breadcrumbDocSavedObjectId = defaultSavedObjectId;
|
||||
const breadcrumbDoc = ({
|
||||
|
@ -237,7 +260,7 @@ describe('Lens App', () => {
|
|||
const { instance, lensStore } = await mountWith({
|
||||
props,
|
||||
services,
|
||||
storePreloadedState: {
|
||||
preloadedState: {
|
||||
isLinkedToOriginatingApp: true,
|
||||
},
|
||||
});
|
||||
|
@ -275,8 +298,8 @@ describe('Lens App', () => {
|
|||
});
|
||||
|
||||
describe('persistence', () => {
|
||||
it('loads a document and uses query and filters if initial input is provided', async () => {
|
||||
const { instance, lensStore, services } = await mountWith({});
|
||||
it('passes query and indexPatterns to TopNavMenu', async () => {
|
||||
const { instance, lensStore, services } = await mountWith({ preloadedState: {} });
|
||||
const document = ({
|
||||
savedObjectId: defaultSavedObjectId,
|
||||
state: {
|
||||
|
@ -290,8 +313,6 @@ describe('Lens App', () => {
|
|||
lensStore.dispatch(
|
||||
setState({
|
||||
query: ('fake query' as unknown) as Query,
|
||||
indexPatternsForTopNav: ([{ id: '1' }] as unknown) as IndexPattern[],
|
||||
lastKnownDoc: document,
|
||||
persistedDoc: document,
|
||||
})
|
||||
);
|
||||
|
@ -301,7 +322,7 @@ describe('Lens App', () => {
|
|||
expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
query: 'fake query',
|
||||
indexPatterns: [{ id: '1' }],
|
||||
indexPatterns: [{ id: 'mockip' }],
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
@ -332,16 +353,11 @@ describe('Lens App', () => {
|
|||
}
|
||||
|
||||
async function save({
|
||||
lastKnownDoc = {
|
||||
references: [],
|
||||
state: {
|
||||
filters: [],
|
||||
},
|
||||
},
|
||||
preloadedState,
|
||||
initialSavedObjectId,
|
||||
...saveProps
|
||||
}: SaveProps & {
|
||||
lastKnownDoc?: object;
|
||||
preloadedState?: Partial<LensAppState>;
|
||||
initialSavedObjectId?: string;
|
||||
}) {
|
||||
const props = {
|
||||
|
@ -366,18 +382,14 @@ describe('Lens App', () => {
|
|||
},
|
||||
} as jest.ResolvedValue<Document>);
|
||||
|
||||
const { frame, instance, lensStore } = await mountWith({ services, props });
|
||||
|
||||
act(() => {
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
isSaveable: true,
|
||||
lastKnownDoc: { savedObjectId: initialSavedObjectId, ...lastKnownDoc } as Document,
|
||||
})
|
||||
);
|
||||
const { frame, instance, lensStore } = await mountWith({
|
||||
services,
|
||||
props,
|
||||
preloadedState: {
|
||||
isSaveable: true,
|
||||
...preloadedState,
|
||||
},
|
||||
});
|
||||
|
||||
instance.update();
|
||||
expect(getButton(instance).disableButton).toEqual(false);
|
||||
await act(async () => {
|
||||
testSave(instance, { ...saveProps });
|
||||
|
@ -399,7 +411,6 @@ describe('Lens App', () => {
|
|||
act(() => {
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
lastKnownDoc: ({ savedObjectId: 'will save this' } as unknown) as Document,
|
||||
isSaveable: true,
|
||||
})
|
||||
);
|
||||
|
@ -415,7 +426,6 @@ describe('Lens App', () => {
|
|||
lensStore.dispatch(
|
||||
setState({
|
||||
isSaveable: true,
|
||||
lastKnownDoc: ({ savedObjectId: 'will save this' } as unknown) as Document,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -455,7 +465,7 @@ describe('Lens App', () => {
|
|||
const { instance } = await mountWith({
|
||||
props,
|
||||
services,
|
||||
storePreloadedState: {
|
||||
preloadedState: {
|
||||
isLinkedToOriginatingApp: true,
|
||||
},
|
||||
});
|
||||
|
@ -483,7 +493,7 @@ describe('Lens App', () => {
|
|||
|
||||
const { instance, services } = await mountWith({
|
||||
props,
|
||||
storePreloadedState: {
|
||||
preloadedState: {
|
||||
isLinkedToOriginatingApp: true,
|
||||
},
|
||||
});
|
||||
|
@ -540,6 +550,7 @@ describe('Lens App', () => {
|
|||
initialSavedObjectId: defaultSavedObjectId,
|
||||
newCopyOnSave: true,
|
||||
newTitle: 'hello there',
|
||||
preloadedState: { persistedDoc: defaultDoc },
|
||||
});
|
||||
expect(services.attributeService.wrapAttributes).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -559,10 +570,11 @@ describe('Lens App', () => {
|
|||
});
|
||||
|
||||
it('saves existing docs', async () => {
|
||||
const { props, services, instance, lensStore } = await save({
|
||||
const { props, services, instance } = await save({
|
||||
initialSavedObjectId: defaultSavedObjectId,
|
||||
newCopyOnSave: false,
|
||||
newTitle: 'hello there',
|
||||
preloadedState: { persistedDoc: defaultDoc },
|
||||
});
|
||||
expect(services.attributeService.wrapAttributes).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -576,22 +588,6 @@ describe('Lens App', () => {
|
|||
await act(async () => {
|
||||
instance.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } });
|
||||
});
|
||||
|
||||
expect(lensStore.dispatch).toHaveBeenCalledWith({
|
||||
payload: {
|
||||
lastKnownDoc: expect.objectContaining({
|
||||
savedObjectId: defaultSavedObjectId,
|
||||
title: 'hello there',
|
||||
}),
|
||||
persistedDoc: expect.objectContaining({
|
||||
savedObjectId: defaultSavedObjectId,
|
||||
title: 'hello there',
|
||||
}),
|
||||
isLinkedToOriginatingApp: false,
|
||||
},
|
||||
type: 'app/setState',
|
||||
});
|
||||
|
||||
expect(services.notifications.toasts.addSuccess).toHaveBeenCalledWith(
|
||||
"Saved 'hello there'"
|
||||
);
|
||||
|
@ -602,18 +598,13 @@ describe('Lens App', () => {
|
|||
services.attributeService.wrapAttributes = jest
|
||||
.fn()
|
||||
.mockRejectedValue({ message: 'failed' });
|
||||
const { instance, props, lensStore } = await mountWith({ services });
|
||||
act(() => {
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
isSaveable: true,
|
||||
lastKnownDoc: ({ id: undefined } as unknown) as Document,
|
||||
})
|
||||
);
|
||||
const { instance, props } = await mountWith({
|
||||
services,
|
||||
preloadedState: {
|
||||
isSaveable: true,
|
||||
},
|
||||
});
|
||||
|
||||
instance.update();
|
||||
|
||||
await act(async () => {
|
||||
testSave(instance, { newCopyOnSave: false, newTitle: 'hello there' });
|
||||
});
|
||||
|
@ -655,22 +646,19 @@ describe('Lens App', () => {
|
|||
initialSavedObjectId: defaultSavedObjectId,
|
||||
newCopyOnSave: false,
|
||||
newTitle: 'hello there2',
|
||||
lastKnownDoc: {
|
||||
expression: 'kibana 3',
|
||||
state: {
|
||||
filters: [pinned, unpinned],
|
||||
},
|
||||
preloadedState: {
|
||||
persistedDoc: defaultDoc,
|
||||
filters: [pinned, unpinned],
|
||||
},
|
||||
});
|
||||
expect(services.attributeService.wrapAttributes).toHaveBeenCalledWith(
|
||||
{
|
||||
expect.objectContaining({
|
||||
savedObjectId: defaultSavedObjectId,
|
||||
title: 'hello there2',
|
||||
expression: 'kibana 3',
|
||||
state: {
|
||||
state: expect.objectContaining({
|
||||
filters: [unpinned],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
true,
|
||||
{ id: '5678', savedObjectId: defaultSavedObjectId }
|
||||
);
|
||||
|
@ -681,17 +669,13 @@ describe('Lens App', () => {
|
|||
services.attributeService.wrapAttributes = jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({ savedObjectId: '123' }));
|
||||
const { instance, lensStore } = await mountWith({ services });
|
||||
await act(async () => {
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
isSaveable: true,
|
||||
lastKnownDoc: ({ savedObjectId: '123' } as unknown) as Document,
|
||||
})
|
||||
);
|
||||
const { instance } = await mountWith({
|
||||
services,
|
||||
preloadedState: {
|
||||
isSaveable: true,
|
||||
persistedDoc: ({ savedObjectId: '123' } as unknown) as Document,
|
||||
},
|
||||
});
|
||||
|
||||
instance.update();
|
||||
await act(async () => {
|
||||
instance.setProps({ initialInput: { savedObjectId: '123' } });
|
||||
getButton(instance).run(instance.getDOMNode());
|
||||
|
@ -716,17 +700,7 @@ describe('Lens App', () => {
|
|||
});
|
||||
|
||||
it('does not show the copy button on first save', async () => {
|
||||
const { instance, lensStore } = await mountWith({});
|
||||
await act(async () => {
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
isSaveable: true,
|
||||
lastKnownDoc: ({} as unknown) as Document,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
instance.update();
|
||||
const { instance } = await mountWith({ preloadedState: { isSaveable: true } });
|
||||
await act(async () => getButton(instance).run(instance.getDOMNode()));
|
||||
instance.update();
|
||||
expect(instance.find(SavedObjectSaveModal).prop('showCopyOnSave')).toEqual(false);
|
||||
|
@ -744,33 +718,18 @@ describe('Lens App', () => {
|
|||
}
|
||||
|
||||
it('should be disabled when no data is available', async () => {
|
||||
const { instance, lensStore } = await mountWith({});
|
||||
await act(async () => {
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
isSaveable: true,
|
||||
lastKnownDoc: ({} as unknown) as Document,
|
||||
})
|
||||
);
|
||||
});
|
||||
instance.update();
|
||||
const { instance } = await mountWith({ preloadedState: { isSaveable: true } });
|
||||
expect(getButton(instance).disableButton).toEqual(true);
|
||||
});
|
||||
|
||||
it('should disable download when not saveable', async () => {
|
||||
const { instance, lensStore } = await mountWith({});
|
||||
|
||||
await act(async () => {
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
lastKnownDoc: ({} as unknown) as Document,
|
||||
isSaveable: false,
|
||||
activeData: { layer1: { type: 'datatable', columns: [], rows: [] } },
|
||||
})
|
||||
);
|
||||
const { instance } = await mountWith({
|
||||
preloadedState: {
|
||||
isSaveable: false,
|
||||
activeData: { layer1: { type: 'datatable', columns: [], rows: [] } },
|
||||
},
|
||||
});
|
||||
|
||||
instance.update();
|
||||
expect(getButton(instance).disableButton).toEqual(true);
|
||||
});
|
||||
|
||||
|
@ -784,17 +743,13 @@ describe('Lens App', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const { instance, lensStore } = await mountWith({ services });
|
||||
await act(async () => {
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
lastKnownDoc: ({} as unknown) as Document,
|
||||
isSaveable: true,
|
||||
activeData: { layer1: { type: 'datatable', columns: [], rows: [] } },
|
||||
})
|
||||
);
|
||||
const { instance } = await mountWith({
|
||||
services,
|
||||
preloadedState: {
|
||||
isSaveable: true,
|
||||
activeData: { layer1: { type: 'datatable', columns: [], rows: [] } },
|
||||
},
|
||||
});
|
||||
instance.update();
|
||||
expect(getButton(instance).disableButton).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
@ -812,7 +767,7 @@ describe('Lens App', () => {
|
|||
);
|
||||
|
||||
expect(lensStore.getState()).toEqual({
|
||||
app: expect.objectContaining({
|
||||
lens: expect.objectContaining({
|
||||
query: { query: '', language: 'lucene' },
|
||||
resolvedDateRange: {
|
||||
fromDate: '2021-01-10T04:00:00.000Z',
|
||||
|
@ -822,49 +777,6 @@ describe('Lens App', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('updates the index patterns when the editor frame is changed', async () => {
|
||||
const { instance, lensStore, services } = await mountWith({});
|
||||
expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
indexPatterns: [],
|
||||
}),
|
||||
{}
|
||||
);
|
||||
await act(async () => {
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
indexPatternsForTopNav: [{ id: '1' }] as IndexPattern[],
|
||||
lastKnownDoc: ({} as unknown) as Document,
|
||||
isSaveable: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
instance.update();
|
||||
expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
indexPatterns: [{ id: '1' }],
|
||||
}),
|
||||
{}
|
||||
);
|
||||
// Do it again to verify that the dirty checking is done right
|
||||
await act(async () => {
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
indexPatternsForTopNav: [{ id: '2' }] as IndexPattern[],
|
||||
lastKnownDoc: ({} as unknown) as Document,
|
||||
isSaveable: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
instance.update();
|
||||
expect(services.navigation.ui.TopNavMenu).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
indexPatterns: [{ id: '2' }],
|
||||
}),
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('updates the editor frame when the user changes query or time in the search bar', async () => {
|
||||
const { instance, services, lensStore } = await mountWith({});
|
||||
(services.data.query.timefilter.timefilter.calculateBounds as jest.Mock).mockReturnValue({
|
||||
|
@ -892,7 +804,7 @@ describe('Lens App', () => {
|
|||
});
|
||||
|
||||
expect(lensStore.getState()).toEqual({
|
||||
app: expect.objectContaining({
|
||||
lens: expect.objectContaining({
|
||||
query: { query: 'new', language: 'lucene' },
|
||||
resolvedDateRange: {
|
||||
fromDate: '2021-01-09T04:00:00.000Z',
|
||||
|
@ -907,7 +819,7 @@ describe('Lens App', () => {
|
|||
const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern;
|
||||
const field = ({ name: 'myfield' } as unknown) as IFieldType;
|
||||
expect(lensStore.getState()).toEqual({
|
||||
app: expect.objectContaining({
|
||||
lens: expect.objectContaining({
|
||||
filters: [],
|
||||
}),
|
||||
});
|
||||
|
@ -918,7 +830,7 @@ describe('Lens App', () => {
|
|||
);
|
||||
instance.update();
|
||||
expect(lensStore.getState()).toEqual({
|
||||
app: expect.objectContaining({
|
||||
lens: expect.objectContaining({
|
||||
filters: [esFilters.buildExistsFilter(field, indexPattern)],
|
||||
}),
|
||||
});
|
||||
|
@ -928,7 +840,7 @@ describe('Lens App', () => {
|
|||
const { instance, services, lensStore } = await mountWith({});
|
||||
|
||||
expect(lensStore.getState()).toEqual({
|
||||
app: expect.objectContaining({
|
||||
lens: expect.objectContaining({
|
||||
searchSessionId: `sessionId-1`,
|
||||
}),
|
||||
});
|
||||
|
@ -942,7 +854,7 @@ describe('Lens App', () => {
|
|||
instance.update();
|
||||
|
||||
expect(lensStore.getState()).toEqual({
|
||||
app: expect.objectContaining({
|
||||
lens: expect.objectContaining({
|
||||
searchSessionId: `sessionId-2`,
|
||||
}),
|
||||
});
|
||||
|
@ -955,7 +867,7 @@ describe('Lens App', () => {
|
|||
);
|
||||
instance.update();
|
||||
expect(lensStore.getState()).toEqual({
|
||||
app: expect.objectContaining({
|
||||
lens: expect.objectContaining({
|
||||
searchSessionId: `sessionId-3`,
|
||||
}),
|
||||
});
|
||||
|
@ -968,7 +880,7 @@ describe('Lens App', () => {
|
|||
);
|
||||
instance.update();
|
||||
expect(lensStore.getState()).toEqual({
|
||||
app: expect.objectContaining({
|
||||
lens: expect.objectContaining({
|
||||
searchSessionId: `sessionId-4`,
|
||||
}),
|
||||
});
|
||||
|
@ -1105,7 +1017,7 @@ describe('Lens App', () => {
|
|||
act(() => instance.find(services.navigation.ui.TopNavMenu).prop('onClearSavedQuery')!());
|
||||
instance.update();
|
||||
expect(lensStore.getState()).toEqual({
|
||||
app: expect.objectContaining({
|
||||
lens: expect.objectContaining({
|
||||
filters: [pinned],
|
||||
}),
|
||||
});
|
||||
|
@ -1137,7 +1049,7 @@ describe('Lens App', () => {
|
|||
});
|
||||
instance.update();
|
||||
expect(lensStore.getState()).toEqual({
|
||||
app: expect.objectContaining({
|
||||
lens: expect.objectContaining({
|
||||
searchSessionId: `sessionId-2`,
|
||||
}),
|
||||
});
|
||||
|
@ -1162,30 +1074,12 @@ describe('Lens App', () => {
|
|||
act(() => instance.find(services.navigation.ui.TopNavMenu).prop('onClearSavedQuery')!());
|
||||
instance.update();
|
||||
expect(lensStore.getState()).toEqual({
|
||||
app: expect.objectContaining({
|
||||
lens: expect.objectContaining({
|
||||
searchSessionId: `sessionId-4`,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
const mockUpdate = {
|
||||
filterableIndexPatterns: [],
|
||||
doc: {
|
||||
title: '',
|
||||
description: '',
|
||||
visualizationType: '',
|
||||
state: {
|
||||
datasourceStates: {},
|
||||
visualization: {},
|
||||
filters: [],
|
||||
query: { query: '', language: 'lucene' },
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
isSaveable: true,
|
||||
activeData: undefined,
|
||||
};
|
||||
|
||||
it('updates the state if session id changes from the outside', async () => {
|
||||
const services = makeDefaultServices(sessionIdSubject);
|
||||
const { lensStore } = await mountWith({ props: undefined, services });
|
||||
|
@ -1197,25 +1091,16 @@ describe('Lens App', () => {
|
|||
await new Promise((r) => setTimeout(r, 0));
|
||||
});
|
||||
expect(lensStore.getState()).toEqual({
|
||||
app: expect.objectContaining({
|
||||
lens: expect.objectContaining({
|
||||
searchSessionId: `new-session-id`,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not update the searchSessionId when the state changes', async () => {
|
||||
const { lensStore } = await mountWith({});
|
||||
act(() => {
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
indexPatternsForTopNav: [],
|
||||
lastKnownDoc: mockUpdate.doc,
|
||||
isSaveable: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
const { lensStore } = await mountWith({ preloadedState: { isSaveable: true } });
|
||||
expect(lensStore.getState()).toEqual({
|
||||
app: expect.objectContaining({
|
||||
lens: expect.objectContaining({
|
||||
searchSessionId: `sessionId-1`,
|
||||
}),
|
||||
});
|
||||
|
@ -1248,20 +1133,7 @@ describe('Lens App', () => {
|
|||
visualize: { save: false, saveQuery: false, show: true },
|
||||
},
|
||||
};
|
||||
const { instance, props, lensStore } = await mountWith({ services });
|
||||
act(() => {
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
indexPatternsForTopNav: [] as IndexPattern[],
|
||||
lastKnownDoc: ({
|
||||
savedObjectId: undefined,
|
||||
references: [],
|
||||
} as unknown) as Document,
|
||||
isSaveable: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
instance.update();
|
||||
const { props } = await mountWith({ services, preloadedState: { isSaveable: true } });
|
||||
const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0];
|
||||
lastCall({ default: defaultLeave, confirm: confirmLeave });
|
||||
expect(defaultLeave).toHaveBeenCalled();
|
||||
|
@ -1269,14 +1141,14 @@ describe('Lens App', () => {
|
|||
});
|
||||
|
||||
it('should confirm when leaving with an unsaved doc', async () => {
|
||||
const { lensStore, props } = await mountWith({});
|
||||
act(() => {
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
lastKnownDoc: ({ savedObjectId: undefined, state: {} } as unknown) as Document,
|
||||
isSaveable: true,
|
||||
})
|
||||
);
|
||||
const { props } = await mountWith({
|
||||
preloadedState: {
|
||||
visualization: {
|
||||
activeId: 'testVis',
|
||||
state: {},
|
||||
},
|
||||
isSaveable: true,
|
||||
},
|
||||
});
|
||||
const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0];
|
||||
lastCall({ default: defaultLeave, confirm: confirmLeave });
|
||||
|
@ -1285,18 +1157,15 @@ describe('Lens App', () => {
|
|||
});
|
||||
|
||||
it('should confirm when leaving with unsaved changes to an existing doc', async () => {
|
||||
const { lensStore, props } = await mountWith({});
|
||||
act(() => {
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
persistedDoc: defaultDoc,
|
||||
lastKnownDoc: ({
|
||||
savedObjectId: defaultSavedObjectId,
|
||||
references: [],
|
||||
} as unknown) as Document,
|
||||
isSaveable: true,
|
||||
})
|
||||
);
|
||||
const { props } = await mountWith({
|
||||
preloadedState: {
|
||||
persistedDoc: defaultDoc,
|
||||
visualization: {
|
||||
activeId: 'testVis',
|
||||
state: {},
|
||||
},
|
||||
isSaveable: true,
|
||||
},
|
||||
});
|
||||
const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0];
|
||||
lastCall({ default: defaultLeave, confirm: confirmLeave });
|
||||
|
@ -1305,15 +1174,23 @@ describe('Lens App', () => {
|
|||
});
|
||||
|
||||
it('should not confirm when changes are saved', async () => {
|
||||
const { lensStore, props } = await mountWith({});
|
||||
act(() => {
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
lastKnownDoc: defaultDoc,
|
||||
persistedDoc: defaultDoc,
|
||||
isSaveable: true,
|
||||
})
|
||||
);
|
||||
const { props } = await mountWith({
|
||||
preloadedState: {
|
||||
persistedDoc: {
|
||||
...defaultDoc,
|
||||
state: {
|
||||
...defaultDoc.state,
|
||||
datasourceStates: { testDatasource: '' },
|
||||
visualization: {},
|
||||
},
|
||||
},
|
||||
isSaveable: true,
|
||||
...(defaultDoc.state as Partial<LensAppState>),
|
||||
visualization: {
|
||||
activeId: 'testVis',
|
||||
state: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0];
|
||||
lastCall({ default: defaultLeave, confirm: confirmLeave });
|
||||
|
@ -1321,16 +1198,13 @@ describe('Lens App', () => {
|
|||
expect(confirmLeave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// not sure how to test it
|
||||
it('should confirm when the latest doc is invalid', async () => {
|
||||
const { lensStore, props } = await mountWith({});
|
||||
act(() => {
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
persistedDoc: defaultDoc,
|
||||
lastKnownDoc: ({
|
||||
savedObjectId: defaultSavedObjectId,
|
||||
references: [],
|
||||
} as unknown) as Document,
|
||||
isSaveable: true,
|
||||
})
|
||||
);
|
||||
|
|
|
@ -10,8 +10,6 @@ import './app.scss';
|
|||
import { isEqual } from 'lodash';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Toast } from 'kibana/public';
|
||||
import { VisualizeFieldContext } from 'src/plugins/ui_actions/public';
|
||||
import { EuiBreadcrumb } from '@elastic/eui';
|
||||
import {
|
||||
createKbnUrlStateStorage,
|
||||
|
@ -24,8 +22,9 @@ import { LensAppProps, LensAppServices } from './types';
|
|||
import { LensTopNavMenu } from './lens_top_nav';
|
||||
import { LensByReferenceInput } from '../embeddable';
|
||||
import { EditorFrameInstance } from '../types';
|
||||
import { Document } from '../persistence/saved_object_store';
|
||||
import {
|
||||
setState as setAppState,
|
||||
setState,
|
||||
useLensSelector,
|
||||
useLensDispatch,
|
||||
LensAppState,
|
||||
|
@ -36,6 +35,7 @@ import {
|
|||
getLastKnownDocWithoutPinnedFilters,
|
||||
runSaveLensVisualization,
|
||||
} from './save_modal_container';
|
||||
import { getSavedObjectFormat } from '../utils';
|
||||
|
||||
export type SaveProps = Omit<OnSaveProps, 'onTitleDuplicate' | 'newDescription'> & {
|
||||
returnToOrigin: boolean;
|
||||
|
@ -54,7 +54,8 @@ export function App({
|
|||
incomingState,
|
||||
redirectToOrigin,
|
||||
setHeaderActionMenu,
|
||||
initialContext,
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
}: LensAppProps) {
|
||||
const lensAppServices = useKibana<LensAppServices>().services;
|
||||
|
||||
|
@ -73,16 +74,69 @@ export function App({
|
|||
|
||||
const dispatch = useLensDispatch();
|
||||
const dispatchSetState: DispatchSetState = useCallback(
|
||||
(state: Partial<LensAppState>) => dispatch(setAppState(state)),
|
||||
(state: Partial<LensAppState>) => dispatch(setState(state)),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const appState = useLensSelector((state) => state.app);
|
||||
const {
|
||||
datasourceStates,
|
||||
visualization,
|
||||
filters,
|
||||
query,
|
||||
activeDatasourceId,
|
||||
persistedDoc,
|
||||
isLinkedToOriginatingApp,
|
||||
searchSessionId,
|
||||
isLoading,
|
||||
isSaveable,
|
||||
} = useLensSelector((state) => state.lens);
|
||||
|
||||
// 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 } = appState;
|
||||
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;
|
||||
}
|
||||
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,
|
||||
]);
|
||||
|
||||
const showNoDataPopover = useCallback(() => {
|
||||
setIndicateNoData(true);
|
||||
|
@ -92,30 +146,17 @@ export function App({
|
|||
if (indicateNoData) {
|
||||
setIndicateNoData(false);
|
||||
}
|
||||
}, [
|
||||
setIndicateNoData,
|
||||
indicateNoData,
|
||||
appState.indexPatternsForTopNav,
|
||||
appState.searchSessionId,
|
||||
]);
|
||||
|
||||
const onError = useCallback(
|
||||
(e: { message: string }) =>
|
||||
notifications.toasts.addDanger({
|
||||
title: e.message,
|
||||
}),
|
||||
[notifications.toasts]
|
||||
);
|
||||
}, [setIndicateNoData, indicateNoData, searchSessionId]);
|
||||
|
||||
const getIsByValueMode = useCallback(
|
||||
() =>
|
||||
Boolean(
|
||||
// Temporarily required until the 'by value' paradigm is default.
|
||||
dashboardFeatureFlag.allowByValueEmbeddables &&
|
||||
appState.isLinkedToOriginatingApp &&
|
||||
isLinkedToOriginatingApp &&
|
||||
!(initialInput as LensByReferenceInput)?.savedObjectId
|
||||
),
|
||||
[dashboardFeatureFlag.allowByValueEmbeddables, appState.isLinkedToOriginatingApp, initialInput]
|
||||
[dashboardFeatureFlag.allowByValueEmbeddables, isLinkedToOriginatingApp, initialInput]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -138,13 +179,11 @@ export function App({
|
|||
onAppLeave((actions) => {
|
||||
// Confirm when the user has made any changes to an existing doc
|
||||
// or when the user has configured something without saving
|
||||
|
||||
if (
|
||||
application.capabilities.visualize.save &&
|
||||
!isEqual(
|
||||
appState.persistedDoc?.state,
|
||||
getLastKnownDocWithoutPinnedFilters(lastKnownDoc)?.state
|
||||
) &&
|
||||
(appState.isSaveable || appState.persistedDoc)
|
||||
!isEqual(persistedDoc?.state, getLastKnownDocWithoutPinnedFilters(lastKnownDoc)?.state) &&
|
||||
(isSaveable || persistedDoc)
|
||||
) {
|
||||
return actions.confirm(
|
||||
i18n.translate('xpack.lens.app.unsavedWorkMessage', {
|
||||
|
@ -158,19 +197,13 @@ export function App({
|
|||
return actions.default();
|
||||
}
|
||||
});
|
||||
}, [
|
||||
onAppLeave,
|
||||
lastKnownDoc,
|
||||
appState.isSaveable,
|
||||
appState.persistedDoc,
|
||||
application.capabilities.visualize.save,
|
||||
]);
|
||||
}, [onAppLeave, lastKnownDoc, isSaveable, persistedDoc, application.capabilities.visualize.save]);
|
||||
|
||||
// Sync Kibana breadcrumbs any time the saved document's title changes
|
||||
useEffect(() => {
|
||||
const isByValueMode = getIsByValueMode();
|
||||
const breadcrumbs: EuiBreadcrumb[] = [];
|
||||
if (appState.isLinkedToOriginatingApp && getOriginatingAppName() && redirectToOrigin) {
|
||||
if (isLinkedToOriginatingApp && getOriginatingAppName() && redirectToOrigin) {
|
||||
breadcrumbs.push({
|
||||
onClick: () => {
|
||||
redirectToOrigin();
|
||||
|
@ -193,10 +226,10 @@ export function App({
|
|||
let currentDocTitle = i18n.translate('xpack.lens.breadcrumbsCreate', {
|
||||
defaultMessage: 'Create',
|
||||
});
|
||||
if (appState.persistedDoc) {
|
||||
if (persistedDoc) {
|
||||
currentDocTitle = isByValueMode
|
||||
? i18n.translate('xpack.lens.breadcrumbsByValue', { defaultMessage: 'Edit visualization' })
|
||||
: appState.persistedDoc.title;
|
||||
: persistedDoc.title;
|
||||
}
|
||||
breadcrumbs.push({ text: currentDocTitle });
|
||||
chrome.setBreadcrumbs(breadcrumbs);
|
||||
|
@ -207,39 +240,55 @@ export function App({
|
|||
getIsByValueMode,
|
||||
application,
|
||||
chrome,
|
||||
appState.isLinkedToOriginatingApp,
|
||||
appState.persistedDoc,
|
||||
isLinkedToOriginatingApp,
|
||||
persistedDoc,
|
||||
]);
|
||||
|
||||
const runSave = (saveProps: SaveProps, options: { saveToLibrary: boolean }) => {
|
||||
return runSaveLensVisualization(
|
||||
{
|
||||
lastKnownDoc,
|
||||
getIsByValueMode,
|
||||
savedObjectsTagging,
|
||||
initialInput,
|
||||
redirectToOrigin,
|
||||
persistedDoc: appState.persistedDoc,
|
||||
onAppLeave,
|
||||
redirectTo,
|
||||
originatingApp: incomingState?.originatingApp,
|
||||
...lensAppServices,
|
||||
},
|
||||
saveProps,
|
||||
options
|
||||
).then(
|
||||
(newState) => {
|
||||
if (newState) {
|
||||
dispatchSetState(newState);
|
||||
setIsSaveModalVisible(false);
|
||||
const runSave = useCallback(
|
||||
(saveProps: SaveProps, options: { saveToLibrary: boolean }) => {
|
||||
return runSaveLensVisualization(
|
||||
{
|
||||
lastKnownDoc,
|
||||
getIsByValueMode,
|
||||
savedObjectsTagging,
|
||||
initialInput,
|
||||
redirectToOrigin,
|
||||
persistedDoc,
|
||||
onAppLeave,
|
||||
redirectTo,
|
||||
originatingApp: incomingState?.originatingApp,
|
||||
...lensAppServices,
|
||||
},
|
||||
saveProps,
|
||||
options
|
||||
).then(
|
||||
(newState) => {
|
||||
if (newState) {
|
||||
dispatchSetState(newState);
|
||||
setIsSaveModalVisible(false);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
// error is handled inside the modal
|
||||
// so ignoring it here
|
||||
}
|
||||
},
|
||||
() => {
|
||||
// error is handled inside the modal
|
||||
// so ignoring it here
|
||||
}
|
||||
);
|
||||
};
|
||||
);
|
||||
},
|
||||
[
|
||||
incomingState?.originatingApp,
|
||||
lastKnownDoc,
|
||||
persistedDoc,
|
||||
getIsByValueMode,
|
||||
savedObjectsTagging,
|
||||
initialInput,
|
||||
redirectToOrigin,
|
||||
onAppLeave,
|
||||
redirectTo,
|
||||
lensAppServices,
|
||||
dispatchSetState,
|
||||
setIsSaveModalVisible,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -253,64 +302,53 @@ export function App({
|
|||
setIsSaveModalVisible={setIsSaveModalVisible}
|
||||
setHeaderActionMenu={setHeaderActionMenu}
|
||||
indicateNoData={indicateNoData}
|
||||
datasourceMap={datasourceMap}
|
||||
title={persistedDoc?.title}
|
||||
/>
|
||||
{(!appState.isAppLoading || appState.persistedDoc) && (
|
||||
{(!isLoading || persistedDoc) && (
|
||||
<MemoizedEditorFrameWrapper
|
||||
editorFrame={editorFrame}
|
||||
onError={onError}
|
||||
showNoDataPopover={showNoDataPopover}
|
||||
initialContext={initialContext}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<SaveModalContainer
|
||||
isVisible={isSaveModalVisible}
|
||||
lensServices={lensAppServices}
|
||||
originatingApp={
|
||||
appState.isLinkedToOriginatingApp ? incomingState?.originatingApp : undefined
|
||||
}
|
||||
isSaveable={appState.isSaveable}
|
||||
runSave={runSave}
|
||||
onClose={() => {
|
||||
setIsSaveModalVisible(false);
|
||||
}}
|
||||
getAppNameFromId={() => getOriginatingAppName()}
|
||||
lastKnownDoc={lastKnownDoc}
|
||||
onAppLeave={onAppLeave}
|
||||
persistedDoc={appState.persistedDoc}
|
||||
initialInput={initialInput}
|
||||
redirectTo={redirectTo}
|
||||
redirectToOrigin={redirectToOrigin}
|
||||
returnToOriginSwitchLabel={
|
||||
getIsByValueMode() && initialInput
|
||||
? i18n.translate('xpack.lens.app.updatePanel', {
|
||||
defaultMessage: 'Update panel on {originatingAppName}',
|
||||
values: { originatingAppName: getOriginatingAppName() },
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{isSaveModalVisible && (
|
||||
<SaveModalContainer
|
||||
lensServices={lensAppServices}
|
||||
originatingApp={isLinkedToOriginatingApp ? incomingState?.originatingApp : undefined}
|
||||
isSaveable={isSaveable}
|
||||
runSave={runSave}
|
||||
onClose={() => {
|
||||
setIsSaveModalVisible(false);
|
||||
}}
|
||||
getAppNameFromId={() => getOriginatingAppName()}
|
||||
lastKnownDoc={lastKnownDoc}
|
||||
onAppLeave={onAppLeave}
|
||||
persistedDoc={persistedDoc}
|
||||
initialInput={initialInput}
|
||||
redirectTo={redirectTo}
|
||||
redirectToOrigin={redirectToOrigin}
|
||||
returnToOriginSwitchLabel={
|
||||
getIsByValueMode() && initialInput
|
||||
? i18n.translate('xpack.lens.app.updatePanel', {
|
||||
defaultMessage: 'Update panel on {originatingAppName}',
|
||||
values: { originatingAppName: getOriginatingAppName() },
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const MemoizedEditorFrameWrapper = React.memo(function EditorFrameWrapper({
|
||||
editorFrame,
|
||||
onError,
|
||||
showNoDataPopover,
|
||||
initialContext,
|
||||
}: {
|
||||
editorFrame: EditorFrameInstance;
|
||||
onError: (e: { message: string }) => Toast;
|
||||
showNoDataPopover: () => void;
|
||||
initialContext: VisualizeFieldContext | undefined;
|
||||
}) {
|
||||
const { EditorFrameContainer } = editorFrame;
|
||||
return (
|
||||
<EditorFrameContainer
|
||||
onError={onError}
|
||||
showNoDataPopover={showNoDataPopover}
|
||||
initialContext={initialContext}
|
||||
/>
|
||||
);
|
||||
return <EditorFrameContainer showNoDataPopover={showNoDataPopover} />;
|
||||
});
|
||||
|
|
|
@ -7,21 +7,21 @@
|
|||
|
||||
import { isEqual } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { TopNavMenuData } from '../../../../../src/plugins/navigation/public';
|
||||
import { LensAppServices, LensTopNavActions, LensTopNavMenuProps } from './types';
|
||||
import { downloadMultipleAs } from '../../../../../src/plugins/share/public';
|
||||
import { trackUiEvent } from '../lens_ui_telemetry';
|
||||
import { exporters } from '../../../../../src/plugins/data/public';
|
||||
|
||||
import { exporters, IndexPattern } from '../../../../../src/plugins/data/public';
|
||||
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
|
||||
import {
|
||||
setState as setAppState,
|
||||
setState,
|
||||
useLensSelector,
|
||||
useLensDispatch,
|
||||
LensAppState,
|
||||
DispatchSetState,
|
||||
} from '../state_management';
|
||||
import { getIndexPatternsObjects, getIndexPatternsIds } from '../utils';
|
||||
|
||||
function getLensTopNavConfig(options: {
|
||||
showSaveAndReturn: boolean;
|
||||
|
@ -127,6 +127,8 @@ export const LensTopNavMenu = ({
|
|||
runSave,
|
||||
onAppLeave,
|
||||
redirectToOrigin,
|
||||
datasourceMap,
|
||||
title,
|
||||
}: LensTopNavMenuProps) => {
|
||||
const {
|
||||
data,
|
||||
|
@ -139,19 +141,52 @@ export const LensTopNavMenu = ({
|
|||
|
||||
const dispatch = useLensDispatch();
|
||||
const dispatchSetState: DispatchSetState = React.useCallback(
|
||||
(state: Partial<LensAppState>) => dispatch(setAppState(state)),
|
||||
(state: Partial<LensAppState>) => dispatch(setState(state)),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const [indexPatterns, setIndexPatterns] = useState<IndexPattern[]>([]);
|
||||
|
||||
const {
|
||||
isSaveable,
|
||||
isLinkedToOriginatingApp,
|
||||
indexPatternsForTopNav,
|
||||
query,
|
||||
lastKnownDoc,
|
||||
activeData,
|
||||
savedQuery,
|
||||
} = useLensSelector((state) => state.app);
|
||||
activeDatasourceId,
|
||||
datasourceStates,
|
||||
} = useLensSelector((state) => state.lens);
|
||||
|
||||
useEffect(() => {
|
||||
const activeDatasource =
|
||||
datasourceMap && activeDatasourceId && !datasourceStates[activeDatasourceId].isLoading
|
||||
? datasourceMap[activeDatasourceId]
|
||||
: undefined;
|
||||
if (!activeDatasource) {
|
||||
return;
|
||||
}
|
||||
const indexPatternIds = getIndexPatternsIds({
|
||||
activeDatasources: Object.keys(datasourceStates).reduce(
|
||||
(acc, datasourceId) => ({
|
||||
...acc,
|
||||
[datasourceId]: datasourceMap[datasourceId],
|
||||
}),
|
||||
{}
|
||||
),
|
||||
datasourceStates,
|
||||
});
|
||||
const hasIndexPatternsChanged =
|
||||
indexPatterns.length !== indexPatternIds.length ||
|
||||
indexPatternIds.some((id) => !indexPatterns.find((indexPattern) => indexPattern.id === id));
|
||||
// Update the cached index patterns if the user made a change to any of them
|
||||
if (hasIndexPatternsChanged) {
|
||||
getIndexPatternsObjects(indexPatternIds, data.indexPatterns).then(
|
||||
({ indexPatterns: indexPatternObjects }) => {
|
||||
setIndexPatterns(indexPatternObjects);
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [datasourceStates, activeDatasourceId, data.indexPatterns, datasourceMap, indexPatterns]);
|
||||
|
||||
const { TopNavMenu } = navigation.ui;
|
||||
const { from, to } = data.query.timefilter.timefilter.getTime();
|
||||
|
@ -190,7 +225,7 @@ export const LensTopNavMenu = ({
|
|||
if (datatable) {
|
||||
const postFix = datatables.length > 1 ? `-${i + 1}` : '';
|
||||
|
||||
memo[`${lastKnownDoc?.title || unsavedTitle}${postFix}.csv`] = {
|
||||
memo[`${title || unsavedTitle}${postFix}.csv`] = {
|
||||
content: exporters.datatableToCSV(datatable, {
|
||||
csvSeparator: uiSettings.get('csv:separator', ','),
|
||||
quoteValues: uiSettings.get('csv:quoteValues', true),
|
||||
|
@ -208,14 +243,14 @@ export const LensTopNavMenu = ({
|
|||
}
|
||||
},
|
||||
saveAndReturn: () => {
|
||||
if (savingToDashboardPermitted && lastKnownDoc) {
|
||||
if (savingToDashboardPermitted) {
|
||||
// disabling the validation on app leave because the document has been saved.
|
||||
onAppLeave((actions) => {
|
||||
return actions.default();
|
||||
});
|
||||
runSave(
|
||||
{
|
||||
newTitle: lastKnownDoc.title,
|
||||
newTitle: title || '',
|
||||
newCopyOnSave: false,
|
||||
isTitleDuplicateConfirmed: false,
|
||||
returnToOrigin: true,
|
||||
|
@ -248,7 +283,7 @@ export const LensTopNavMenu = ({
|
|||
initialInput,
|
||||
isLinkedToOriginatingApp,
|
||||
isSaveable,
|
||||
lastKnownDoc,
|
||||
title,
|
||||
onAppLeave,
|
||||
redirectToOrigin,
|
||||
runSave,
|
||||
|
@ -321,7 +356,7 @@ export const LensTopNavMenu = ({
|
|||
onSaved={onSavedWrapped}
|
||||
onSavedQueryUpdated={onSavedQueryUpdatedWrapped}
|
||||
onClearSavedQuery={onClearSavedQueryWrapped}
|
||||
indexPatterns={indexPatternsForTopNav}
|
||||
indexPatterns={indexPatterns}
|
||||
query={query}
|
||||
dateRangeFrom={from}
|
||||
dateRangeTo={to}
|
||||
|
|
|
@ -4,45 +4,150 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { makeDefaultServices, mockLensStore } from '../mocks';
|
||||
import { makeDefaultServices, makeLensStore, defaultDoc, createMockVisualization } from '../mocks';
|
||||
import { createMockDatasource, DatasourceMock } from '../mocks';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { loadDocument } from './mounter';
|
||||
import { loadInitialStore } from './mounter';
|
||||
import { LensEmbeddableInput } from '../embeddable/embeddable';
|
||||
|
||||
const defaultSavedObjectId = '1234';
|
||||
const preloadedState = {
|
||||
isLoading: true,
|
||||
visualization: {
|
||||
state: null,
|
||||
activeId: 'testVis',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Mounter', () => {
|
||||
const byValueFlag = { allowByValueEmbeddables: true };
|
||||
describe('loadDocument', () => {
|
||||
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 mockVisualization2 = {
|
||||
...createMockVisualization(),
|
||||
id: 'testVis2',
|
||||
visualizationTypes: [
|
||||
{
|
||||
icon: 'empty',
|
||||
id: 'testVis2',
|
||||
label: 'TEST2',
|
||||
groupLabel: 'testVis2Group',
|
||||
},
|
||||
],
|
||||
};
|
||||
const visualizationMap = {
|
||||
testVis: mockVisualization,
|
||||
testVis2: mockVisualization2,
|
||||
};
|
||||
|
||||
it('should initialize initial datasource', async () => {
|
||||
const services = makeDefaultServices();
|
||||
const redirectCallback = jest.fn();
|
||||
services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc);
|
||||
|
||||
const lensStore = await makeLensStore({
|
||||
data: services.data,
|
||||
preloadedState,
|
||||
});
|
||||
await act(async () => {
|
||||
await loadInitialStore(
|
||||
redirectCallback,
|
||||
undefined,
|
||||
services,
|
||||
lensStore,
|
||||
undefined,
|
||||
byValueFlag,
|
||||
datasourceMap,
|
||||
visualizationMap
|
||||
);
|
||||
});
|
||||
expect(mockDatasource.initialize).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should have initialized only the initial datasource and visualization', async () => {
|
||||
const services = makeDefaultServices();
|
||||
const redirectCallback = jest.fn();
|
||||
services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc);
|
||||
|
||||
const lensStore = await makeLensStore({ data: services.data, preloadedState });
|
||||
await act(async () => {
|
||||
await loadInitialStore(
|
||||
redirectCallback,
|
||||
undefined,
|
||||
services,
|
||||
lensStore,
|
||||
undefined,
|
||||
byValueFlag,
|
||||
datasourceMap,
|
||||
visualizationMap
|
||||
);
|
||||
});
|
||||
expect(mockDatasource.initialize).toHaveBeenCalled();
|
||||
expect(mockDatasource2.initialize).not.toHaveBeenCalled();
|
||||
|
||||
expect(mockVisualization.initialize).toHaveBeenCalled();
|
||||
expect(mockVisualization2.initialize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// it('should initialize all datasources with state from doc', async () => {})
|
||||
// it('should pass the datasource api for each layer to the visualization', async () => {})
|
||||
// it('should create a separate datasource public api for each layer', async () => {})
|
||||
// it('should not initialize visualization before datasource is initialized', async () => {})
|
||||
// it('should pass the public frame api into visualization initialize', async () => {})
|
||||
// it('should fetch suggestions of currently active datasource when initializes from visualization trigger', async () => {})
|
||||
// it.skip('should pass the datasource api for each layer to the visualization', async () => {})
|
||||
// it('displays errors from the frame in a toast', async () => {
|
||||
|
||||
describe('loadInitialStore', () => {
|
||||
it('does not load a document if there is no initial input', async () => {
|
||||
const services = makeDefaultServices();
|
||||
const redirectCallback = jest.fn();
|
||||
const lensStore = mockLensStore({ data: services.data });
|
||||
await loadDocument(redirectCallback, undefined, services, lensStore, undefined, byValueFlag);
|
||||
const lensStore = makeLensStore({ data: services.data, preloadedState });
|
||||
await loadInitialStore(
|
||||
redirectCallback,
|
||||
undefined,
|
||||
services,
|
||||
lensStore,
|
||||
undefined,
|
||||
byValueFlag,
|
||||
datasourceMap,
|
||||
visualizationMap
|
||||
);
|
||||
expect(services.attributeService.unwrapAttributes).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('loads a document and uses query and filters if initial input is provided', async () => {
|
||||
const services = makeDefaultServices();
|
||||
const redirectCallback = jest.fn();
|
||||
services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({
|
||||
savedObjectId: defaultSavedObjectId,
|
||||
state: {
|
||||
query: 'fake query',
|
||||
filters: [{ query: { match_phrase: { src: 'test' } } }],
|
||||
},
|
||||
references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }],
|
||||
});
|
||||
services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc);
|
||||
|
||||
const lensStore = await mockLensStore({ data: services.data });
|
||||
const lensStore = await makeLensStore({ data: services.data, preloadedState });
|
||||
await act(async () => {
|
||||
await loadDocument(
|
||||
await loadInitialStore(
|
||||
redirectCallback,
|
||||
{ savedObjectId: defaultSavedObjectId } as LensEmbeddableInput,
|
||||
services,
|
||||
lensStore,
|
||||
undefined,
|
||||
byValueFlag
|
||||
byValueFlag,
|
||||
datasourceMap,
|
||||
visualizationMap
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -50,21 +155,16 @@ describe('Mounter', () => {
|
|||
savedObjectId: defaultSavedObjectId,
|
||||
});
|
||||
|
||||
expect(services.data.indexPatterns.get).toHaveBeenCalledWith('1');
|
||||
|
||||
expect(services.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([
|
||||
{ query: { match_phrase: { src: 'test' } } },
|
||||
]);
|
||||
|
||||
expect(lensStore.getState()).toEqual({
|
||||
app: expect.objectContaining({
|
||||
persistedDoc: expect.objectContaining({
|
||||
savedObjectId: defaultSavedObjectId,
|
||||
state: expect.objectContaining({
|
||||
query: 'fake query',
|
||||
filters: [{ query: { match_phrase: { src: 'test' } } }],
|
||||
}),
|
||||
}),
|
||||
lens: expect.objectContaining({
|
||||
persistedDoc: { ...defaultDoc, type: 'lens' },
|
||||
query: 'kuery',
|
||||
isLoading: false,
|
||||
activeDatasourceId: 'testDatasource',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
@ -72,40 +172,46 @@ describe('Mounter', () => {
|
|||
it('does not load documents on sequential renders unless the id changes', async () => {
|
||||
const redirectCallback = jest.fn();
|
||||
const services = makeDefaultServices();
|
||||
const lensStore = mockLensStore({ data: services.data });
|
||||
const lensStore = makeLensStore({ data: services.data, preloadedState });
|
||||
|
||||
await act(async () => {
|
||||
await loadDocument(
|
||||
await loadInitialStore(
|
||||
redirectCallback,
|
||||
{ savedObjectId: defaultSavedObjectId } as LensEmbeddableInput,
|
||||
services,
|
||||
lensStore,
|
||||
undefined,
|
||||
byValueFlag
|
||||
byValueFlag,
|
||||
datasourceMap,
|
||||
visualizationMap
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await loadDocument(
|
||||
await loadInitialStore(
|
||||
redirectCallback,
|
||||
{ savedObjectId: defaultSavedObjectId } as LensEmbeddableInput,
|
||||
services,
|
||||
lensStore,
|
||||
undefined,
|
||||
byValueFlag
|
||||
byValueFlag,
|
||||
datasourceMap,
|
||||
visualizationMap
|
||||
);
|
||||
});
|
||||
|
||||
expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
await loadDocument(
|
||||
await loadInitialStore(
|
||||
redirectCallback,
|
||||
{ savedObjectId: '5678' } as LensEmbeddableInput,
|
||||
services,
|
||||
lensStore,
|
||||
undefined,
|
||||
byValueFlag
|
||||
byValueFlag,
|
||||
datasourceMap,
|
||||
visualizationMap
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -116,18 +222,20 @@ describe('Mounter', () => {
|
|||
const services = makeDefaultServices();
|
||||
const redirectCallback = jest.fn();
|
||||
|
||||
const lensStore = mockLensStore({ data: services.data });
|
||||
const lensStore = makeLensStore({ data: services.data, preloadedState });
|
||||
|
||||
services.attributeService.unwrapAttributes = jest.fn().mockRejectedValue('failed to load');
|
||||
|
||||
await act(async () => {
|
||||
await loadDocument(
|
||||
await loadInitialStore(
|
||||
redirectCallback,
|
||||
{ savedObjectId: defaultSavedObjectId } as LensEmbeddableInput,
|
||||
services,
|
||||
lensStore,
|
||||
undefined,
|
||||
byValueFlag
|
||||
byValueFlag,
|
||||
datasourceMap,
|
||||
visualizationMap
|
||||
);
|
||||
});
|
||||
expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({
|
||||
|
@ -141,15 +249,17 @@ describe('Mounter', () => {
|
|||
const redirectCallback = jest.fn();
|
||||
|
||||
const services = makeDefaultServices();
|
||||
const lensStore = mockLensStore({ data: services.data });
|
||||
const lensStore = makeLensStore({ data: services.data, preloadedState });
|
||||
await act(async () => {
|
||||
await loadDocument(
|
||||
await loadInitialStore(
|
||||
redirectCallback,
|
||||
({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput,
|
||||
services,
|
||||
lensStore,
|
||||
undefined,
|
||||
byValueFlag
|
||||
byValueFlag,
|
||||
datasourceMap,
|
||||
visualizationMap
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ import { Storage } from '../../../../../src/plugins/kibana_utils/public';
|
|||
import { LensReportManager, setReportManager, trackUiEvent } from '../lens_ui_telemetry';
|
||||
|
||||
import { App } from './app';
|
||||
import { EditorFrameStart } from '../types';
|
||||
import { Datasource, EditorFrameStart, Visualization } from '../types';
|
||||
import { addHelpMenuToAppChrome } from '../help_menu_util';
|
||||
import { LensPluginStartDependencies } from '../plugin';
|
||||
import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID } from '../../common';
|
||||
|
@ -32,7 +32,10 @@ import {
|
|||
LensByReferenceInput,
|
||||
LensByValueInput,
|
||||
} from '../embeddable/embeddable';
|
||||
import { ACTION_VISUALIZE_LENS_FIELD } from '../../../../../src/plugins/ui_actions/public';
|
||||
import {
|
||||
ACTION_VISUALIZE_LENS_FIELD,
|
||||
VisualizeFieldContext,
|
||||
} from '../../../../../src/plugins/ui_actions/public';
|
||||
import { LensAttributeService } from '../lens_attribute_service';
|
||||
import { LensAppServices, RedirectToOriginProps, HistoryLocationState } from './types';
|
||||
import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
|
||||
|
@ -43,9 +46,18 @@ import {
|
|||
getPreloadedState,
|
||||
LensRootStore,
|
||||
setState,
|
||||
LensAppState,
|
||||
updateLayer,
|
||||
updateVisualizationState,
|
||||
} from '../state_management';
|
||||
import { getResolvedDateRange } from '../utils';
|
||||
import { getLastKnownDoc } from './save_modal_container';
|
||||
import { getPersistedDoc } from './save_modal_container';
|
||||
import { getResolvedDateRange, getInitialDatasourceId } from '../utils';
|
||||
import { initializeDatasources } from '../editor_frame_service/editor_frame';
|
||||
import { generateId } from '../id_generator';
|
||||
import {
|
||||
getVisualizeFieldSuggestions,
|
||||
switchToSuggestion,
|
||||
} from '../editor_frame_service/editor_frame/suggestion_helpers';
|
||||
|
||||
export async function getLensServices(
|
||||
coreStart: CoreStart,
|
||||
|
@ -166,7 +178,19 @@ export async function mountApp(
|
|||
if (!initialContext) {
|
||||
data.query.filterManager.setAppFilters([]);
|
||||
}
|
||||
const { datasourceMap, visualizationMap } = instance;
|
||||
|
||||
const initialDatasourceId = getInitialDatasourceId(datasourceMap);
|
||||
const datasourceStates: LensAppState['datasourceStates'] = {};
|
||||
if (initialDatasourceId) {
|
||||
datasourceStates[initialDatasourceId] = {
|
||||
state: null,
|
||||
isLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
const preloadedState = getPreloadedState({
|
||||
isLoading: true,
|
||||
query: data.query.queryString.getQuery(),
|
||||
// Do not use app-specific filters from previous app,
|
||||
// only if Lens was opened with the intention to visualize a field (e.g. coming from Discover)
|
||||
|
@ -176,10 +200,15 @@ export async function mountApp(
|
|||
searchSessionId: data.search.session.getSessionId(),
|
||||
resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter),
|
||||
isLinkedToOriginatingApp: Boolean(embeddableEditorIncomingState?.originatingApp),
|
||||
activeDatasourceId: initialDatasourceId,
|
||||
datasourceStates,
|
||||
visualization: {
|
||||
state: null,
|
||||
activeId: Object.keys(visualizationMap)[0] || null,
|
||||
},
|
||||
});
|
||||
|
||||
const lensStore: LensRootStore = makeConfigureStore(preloadedState, { data });
|
||||
|
||||
const EditorRenderer = React.memo(
|
||||
(props: { id?: string; history: History<unknown>; editByValue?: boolean }) => {
|
||||
const redirectCallback = useCallback(
|
||||
|
@ -190,14 +219,18 @@ export async function mountApp(
|
|||
);
|
||||
trackUiEvent('loaded');
|
||||
const initialInput = getInitialInput(props.id, props.editByValue);
|
||||
loadDocument(
|
||||
loadInitialStore(
|
||||
redirectCallback,
|
||||
initialInput,
|
||||
lensServices,
|
||||
lensStore,
|
||||
embeddableEditorIncomingState,
|
||||
dashboardFeatureFlag
|
||||
dashboardFeatureFlag,
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
initialContext
|
||||
);
|
||||
|
||||
return (
|
||||
<Provider store={lensStore}>
|
||||
<App
|
||||
|
@ -209,7 +242,8 @@ export async function mountApp(
|
|||
onAppLeave={params.onAppLeave}
|
||||
setHeaderActionMenu={params.setHeaderActionMenu}
|
||||
history={props.history}
|
||||
initialContext={initialContext}
|
||||
datasourceMap={datasourceMap}
|
||||
visualizationMap={visualizationMap}
|
||||
/>
|
||||
</Provider>
|
||||
);
|
||||
|
@ -270,64 +304,180 @@ export async function mountApp(
|
|||
};
|
||||
}
|
||||
|
||||
export function loadDocument(
|
||||
export function loadInitialStore(
|
||||
redirectCallback: (savedObjectId?: string) => void,
|
||||
initialInput: LensEmbeddableInput | undefined,
|
||||
lensServices: LensAppServices,
|
||||
lensStore: LensRootStore,
|
||||
embeddableEditorIncomingState: EmbeddableEditorState | undefined,
|
||||
dashboardFeatureFlag: DashboardFeatureFlagConfig
|
||||
dashboardFeatureFlag: DashboardFeatureFlagConfig,
|
||||
datasourceMap: Record<string, Datasource>,
|
||||
visualizationMap: Record<string, Visualization>,
|
||||
initialContext?: VisualizeFieldContext
|
||||
) {
|
||||
const { attributeService, chrome, notifications, data } = lensServices;
|
||||
const { persistedDoc } = lensStore.getState().app;
|
||||
const { persistedDoc } = lensStore.getState().lens;
|
||||
if (
|
||||
!initialInput ||
|
||||
(attributeService.inputIsRefType(initialInput) &&
|
||||
initialInput.savedObjectId === persistedDoc?.savedObjectId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
lensStore.dispatch(setState({ isAppLoading: true }));
|
||||
return initializeDatasources(
|
||||
datasourceMap,
|
||||
lensStore.getState().lens.datasourceStates,
|
||||
undefined,
|
||||
initialContext,
|
||||
{
|
||||
isFullEditor: true,
|
||||
}
|
||||
)
|
||||
.then((result) => {
|
||||
const datasourceStates = Object.entries(result).reduce(
|
||||
(state, [datasourceId, datasourceState]) => ({
|
||||
...state,
|
||||
[datasourceId]: {
|
||||
...datasourceState,
|
||||
isLoading: false,
|
||||
},
|
||||
}),
|
||||
{}
|
||||
);
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
datasourceStates,
|
||||
isLoading: false,
|
||||
})
|
||||
);
|
||||
if (initialContext) {
|
||||
const selectedSuggestion = getVisualizeFieldSuggestions({
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
visualizationMap,
|
||||
activeVisualizationId: Object.keys(visualizationMap)[0] || null,
|
||||
visualizationState: null,
|
||||
visualizeTriggerFieldContext: initialContext,
|
||||
});
|
||||
if (selectedSuggestion) {
|
||||
switchToSuggestion(lensStore.dispatch, selectedSuggestion, 'SWITCH_VISUALIZATION');
|
||||
}
|
||||
}
|
||||
const activeDatasourceId = getInitialDatasourceId(datasourceMap);
|
||||
const visualization = lensStore.getState().lens.visualization;
|
||||
const activeVisualization =
|
||||
visualization.activeId && visualizationMap[visualization.activeId];
|
||||
|
||||
getLastKnownDoc({
|
||||
if (visualization.state === null && activeVisualization) {
|
||||
const newLayerId = generateId();
|
||||
|
||||
const initialVisualizationState = activeVisualization.initialize(() => newLayerId);
|
||||
lensStore.dispatch(
|
||||
updateLayer({
|
||||
datasourceId: activeDatasourceId!,
|
||||
layerId: newLayerId,
|
||||
updater: datasourceMap[activeDatasourceId!].insertLayer,
|
||||
})
|
||||
);
|
||||
lensStore.dispatch(
|
||||
updateVisualizationState({
|
||||
visualizationId: activeVisualization.id,
|
||||
updater: initialVisualizationState,
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((e: { message: string }) => {
|
||||
notifications.toasts.addDanger({
|
||||
title: e.message,
|
||||
});
|
||||
redirectCallback();
|
||||
});
|
||||
}
|
||||
|
||||
getPersistedDoc({
|
||||
initialInput,
|
||||
attributeService,
|
||||
data,
|
||||
chrome,
|
||||
notifications,
|
||||
}).then(
|
||||
(newState) => {
|
||||
if (newState) {
|
||||
const { doc, indexPatterns } = newState;
|
||||
const currentSessionId = data.search.session.getSessionId();
|
||||
})
|
||||
.then(
|
||||
(doc) => {
|
||||
if (doc) {
|
||||
const currentSessionId = data.search.session.getSessionId();
|
||||
const docDatasourceStates = Object.entries(doc.state.datasourceStates).reduce(
|
||||
(stateMap, [datasourceId, datasourceState]) => ({
|
||||
...stateMap,
|
||||
[datasourceId]: {
|
||||
isLoading: true,
|
||||
state: datasourceState,
|
||||
},
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
initializeDatasources(
|
||||
datasourceMap,
|
||||
docDatasourceStates,
|
||||
doc.references,
|
||||
initialContext,
|
||||
{
|
||||
isFullEditor: true,
|
||||
}
|
||||
)
|
||||
.then((result) => {
|
||||
const activeDatasourceId = getInitialDatasourceId(datasourceMap, doc);
|
||||
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
query: doc.state.query,
|
||||
searchSessionId:
|
||||
dashboardFeatureFlag.allowByValueEmbeddables &&
|
||||
Boolean(embeddableEditorIncomingState?.originatingApp) &&
|
||||
!(initialInput as LensByReferenceInput)?.savedObjectId &&
|
||||
currentSessionId
|
||||
? currentSessionId
|
||||
: data.search.session.start(),
|
||||
...(!isEqual(persistedDoc, doc) ? { persistedDoc: doc } : null),
|
||||
activeDatasourceId,
|
||||
visualization: {
|
||||
activeId: doc.visualizationType,
|
||||
state: doc.state.visualization,
|
||||
},
|
||||
datasourceStates: Object.entries(result).reduce(
|
||||
(state, [datasourceId, datasourceState]) => ({
|
||||
...state,
|
||||
[datasourceId]: {
|
||||
...datasourceState,
|
||||
isLoading: false,
|
||||
},
|
||||
}),
|
||||
{}
|
||||
),
|
||||
isLoading: false,
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((e: { message: string }) =>
|
||||
notifications.toasts.addDanger({
|
||||
title: e.message,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
redirectCallback();
|
||||
}
|
||||
},
|
||||
() => {
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
query: doc.state.query,
|
||||
isAppLoading: false,
|
||||
indexPatternsForTopNav: indexPatterns,
|
||||
lastKnownDoc: doc,
|
||||
searchSessionId:
|
||||
dashboardFeatureFlag.allowByValueEmbeddables &&
|
||||
Boolean(embeddableEditorIncomingState?.originatingApp) &&
|
||||
!(initialInput as LensByReferenceInput)?.savedObjectId &&
|
||||
currentSessionId
|
||||
? currentSessionId
|
||||
: data.search.session.start(),
|
||||
...(!isEqual(persistedDoc, doc) ? { persistedDoc: doc } : null),
|
||||
isLoading: false,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
redirectCallback();
|
||||
}
|
||||
},
|
||||
() => {
|
||||
lensStore.dispatch(
|
||||
setState({
|
||||
isAppLoading: false,
|
||||
})
|
||||
);
|
||||
|
||||
redirectCallback();
|
||||
}
|
||||
);
|
||||
)
|
||||
.catch((e: { message: string }) =>
|
||||
notifications.toasts.addDanger({
|
||||
title: e.message,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { Document } from '../persistence';
|
||||
import type { SavedObjectTaggingPluginStart } from '../../../saved_objects_tagging/public';
|
||||
|
||||
import {
|
||||
|
@ -23,7 +21,6 @@ import {
|
|||
export type SaveProps = OriginSaveProps | DashboardSaveProps;
|
||||
|
||||
export interface Props {
|
||||
isVisible: boolean;
|
||||
savingToLibraryPermitted?: boolean;
|
||||
|
||||
originatingApp?: string;
|
||||
|
@ -32,7 +29,9 @@ export interface Props {
|
|||
savedObjectsTagging?: SavedObjectTaggingPluginStart;
|
||||
tagsIds: string[];
|
||||
|
||||
lastKnownDoc?: Document;
|
||||
title?: string;
|
||||
savedObjectId?: string;
|
||||
description?: string;
|
||||
|
||||
getAppNameFromId: () => string | undefined;
|
||||
returnToOriginSwitchLabel?: string;
|
||||
|
@ -42,16 +41,14 @@ export interface Props {
|
|||
}
|
||||
|
||||
export const SaveModal = (props: Props) => {
|
||||
if (!props.isVisible || !props.lastKnownDoc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
originatingApp,
|
||||
savingToLibraryPermitted,
|
||||
savedObjectsTagging,
|
||||
tagsIds,
|
||||
lastKnownDoc,
|
||||
savedObjectId,
|
||||
title,
|
||||
description,
|
||||
allowByValueEmbeddables,
|
||||
returnToOriginSwitchLabel,
|
||||
getAppNameFromId,
|
||||
|
@ -70,9 +67,9 @@ export const SaveModal = (props: Props) => {
|
|||
onSave={(saveProps) => onSave(saveProps, { saveToLibrary: true })}
|
||||
getAppNameFromId={getAppNameFromId}
|
||||
documentInfo={{
|
||||
id: lastKnownDoc.savedObjectId,
|
||||
title: lastKnownDoc.title || '',
|
||||
description: lastKnownDoc.description || '',
|
||||
id: savedObjectId,
|
||||
title: title || '',
|
||||
description: description || '',
|
||||
}}
|
||||
returnToOriginSwitchLabel={returnToOriginSwitchLabel}
|
||||
objectType={i18n.translate('xpack.lens.app.saveModalType', {
|
||||
|
@ -95,9 +92,9 @@ export const SaveModal = (props: Props) => {
|
|||
onClose={onClose}
|
||||
documentInfo={{
|
||||
// if the user cannot save to the library - treat this as a new document.
|
||||
id: savingToLibraryPermitted ? lastKnownDoc.savedObjectId : undefined,
|
||||
title: lastKnownDoc.title || '',
|
||||
description: lastKnownDoc.description || '',
|
||||
id: savingToLibraryPermitted ? savedObjectId : undefined,
|
||||
title: title || '',
|
||||
description: description || '',
|
||||
}}
|
||||
objectType={i18n.translate('xpack.lens.app.saveModalType', {
|
||||
defaultMessage: 'Lens visualization',
|
||||
|
|
|
@ -8,21 +8,16 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { ChromeStart, NotificationsStart } from 'kibana/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { partition, uniq } from 'lodash';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { partition } from 'lodash';
|
||||
import { SaveModal } from './save_modal';
|
||||
import { LensAppProps, LensAppServices } from './types';
|
||||
import type { SaveProps } from './app';
|
||||
import { Document, injectFilterReferences } from '../persistence';
|
||||
import { LensByReferenceInput, LensEmbeddableInput } from '../embeddable';
|
||||
import { LensAttributeService } from '../lens_attribute_service';
|
||||
import {
|
||||
DataPublicPluginStart,
|
||||
esFilters,
|
||||
IndexPattern,
|
||||
} from '../../../../../src/plugins/data/public';
|
||||
import { DataPublicPluginStart, esFilters } from '../../../../../src/plugins/data/public';
|
||||
import { APP_ID, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../common';
|
||||
import { getAllIndexPatterns } from '../utils';
|
||||
import { trackUiEvent } from '../lens_ui_telemetry';
|
||||
import { checkForDuplicateTitle } from '../../../../../src/plugins/saved_objects/public';
|
||||
import { LensAppState } from '../state_management';
|
||||
|
@ -31,7 +26,6 @@ type ExtraProps = Pick<LensAppProps, 'initialInput'> &
|
|||
Partial<Pick<LensAppProps, 'redirectToOrigin' | 'redirectTo' | 'onAppLeave'>>;
|
||||
|
||||
export type SaveModalContainerProps = {
|
||||
isVisible: boolean;
|
||||
originatingApp?: string;
|
||||
persistedDoc?: Document;
|
||||
lastKnownDoc?: Document;
|
||||
|
@ -49,7 +43,6 @@ export function SaveModalContainer({
|
|||
onClose,
|
||||
onSave,
|
||||
runSave,
|
||||
isVisible,
|
||||
persistedDoc,
|
||||
originatingApp,
|
||||
initialInput,
|
||||
|
@ -61,6 +54,14 @@ export function SaveModalContainer({
|
|||
lensServices,
|
||||
}: SaveModalContainerProps) {
|
||||
const [lastKnownDoc, setLastKnownDoc] = useState<Document | undefined>(initLastKnowDoc);
|
||||
let title = '';
|
||||
let description;
|
||||
let savedObjectId;
|
||||
if (lastKnownDoc) {
|
||||
title = lastKnownDoc.title;
|
||||
description = lastKnownDoc.description;
|
||||
savedObjectId = lastKnownDoc.savedObjectId;
|
||||
}
|
||||
|
||||
const {
|
||||
attributeService,
|
||||
|
@ -77,22 +78,26 @@ export function SaveModalContainer({
|
|||
}, [initLastKnowDoc]);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadLastKnownDoc() {
|
||||
if (initialInput && isVisible) {
|
||||
getLastKnownDoc({
|
||||
let isMounted = true;
|
||||
async function loadPersistedDoc() {
|
||||
if (initialInput) {
|
||||
getPersistedDoc({
|
||||
data,
|
||||
initialInput,
|
||||
chrome,
|
||||
notifications,
|
||||
attributeService,
|
||||
}).then((result) => {
|
||||
if (result) setLastKnownDoc(result.doc);
|
||||
}).then((doc) => {
|
||||
if (doc && isMounted) setLastKnownDoc(doc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
loadLastKnownDoc();
|
||||
}, [chrome, data, initialInput, notifications, attributeService, isVisible]);
|
||||
loadPersistedDoc();
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [chrome, data, initialInput, notifications, attributeService]);
|
||||
|
||||
const tagsIds =
|
||||
persistedDoc && savedObjectsTagging
|
||||
|
@ -131,7 +136,6 @@ export function SaveModalContainer({
|
|||
|
||||
return (
|
||||
<SaveModal
|
||||
isVisible={isVisible}
|
||||
originatingApp={originatingApp}
|
||||
savingToLibraryPermitted={savingToLibraryPermitted}
|
||||
allowByValueEmbeddables={dashboardFeatureFlag?.allowByValueEmbeddables}
|
||||
|
@ -142,7 +146,9 @@ export function SaveModalContainer({
|
|||
}}
|
||||
onClose={onClose}
|
||||
getAppNameFromId={getAppNameFromId}
|
||||
lastKnownDoc={lastKnownDoc}
|
||||
title={title}
|
||||
description={description}
|
||||
savedObjectId={savedObjectId}
|
||||
returnToOriginSwitchLabel={returnToOriginSwitchLabel}
|
||||
/>
|
||||
);
|
||||
|
@ -330,7 +336,10 @@ export const runSaveLensVisualization = async (
|
|||
...newInput,
|
||||
};
|
||||
|
||||
return { persistedDoc: newDoc, lastKnownDoc: newDoc, isLinkedToOriginatingApp: false };
|
||||
return {
|
||||
persistedDoc: newDoc,
|
||||
isLinkedToOriginatingApp: false,
|
||||
};
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.dir(e);
|
||||
|
@ -356,7 +365,7 @@ export function getLastKnownDocWithoutPinnedFilters(doc?: Document) {
|
|||
: doc;
|
||||
}
|
||||
|
||||
export const getLastKnownDoc = async ({
|
||||
export const getPersistedDoc = async ({
|
||||
initialInput,
|
||||
attributeService,
|
||||
data,
|
||||
|
@ -368,7 +377,7 @@ export const getLastKnownDoc = async ({
|
|||
data: DataPublicPluginStart;
|
||||
notifications: NotificationsStart;
|
||||
chrome: ChromeStart;
|
||||
}): Promise<{ doc: Document; indexPatterns: IndexPattern[] } | undefined> => {
|
||||
}): Promise<Document | undefined> => {
|
||||
let doc: Document;
|
||||
|
||||
try {
|
||||
|
@ -387,19 +396,12 @@ export const getLastKnownDoc = async ({
|
|||
initialInput.savedObjectId
|
||||
);
|
||||
}
|
||||
const indexPatternIds = uniq(
|
||||
doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id)
|
||||
);
|
||||
const { indexPatterns } = await getAllIndexPatterns(indexPatternIds, data.indexPatterns);
|
||||
|
||||
// Don't overwrite any pinned filters
|
||||
data.query.filterManager.setAppFilters(
|
||||
injectFilterReferences(doc.state.filters, doc.references)
|
||||
);
|
||||
return {
|
||||
doc,
|
||||
indexPatterns,
|
||||
};
|
||||
return doc;
|
||||
} catch (e) {
|
||||
notifications.toasts.addDanger(
|
||||
i18n.translate('xpack.lens.app.docLoadingError', {
|
||||
|
|
|
@ -34,7 +34,7 @@ import {
|
|||
EmbeddableEditorState,
|
||||
EmbeddableStateTransfer,
|
||||
} from '../../../../../src/plugins/embeddable/public';
|
||||
import { EditorFrameInstance } from '../types';
|
||||
import { Datasource, EditorFrameInstance, Visualization } from '../types';
|
||||
import { PresentationUtilPluginStart } from '../../../../../src/plugins/presentation_util/public';
|
||||
export interface RedirectToOriginProps {
|
||||
input?: LensEmbeddableInput;
|
||||
|
@ -54,7 +54,8 @@ export interface LensAppProps {
|
|||
|
||||
// State passed in by the container which is used to determine the id of the Originating App.
|
||||
incomingState?: EmbeddableEditorState;
|
||||
initialContext?: VisualizeFieldContext;
|
||||
datasourceMap: Record<string, Datasource>;
|
||||
visualizationMap: Record<string, Visualization>;
|
||||
}
|
||||
|
||||
export type RunSave = (
|
||||
|
@ -81,6 +82,8 @@ export interface LensTopNavMenuProps {
|
|||
indicateNoData: boolean;
|
||||
setIsSaveModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
runSave: RunSave;
|
||||
datasourceMap: Record<string, Datasource>;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface HistoryLocationState {
|
||||
|
|
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
import { EuiButtonGroup, EuiComboBox, EuiFieldText } from '@elastic/eui';
|
||||
import { FramePublicAPI, Operation, VisualizationDimensionEditorProps } from '../../types';
|
||||
import { DatatableVisualizationState } from '../visualization';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../../editor_frame_service/mocks';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../../mocks';
|
||||
import { mountWithIntl } from '@kbn/test/jest';
|
||||
import { TableDimensionEditor } from './dimension_editor';
|
||||
import { chartPluginMock } from 'src/plugins/charts/public/mocks';
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { Ast } from '@kbn/interpreter/common';
|
||||
import { buildExpression } from '../../../../../src/plugins/expressions/public';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../mocks';
|
||||
import { DatatableVisualizationState, getDatatableVisualization } from './visualization';
|
||||
import {
|
||||
Operation,
|
||||
|
@ -21,8 +21,6 @@ import { chartPluginMock } from 'src/plugins/charts/public/mocks';
|
|||
function mockFrame(): FramePublicAPI {
|
||||
return {
|
||||
...createMockFramePublicAPI(),
|
||||
addNewLayer: () => 'aaa',
|
||||
removeLayers: () => {},
|
||||
datasourceLayers: {},
|
||||
query: { query: '', language: 'lucene' },
|
||||
dateRange: {
|
||||
|
@ -40,7 +38,7 @@ const datatableVisualization = getDatatableVisualization({
|
|||
describe('Datatable Visualization', () => {
|
||||
describe('#initialize', () => {
|
||||
it('should initialize from the empty state', () => {
|
||||
expect(datatableVisualization.initialize(mockFrame(), undefined)).toEqual({
|
||||
expect(datatableVisualization.initialize(() => 'aaa', undefined)).toEqual({
|
||||
layerId: 'aaa',
|
||||
columns: [],
|
||||
});
|
||||
|
@ -51,7 +49,7 @@ describe('Datatable Visualization', () => {
|
|||
layerId: 'foo',
|
||||
columns: [{ columnId: 'saved' }],
|
||||
};
|
||||
expect(datatableVisualization.initialize(mockFrame(), expectedState)).toEqual(expectedState);
|
||||
expect(datatableVisualization.initialize(() => 'foo', expectedState)).toEqual(expectedState);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -101,11 +101,11 @@ export const getDatatableVisualization = ({
|
|||
|
||||
switchVisualizationType: (_, state) => state,
|
||||
|
||||
initialize(frame, state) {
|
||||
initialize(addNewLayer, state) {
|
||||
return (
|
||||
state || {
|
||||
columns: [],
|
||||
layerId: frame.addNewLayer(),
|
||||
layerId: addNewLayer(),
|
||||
}
|
||||
);
|
||||
},
|
||||
|
|
|
@ -12,13 +12,13 @@ import {
|
|||
createMockFramePublicAPI,
|
||||
createMockDatasource,
|
||||
DatasourceMock,
|
||||
} from '../../mocks';
|
||||
} from '../../../mocks';
|
||||
import { Visualization } from '../../../types';
|
||||
import { mountWithIntl } from '@kbn/test/jest';
|
||||
import { LayerPanels } from './config_panel';
|
||||
import { LayerPanel } from './layer_panel';
|
||||
import { coreMock } from 'src/core/public/mocks';
|
||||
import { generateId } from '../../../id_generator';
|
||||
import { mountWithProvider } from '../../../mocks';
|
||||
|
||||
jest.mock('../../../id_generator');
|
||||
|
||||
|
@ -54,17 +54,17 @@ describe('ConfigPanel', () => {
|
|||
vis1: mockVisualization,
|
||||
vis2: mockVisualization2,
|
||||
},
|
||||
activeDatasourceId: 'ds1',
|
||||
activeDatasourceId: 'mockindexpattern',
|
||||
datasourceMap: {
|
||||
ds1: mockDatasource,
|
||||
mockindexpattern: mockDatasource,
|
||||
},
|
||||
activeVisualization: ({
|
||||
...mockVisualization,
|
||||
getLayerIds: () => Object.keys(frame.datasourceLayers),
|
||||
appendLayer: true,
|
||||
appendLayer: jest.fn(),
|
||||
} as unknown) as Visualization,
|
||||
datasourceStates: {
|
||||
ds1: {
|
||||
mockindexpattern: {
|
||||
isLoading: false,
|
||||
state: 'state',
|
||||
},
|
||||
|
@ -110,113 +110,184 @@ describe('ConfigPanel', () => {
|
|||
};
|
||||
|
||||
mockVisualization.getLayerIds.mockReturnValue(Object.keys(frame.datasourceLayers));
|
||||
mockDatasource = createMockDatasource('ds1');
|
||||
mockDatasource = createMockDatasource('mockindexpattern');
|
||||
});
|
||||
|
||||
// in what case is this test needed?
|
||||
it('should fail to render layerPanels if the public API is out of date', () => {
|
||||
it('should fail to render layerPanels if the public API is out of date', async () => {
|
||||
const props = getDefaultProps();
|
||||
props.framePublicAPI.datasourceLayers = {};
|
||||
const component = mountWithIntl(<LayerPanels {...props} />);
|
||||
expect(component.find(LayerPanel).exists()).toBe(false);
|
||||
const { instance } = await mountWithProvider(<LayerPanels {...props} />);
|
||||
expect(instance.find(LayerPanel).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('allow datasources and visualizations to use setters', async () => {
|
||||
const props = getDefaultProps();
|
||||
const component = mountWithIntl(<LayerPanels {...props} />);
|
||||
const { updateDatasource, updateAll } = component.find(LayerPanel).props();
|
||||
const { instance, lensStore } = await mountWithProvider(<LayerPanels {...props} />, {
|
||||
preloadedState: {
|
||||
datasourceStates: {
|
||||
mockindexpattern: {
|
||||
isLoading: false,
|
||||
state: 'state',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const { updateDatasource, updateAll } = instance.find(LayerPanel).props();
|
||||
|
||||
const updater = () => 'updated';
|
||||
updateDatasource('ds1', updater);
|
||||
// wait for one tick so async updater has a chance to trigger
|
||||
updateDatasource('mockindexpattern', updater);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect(props.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(props.dispatch.mock.calls[0][0].updater(props.datasourceStates.ds1.state)).toEqual(
|
||||
'updated'
|
||||
);
|
||||
expect(lensStore.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
(lensStore.dispatch as jest.Mock).mock.calls[0][0].payload.updater(
|
||||
props.datasourceStates.mockindexpattern.state
|
||||
)
|
||||
).toEqual('updated');
|
||||
|
||||
updateAll('ds1', updater, props.visualizationState);
|
||||
updateAll('mockindexpattern', updater, props.visualizationState);
|
||||
// wait for one tick so async updater has a chance to trigger
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect(props.dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(props.dispatch.mock.calls[0][0].updater(props.datasourceStates.ds1.state)).toEqual(
|
||||
'updated'
|
||||
);
|
||||
expect(lensStore.dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
(lensStore.dispatch as jest.Mock).mock.calls[0][0].payload.updater(
|
||||
props.datasourceStates.mockindexpattern.state
|
||||
)
|
||||
).toEqual('updated');
|
||||
});
|
||||
|
||||
describe('focus behavior when adding or removing layers', () => {
|
||||
it('should focus the only layer when resetting the layer', () => {
|
||||
const component = mountWithIntl(<LayerPanels {...getDefaultProps()} />, {
|
||||
attachTo: container,
|
||||
});
|
||||
const firstLayerFocusable = component
|
||||
it('should focus the only layer when resetting the layer', async () => {
|
||||
const { instance } = await mountWithProvider(
|
||||
<LayerPanels {...getDefaultProps()} />,
|
||||
{
|
||||
preloadedState: {
|
||||
datasourceStates: {
|
||||
mockindexpattern: {
|
||||
isLoading: false,
|
||||
state: 'state',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
attachTo: container,
|
||||
}
|
||||
);
|
||||
const firstLayerFocusable = instance
|
||||
.find(LayerPanel)
|
||||
.first()
|
||||
.find('section')
|
||||
.first()
|
||||
.instance();
|
||||
act(() => {
|
||||
component.find('[data-test-subj="lnsLayerRemove"]').first().simulate('click');
|
||||
instance.find('[data-test-subj="lnsLayerRemove"]').first().simulate('click');
|
||||
});
|
||||
const focusedEl = document.activeElement;
|
||||
expect(focusedEl).toEqual(firstLayerFocusable);
|
||||
});
|
||||
|
||||
it('should focus the second layer when removing the first layer', () => {
|
||||
it('should focus the second layer when removing the first layer', async () => {
|
||||
const defaultProps = getDefaultProps();
|
||||
// overwriting datasourceLayers to test two layers
|
||||
frame.datasourceLayers = {
|
||||
first: mockDatasource.publicAPIMock,
|
||||
second: mockDatasource.publicAPIMock,
|
||||
};
|
||||
const component = mountWithIntl(<LayerPanels {...defaultProps} />, { attachTo: container });
|
||||
const secondLayerFocusable = component
|
||||
const { instance } = await mountWithProvider(
|
||||
<LayerPanels {...defaultProps} />,
|
||||
{
|
||||
preloadedState: {
|
||||
datasourceStates: {
|
||||
mockindexpattern: {
|
||||
isLoading: false,
|
||||
state: 'state',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
attachTo: container,
|
||||
}
|
||||
);
|
||||
|
||||
const secondLayerFocusable = instance
|
||||
.find(LayerPanel)
|
||||
.at(1)
|
||||
.find('section')
|
||||
.first()
|
||||
.instance();
|
||||
act(() => {
|
||||
component.find('[data-test-subj="lnsLayerRemove"]').at(0).simulate('click');
|
||||
instance.find('[data-test-subj="lnsLayerRemove"]').at(0).simulate('click');
|
||||
});
|
||||
const focusedEl = document.activeElement;
|
||||
expect(focusedEl).toEqual(secondLayerFocusable);
|
||||
});
|
||||
|
||||
it('should focus the first layer when removing the second layer', () => {
|
||||
it('should focus the first layer when removing the second layer', async () => {
|
||||
const defaultProps = getDefaultProps();
|
||||
// overwriting datasourceLayers to test two layers
|
||||
frame.datasourceLayers = {
|
||||
first: mockDatasource.publicAPIMock,
|
||||
second: mockDatasource.publicAPIMock,
|
||||
};
|
||||
const component = mountWithIntl(<LayerPanels {...defaultProps} />, { attachTo: container });
|
||||
const firstLayerFocusable = component
|
||||
const { instance } = await mountWithProvider(
|
||||
<LayerPanels {...defaultProps} />,
|
||||
{
|
||||
preloadedState: {
|
||||
datasourceStates: {
|
||||
mockindexpattern: {
|
||||
isLoading: false,
|
||||
state: 'state',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
attachTo: container,
|
||||
}
|
||||
);
|
||||
const firstLayerFocusable = instance
|
||||
.find(LayerPanel)
|
||||
.first()
|
||||
.find('section')
|
||||
.first()
|
||||
.instance();
|
||||
act(() => {
|
||||
component.find('[data-test-subj="lnsLayerRemove"]').at(2).simulate('click');
|
||||
instance.find('[data-test-subj="lnsLayerRemove"]').at(2).simulate('click');
|
||||
});
|
||||
const focusedEl = document.activeElement;
|
||||
expect(focusedEl).toEqual(firstLayerFocusable);
|
||||
});
|
||||
|
||||
it('should focus the added layer', () => {
|
||||
it('should focus the added layer', async () => {
|
||||
(generateId as jest.Mock).mockReturnValue(`second`);
|
||||
const dispatch = jest.fn((x) => {
|
||||
if (x.subType === 'ADD_LAYER') {
|
||||
frame.datasourceLayers.second = mockDatasource.publicAPIMock;
|
||||
}
|
||||
});
|
||||
|
||||
const component = mountWithIntl(<LayerPanels {...getDefaultProps()} dispatch={dispatch} />, {
|
||||
attachTo: container,
|
||||
});
|
||||
const { instance } = await mountWithProvider(
|
||||
<LayerPanels {...getDefaultProps()} />,
|
||||
|
||||
{
|
||||
preloadedState: {
|
||||
datasourceStates: {
|
||||
mockindexpattern: {
|
||||
isLoading: false,
|
||||
state: 'state',
|
||||
},
|
||||
},
|
||||
activeDatasourceId: 'mockindexpattern',
|
||||
},
|
||||
dispatch: jest.fn((x) => {
|
||||
if (x.payload.subType === 'ADD_LAYER') {
|
||||
frame.datasourceLayers.second = mockDatasource.publicAPIMock;
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
attachTo: container,
|
||||
}
|
||||
);
|
||||
act(() => {
|
||||
component.find('[data-test-subj="lnsLayerAddButton"]').first().simulate('click');
|
||||
instance.find('[data-test-subj="lnsLayerAddButton"]').first().simulate('click');
|
||||
});
|
||||
const focusedEl = document.activeElement;
|
||||
expect(focusedEl?.children[0].getAttribute('data-test-subj')).toEqual('lns-layerPanel-1');
|
||||
|
|
|
@ -10,13 +10,21 @@ import './config_panel.scss';
|
|||
import React, { useMemo, memo } from 'react';
|
||||
import { EuiFlexItem, EuiToolTip, EuiButton, EuiForm } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { mapValues } from 'lodash';
|
||||
import { Visualization } from '../../../types';
|
||||
import { LayerPanel } from './layer_panel';
|
||||
import { trackUiEvent } from '../../../lens_ui_telemetry';
|
||||
import { generateId } from '../../../id_generator';
|
||||
import { removeLayer, appendLayer } from './layer_actions';
|
||||
import { appendLayer } from './layer_actions';
|
||||
import { ConfigPanelWrapperProps } from './types';
|
||||
import { useFocusUpdate } from './use_focus_update';
|
||||
import {
|
||||
useLensDispatch,
|
||||
updateState,
|
||||
updateDatasourceState,
|
||||
updateVisualizationState,
|
||||
setToggleFullscreen,
|
||||
} from '../../../state_management';
|
||||
|
||||
export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) {
|
||||
const activeVisualization = props.visualizationMap[props.activeVisualizationId || ''];
|
||||
|
@ -33,13 +41,8 @@ export function LayerPanels(
|
|||
activeVisualization: Visualization;
|
||||
}
|
||||
) {
|
||||
const {
|
||||
activeVisualization,
|
||||
visualizationState,
|
||||
dispatch,
|
||||
activeDatasourceId,
|
||||
datasourceMap,
|
||||
} = props;
|
||||
const { activeVisualization, visualizationState, activeDatasourceId, datasourceMap } = props;
|
||||
const dispatchLens = useLensDispatch();
|
||||
|
||||
const layerIds = activeVisualization.getLayerIds(visualizationState);
|
||||
const {
|
||||
|
@ -50,26 +53,28 @@ export function LayerPanels(
|
|||
|
||||
const setVisualizationState = useMemo(
|
||||
() => (newState: unknown) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_VISUALIZATION_STATE',
|
||||
visualizationId: activeVisualization.id,
|
||||
updater: newState,
|
||||
clearStagedPreview: false,
|
||||
});
|
||||
dispatchLens(
|
||||
updateVisualizationState({
|
||||
visualizationId: activeVisualization.id,
|
||||
updater: newState,
|
||||
clearStagedPreview: false,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, activeVisualization]
|
||||
[activeVisualization, dispatchLens]
|
||||
);
|
||||
const updateDatasource = useMemo(
|
||||
() => (datasourceId: string, newState: unknown) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_DATASOURCE_STATE',
|
||||
updater: (prevState: unknown) =>
|
||||
typeof newState === 'function' ? newState(prevState) : newState,
|
||||
datasourceId,
|
||||
clearStagedPreview: false,
|
||||
});
|
||||
dispatchLens(
|
||||
updateDatasourceState({
|
||||
updater: (prevState: unknown) =>
|
||||
typeof newState === 'function' ? newState(prevState) : newState,
|
||||
datasourceId,
|
||||
clearStagedPreview: false,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
[dispatchLens]
|
||||
);
|
||||
const updateDatasourceAsync = useMemo(
|
||||
() => (datasourceId: string, newState: unknown) => {
|
||||
|
@ -86,42 +91,42 @@ export function LayerPanels(
|
|||
// React will synchronously update if this is triggered from a third party component,
|
||||
// which we don't want. The timeout lets user interaction have priority, then React updates.
|
||||
setTimeout(() => {
|
||||
dispatch({
|
||||
type: 'UPDATE_STATE',
|
||||
subType: 'UPDATE_ALL_STATES',
|
||||
updater: (prevState) => {
|
||||
const updatedDatasourceState =
|
||||
typeof newDatasourceState === 'function'
|
||||
? newDatasourceState(prevState.datasourceStates[datasourceId].state)
|
||||
: newDatasourceState;
|
||||
return {
|
||||
...prevState,
|
||||
datasourceStates: {
|
||||
...prevState.datasourceStates,
|
||||
[datasourceId]: {
|
||||
state: updatedDatasourceState,
|
||||
isLoading: false,
|
||||
dispatchLens(
|
||||
updateState({
|
||||
subType: 'UPDATE_ALL_STATES',
|
||||
updater: (prevState) => {
|
||||
const updatedDatasourceState =
|
||||
typeof newDatasourceState === 'function'
|
||||
? newDatasourceState(prevState.datasourceStates[datasourceId].state)
|
||||
: newDatasourceState;
|
||||
return {
|
||||
...prevState,
|
||||
datasourceStates: {
|
||||
...prevState.datasourceStates,
|
||||
[datasourceId]: {
|
||||
state: updatedDatasourceState,
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
visualization: {
|
||||
...prevState.visualization,
|
||||
state: newVisualizationState,
|
||||
},
|
||||
stagedPreview: undefined,
|
||||
};
|
||||
},
|
||||
});
|
||||
visualization: {
|
||||
...prevState.visualization,
|
||||
state: newVisualizationState,
|
||||
},
|
||||
stagedPreview: undefined,
|
||||
};
|
||||
},
|
||||
})
|
||||
);
|
||||
}, 0);
|
||||
},
|
||||
[dispatch]
|
||||
[dispatchLens]
|
||||
);
|
||||
|
||||
const toggleFullscreen = useMemo(
|
||||
() => () => {
|
||||
dispatch({
|
||||
type: 'TOGGLE_FULLSCREEN',
|
||||
});
|
||||
dispatchLens(setToggleFullscreen());
|
||||
},
|
||||
[dispatch]
|
||||
[dispatchLens]
|
||||
);
|
||||
|
||||
const datasourcePublicAPIs = props.framePublicAPI.datasourceLayers;
|
||||
|
@ -144,18 +149,41 @@ export function LayerPanels(
|
|||
updateAll={updateAll}
|
||||
isOnlyLayer={layerIds.length === 1}
|
||||
onRemoveLayer={() => {
|
||||
dispatch({
|
||||
type: 'UPDATE_STATE',
|
||||
subType: 'REMOVE_OR_CLEAR_LAYER',
|
||||
updater: (state) =>
|
||||
removeLayer({
|
||||
activeVisualization,
|
||||
layerId,
|
||||
trackUiEvent,
|
||||
datasourceMap,
|
||||
state,
|
||||
}),
|
||||
});
|
||||
dispatchLens(
|
||||
updateState({
|
||||
subType: 'REMOVE_OR_CLEAR_LAYER',
|
||||
updater: (state) => {
|
||||
const isOnlyLayer = activeVisualization
|
||||
.getLayerIds(state.visualization.state)
|
||||
.every((id) => id === layerId);
|
||||
|
||||
return {
|
||||
...state,
|
||||
datasourceStates: mapValues(
|
||||
state.datasourceStates,
|
||||
(datasourceState, datasourceId) => {
|
||||
const datasource = datasourceMap[datasourceId!];
|
||||
return {
|
||||
...datasourceState,
|
||||
state: isOnlyLayer
|
||||
? datasource.clearLayer(datasourceState.state, layerId)
|
||||
: datasource.removeLayer(datasourceState.state, layerId),
|
||||
};
|
||||
}
|
||||
),
|
||||
visualization: {
|
||||
...state.visualization,
|
||||
state:
|
||||
isOnlyLayer || !activeVisualization.removeLayer
|
||||
? activeVisualization.clearLayer(state.visualization.state, layerId)
|
||||
: activeVisualization.removeLayer(state.visualization.state, layerId),
|
||||
},
|
||||
stagedPreview: undefined,
|
||||
};
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
removeLayerRef(layerId);
|
||||
}}
|
||||
toggleFullscreen={toggleFullscreen}
|
||||
|
@ -187,18 +215,19 @@ export function LayerPanels(
|
|||
color="text"
|
||||
onClick={() => {
|
||||
const id = generateId();
|
||||
dispatch({
|
||||
type: 'UPDATE_STATE',
|
||||
subType: 'ADD_LAYER',
|
||||
updater: (state) =>
|
||||
appendLayer({
|
||||
activeVisualization,
|
||||
generateId: () => id,
|
||||
trackUiEvent,
|
||||
activeDatasource: datasourceMap[activeDatasourceId],
|
||||
state,
|
||||
}),
|
||||
});
|
||||
dispatchLens(
|
||||
updateState({
|
||||
subType: 'ADD_LAYER',
|
||||
updater: (state) =>
|
||||
appendLayer({
|
||||
activeVisualization,
|
||||
generateId: () => id,
|
||||
trackUiEvent,
|
||||
activeDatasource: datasourceMap[activeDatasourceId],
|
||||
state,
|
||||
}),
|
||||
})
|
||||
);
|
||||
setNextFocusedLayerId(id);
|
||||
}}
|
||||
iconType="plusInCircleFilled"
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { initialState } from '../../../state_management/lens_slice';
|
||||
import { removeLayer, appendLayer } from './layer_actions';
|
||||
|
||||
function createTestArgs(initialLayerIds: string[]) {
|
||||
|
@ -42,6 +43,7 @@ function createTestArgs(initialLayerIds: string[]) {
|
|||
|
||||
return {
|
||||
state: {
|
||||
...initialState,
|
||||
activeDatasourceId: 'ds1',
|
||||
datasourceStates,
|
||||
title: 'foo',
|
||||
|
|
|
@ -6,12 +6,13 @@
|
|||
*/
|
||||
|
||||
import { mapValues } from 'lodash';
|
||||
import { EditorFrameState } from '../state_management';
|
||||
import { LensAppState } from '../../../state_management';
|
||||
|
||||
import { Datasource, Visualization } from '../../../types';
|
||||
|
||||
interface RemoveLayerOptions {
|
||||
trackUiEvent: (name: string) => void;
|
||||
state: EditorFrameState;
|
||||
state: LensAppState;
|
||||
layerId: string;
|
||||
activeVisualization: Pick<Visualization, 'getLayerIds' | 'clearLayer' | 'removeLayer'>;
|
||||
datasourceMap: Record<string, Pick<Datasource, 'clearLayer' | 'removeLayer'>>;
|
||||
|
@ -19,13 +20,13 @@ interface RemoveLayerOptions {
|
|||
|
||||
interface AppendLayerOptions {
|
||||
trackUiEvent: (name: string) => void;
|
||||
state: EditorFrameState;
|
||||
state: LensAppState;
|
||||
generateId: () => string;
|
||||
activeDatasource: Pick<Datasource, 'insertLayer' | 'id'>;
|
||||
activeVisualization: Pick<Visualization, 'appendLayer'>;
|
||||
}
|
||||
|
||||
export function removeLayer(opts: RemoveLayerOptions): EditorFrameState {
|
||||
export function removeLayer(opts: RemoveLayerOptions): LensAppState {
|
||||
const { state, trackUiEvent: trackUiEvent, activeVisualization, layerId, datasourceMap } = opts;
|
||||
const isOnlyLayer = activeVisualization
|
||||
.getLayerIds(state.visualization.state)
|
||||
|
@ -61,7 +62,7 @@ export function appendLayer({
|
|||
state,
|
||||
generateId,
|
||||
activeDatasource,
|
||||
}: AppendLayerOptions): EditorFrameState {
|
||||
}: AppendLayerOptions): LensAppState {
|
||||
trackUiEvent('layer_added');
|
||||
|
||||
if (!activeVisualization.appendLayer) {
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
createMockFramePublicAPI,
|
||||
createMockDatasource,
|
||||
DatasourceMock,
|
||||
} from '../../mocks';
|
||||
} from '../../../mocks';
|
||||
|
||||
jest.mock('../../../id_generator');
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Action } from '../state_management';
|
||||
import {
|
||||
Visualization,
|
||||
FramePublicAPI,
|
||||
|
@ -18,7 +17,6 @@ export interface ConfigPanelWrapperProps {
|
|||
visualizationState: unknown;
|
||||
visualizationMap: Record<string, Visualization>;
|
||||
activeVisualizationId: string | null;
|
||||
dispatch: (action: Action) => void;
|
||||
framePublicAPI: FramePublicAPI;
|
||||
datasourceMap: Record<string, Datasource>;
|
||||
datasourceStates: Record<
|
||||
|
@ -37,7 +35,6 @@ export interface LayerPanelProps {
|
|||
visualizationState: unknown;
|
||||
datasourceMap: Record<string, Datasource>;
|
||||
activeVisualization: Visualization;
|
||||
dispatch: (action: Action) => void;
|
||||
framePublicAPI: FramePublicAPI;
|
||||
datasourceStates: Record<
|
||||
string,
|
||||
|
|
|
@ -7,54 +7,94 @@
|
|||
|
||||
import './data_panel_wrapper.scss';
|
||||
|
||||
import React, { useMemo, memo, useContext, useState } from 'react';
|
||||
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 { Action } from './state_management';
|
||||
import { DragContext, DragDropIdentifier } from '../../drag_drop';
|
||||
import { StateSetter, FramePublicAPI, DatasourceDataPanelProps, Datasource } from '../../types';
|
||||
import { Query, Filter } from '../../../../../../src/plugins/data/public';
|
||||
import { StateSetter, DatasourceDataPanelProps, Datasource } from '../../types';
|
||||
import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public';
|
||||
import {
|
||||
switchDatasource,
|
||||
useLensDispatch,
|
||||
updateDatasourceState,
|
||||
LensState,
|
||||
useLensSelector,
|
||||
setState,
|
||||
} from '../../state_management';
|
||||
import { initializeDatasources } from './state_helpers';
|
||||
|
||||
interface DataPanelWrapperProps {
|
||||
datasourceState: unknown;
|
||||
datasourceMap: Record<string, Datasource>;
|
||||
activeDatasource: string | null;
|
||||
datasourceIsLoading: boolean;
|
||||
dispatch: (action: Action) => void;
|
||||
showNoDataPopover: () => void;
|
||||
core: DatasourceDataPanelProps['core'];
|
||||
query: Query;
|
||||
dateRange: FramePublicAPI['dateRange'];
|
||||
filters: Filter[];
|
||||
dropOntoWorkspace: (field: DragDropIdentifier) => void;
|
||||
hasSuggestionForField: (field: DragDropIdentifier) => boolean;
|
||||
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 { dispatch, activeDatasource } = props;
|
||||
const setDatasourceState: StateSetter<unknown> = useMemo(
|
||||
() => (updater) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_DATASOURCE_STATE',
|
||||
updater,
|
||||
datasourceId: activeDatasource!,
|
||||
clearStagedPreview: true,
|
||||
});
|
||||
},
|
||||
[dispatch, activeDatasource]
|
||||
const { activeDatasource } = props;
|
||||
|
||||
const { filters, query, dateRange, datasourceStates, activeDatasourceId } = useLensSelector(
|
||||
getExternals
|
||||
);
|
||||
const dispatchLens = useLensDispatch();
|
||||
const setDatasourceState: StateSetter<unknown> = useMemo(() => {
|
||||
return (updater) => {
|
||||
dispatchLens(
|
||||
updateDatasourceState({
|
||||
updater,
|
||||
datasourceId: activeDatasource!,
|
||||
clearStagedPreview: true,
|
||||
})
|
||||
);
|
||||
};
|
||||
}, [activeDatasource, dispatchLens]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeDatasourceId && datasourceStates[activeDatasourceId].state === null) {
|
||||
initializeDatasources(props.datasourceMap, datasourceStates, undefined, undefined, {
|
||||
isFullEditor: true,
|
||||
}).then((result) => {
|
||||
const newDatasourceStates = Object.entries(result).reduce(
|
||||
(state, [datasourceId, datasourceState]) => ({
|
||||
...state,
|
||||
[datasourceId]: {
|
||||
...datasourceState,
|
||||
isLoading: false,
|
||||
},
|
||||
}),
|
||||
{}
|
||||
);
|
||||
dispatchLens(setState({ datasourceStates: newDatasourceStates }));
|
||||
});
|
||||
}
|
||||
}, [datasourceStates, activeDatasourceId, props.datasourceMap, dispatchLens]);
|
||||
|
||||
const datasourceProps: DatasourceDataPanelProps = {
|
||||
dragDropContext: useContext(DragContext),
|
||||
state: props.datasourceState,
|
||||
setState: setDatasourceState,
|
||||
core: props.core,
|
||||
query: props.query,
|
||||
dateRange: props.dateRange,
|
||||
filters: props.filters,
|
||||
filters,
|
||||
query,
|
||||
dateRange,
|
||||
showNoDataPopover: props.showNoDataPopover,
|
||||
dropOntoWorkspace: props.dropOntoWorkspace,
|
||||
hasSuggestionForField: props.hasSuggestionForField,
|
||||
|
@ -98,10 +138,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
|
|||
icon={props.activeDatasource === datasourceId ? 'check' : 'empty'}
|
||||
onClick={() => {
|
||||
setDatasourceSwitcher(false);
|
||||
props.dispatch({
|
||||
type: 'SWITCH_DATASOURCE',
|
||||
newDatasourceId: datasourceId,
|
||||
});
|
||||
dispatchLens(switchDatasource({ newDatasourceId: datasourceId }));
|
||||
}}
|
||||
>
|
||||
{datasourceId}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -5,118 +5,53 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useReducer, useState, useCallback, useRef, useMemo } from 'react';
|
||||
import React, { useCallback, useRef, useMemo } from 'react';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import { isEqual } from 'lodash';
|
||||
import { PaletteRegistry } from 'src/plugins/charts/public';
|
||||
import { IndexPattern } from '../../../../../../src/plugins/data/public';
|
||||
import { getAllIndexPatterns } from '../../utils';
|
||||
import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public';
|
||||
import { Datasource, FramePublicAPI, Visualization } from '../../types';
|
||||
import { reducer, getInitialState } from './state_management';
|
||||
import { DataPanelWrapper } from './data_panel_wrapper';
|
||||
import { ConfigPanelWrapper } from './config_panel';
|
||||
import { FrameLayout } from './frame_layout';
|
||||
import { SuggestionPanel } from './suggestion_panel';
|
||||
import { WorkspacePanel } from './workspace_panel';
|
||||
import { Document } from '../../persistence/saved_object_store';
|
||||
import { DragDropIdentifier, RootDragDropProvider } from '../../drag_drop';
|
||||
import { getSavedObjectFormat } from './save';
|
||||
import { generateId } from '../../id_generator';
|
||||
import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public';
|
||||
import { EditorFrameStartPlugins } from '../service';
|
||||
import { initializeDatasources, createDatasourceLayers } from './state_helpers';
|
||||
import {
|
||||
applyVisualizeFieldSuggestions,
|
||||
getTopSuggestionForField,
|
||||
switchToSuggestion,
|
||||
Suggestion,
|
||||
} from './suggestion_helpers';
|
||||
import { createDatasourceLayers } from './state_helpers';
|
||||
import { getTopSuggestionForField, switchToSuggestion, Suggestion } from './suggestion_helpers';
|
||||
import { trackUiEvent } from '../../lens_ui_telemetry';
|
||||
import {
|
||||
useLensSelector,
|
||||
useLensDispatch,
|
||||
LensAppState,
|
||||
DispatchSetState,
|
||||
onChangeFromEditorFrame,
|
||||
} from '../../state_management';
|
||||
import { useLensSelector, useLensDispatch } from '../../state_management';
|
||||
|
||||
export interface EditorFrameProps {
|
||||
datasourceMap: Record<string, Datasource>;
|
||||
visualizationMap: Record<string, Visualization>;
|
||||
ExpressionRenderer: ReactExpressionRendererType;
|
||||
palettes: PaletteRegistry;
|
||||
onError: (e: { message: string }) => void;
|
||||
core: CoreStart;
|
||||
plugins: EditorFrameStartPlugins;
|
||||
showNoDataPopover: () => void;
|
||||
initialContext?: VisualizeFieldContext;
|
||||
}
|
||||
|
||||
export function EditorFrame(props: EditorFrameProps) {
|
||||
const {
|
||||
activeData,
|
||||
resolvedDateRange: dateRange,
|
||||
query,
|
||||
filters,
|
||||
searchSessionId,
|
||||
savedQuery,
|
||||
query,
|
||||
persistedDoc,
|
||||
indexPatternsForTopNav,
|
||||
lastKnownDoc,
|
||||
activeData,
|
||||
isSaveable,
|
||||
resolvedDateRange: dateRange,
|
||||
} = useLensSelector((state) => state.app);
|
||||
const [state, dispatch] = useReducer(reducer, { ...props, doc: persistedDoc }, getInitialState);
|
||||
activeDatasourceId,
|
||||
visualization,
|
||||
datasourceStates,
|
||||
stagedPreview,
|
||||
isFullscreenDatasource,
|
||||
} = useLensSelector((state) => state.lens);
|
||||
|
||||
const dispatchLens = useLensDispatch();
|
||||
const dispatchChange: DispatchSetState = useCallback(
|
||||
(s: Partial<LensAppState>) => dispatchLens(onChangeFromEditorFrame(s)),
|
||||
[dispatchLens]
|
||||
);
|
||||
const [visualizeTriggerFieldContext, setVisualizeTriggerFieldContext] = useState(
|
||||
props.initialContext
|
||||
);
|
||||
const { onError } = props;
|
||||
const activeVisualization =
|
||||
state.visualization.activeId && props.visualizationMap[state.visualization.activeId];
|
||||
|
||||
const allLoaded = Object.values(state.datasourceStates).every(
|
||||
({ isLoading }) => typeof isLoading === 'boolean' && !isLoading
|
||||
);
|
||||
const allLoaded = Object.values(datasourceStates).every(({ isLoading }) => isLoading === false);
|
||||
|
||||
// Initialize current datasource and all active datasources
|
||||
useEffect(
|
||||
() => {
|
||||
// prevents executing dispatch on unmounted component
|
||||
let isUnmounted = false;
|
||||
if (!allLoaded) {
|
||||
initializeDatasources(
|
||||
props.datasourceMap,
|
||||
state.datasourceStates,
|
||||
persistedDoc?.references,
|
||||
visualizeTriggerFieldContext,
|
||||
{ isFullEditor: true }
|
||||
)
|
||||
.then((result) => {
|
||||
if (!isUnmounted) {
|
||||
Object.entries(result).forEach(([datasourceId, { state: datasourceState }]) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_DATASOURCE_STATE',
|
||||
updater: datasourceState,
|
||||
datasourceId,
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(onError);
|
||||
}
|
||||
return () => {
|
||||
isUnmounted = true;
|
||||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[allLoaded, onError]
|
||||
const datasourceLayers = React.useMemo(
|
||||
() => createDatasourceLayers(props.datasourceMap, datasourceStates),
|
||||
[props.datasourceMap, datasourceStates]
|
||||
);
|
||||
const datasourceLayers = createDatasourceLayers(props.datasourceMap, state.datasourceStates);
|
||||
|
||||
const framePublicAPI: FramePublicAPI = useMemo(
|
||||
() => ({
|
||||
|
@ -126,232 +61,15 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
query,
|
||||
filters,
|
||||
searchSessionId,
|
||||
availablePalettes: props.palettes,
|
||||
|
||||
addNewLayer() {
|
||||
const newLayerId = generateId();
|
||||
|
||||
dispatch({
|
||||
type: 'UPDATE_LAYER',
|
||||
datasourceId: state.activeDatasourceId!,
|
||||
layerId: newLayerId,
|
||||
updater: props.datasourceMap[state.activeDatasourceId!].insertLayer,
|
||||
});
|
||||
|
||||
return newLayerId;
|
||||
},
|
||||
|
||||
removeLayers(layerIds: string[]) {
|
||||
if (activeVisualization && activeVisualization.removeLayer && state.visualization.state) {
|
||||
dispatch({
|
||||
type: 'UPDATE_VISUALIZATION_STATE',
|
||||
visualizationId: activeVisualization.id,
|
||||
updater: layerIds.reduce(
|
||||
(acc, layerId) =>
|
||||
activeVisualization.removeLayer
|
||||
? activeVisualization.removeLayer(acc, layerId)
|
||||
: acc,
|
||||
state.visualization.state
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
layerIds.forEach((layerId) => {
|
||||
const layerDatasourceId = Object.entries(props.datasourceMap).find(
|
||||
([datasourceId, datasource]) =>
|
||||
state.datasourceStates[datasourceId] &&
|
||||
datasource.getLayers(state.datasourceStates[datasourceId].state).includes(layerId)
|
||||
)![0];
|
||||
dispatch({
|
||||
type: 'UPDATE_LAYER',
|
||||
layerId,
|
||||
datasourceId: layerDatasourceId,
|
||||
updater: props.datasourceMap[layerDatasourceId].removeLayer,
|
||||
});
|
||||
});
|
||||
},
|
||||
}),
|
||||
[
|
||||
activeData,
|
||||
activeVisualization,
|
||||
datasourceLayers,
|
||||
dateRange,
|
||||
query,
|
||||
filters,
|
||||
searchSessionId,
|
||||
props.palettes,
|
||||
props.datasourceMap,
|
||||
state.activeDatasourceId,
|
||||
state.datasourceStates,
|
||||
state.visualization.state,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (persistedDoc) {
|
||||
dispatch({
|
||||
type: 'VISUALIZATION_LOADED',
|
||||
doc: {
|
||||
...persistedDoc,
|
||||
state: {
|
||||
...persistedDoc.state,
|
||||
visualization: persistedDoc.visualizationType
|
||||
? props.visualizationMap[persistedDoc.visualizationType].initialize(
|
||||
framePublicAPI,
|
||||
persistedDoc.state.visualization
|
||||
)
|
||||
: persistedDoc.state.visualization,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'RESET',
|
||||
state: getInitialState(props),
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[persistedDoc]
|
||||
);
|
||||
|
||||
// Initialize visualization as soon as all datasources are ready
|
||||
useEffect(
|
||||
() => {
|
||||
if (allLoaded && state.visualization.state === null && activeVisualization) {
|
||||
const initialVisualizationState = activeVisualization.initialize(framePublicAPI);
|
||||
dispatch({
|
||||
type: 'UPDATE_VISUALIZATION_STATE',
|
||||
visualizationId: activeVisualization.id,
|
||||
updater: initialVisualizationState,
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[allLoaded, activeVisualization, state.visualization.state]
|
||||
);
|
||||
|
||||
// Get suggestions for visualize field when all datasources are ready
|
||||
useEffect(() => {
|
||||
if (allLoaded && visualizeTriggerFieldContext && !persistedDoc) {
|
||||
applyVisualizeFieldSuggestions({
|
||||
datasourceMap: props.datasourceMap,
|
||||
datasourceStates: state.datasourceStates,
|
||||
visualizationMap: props.visualizationMap,
|
||||
activeVisualizationId: state.visualization.activeId,
|
||||
visualizationState: state.visualization.state,
|
||||
visualizeTriggerFieldContext,
|
||||
dispatch,
|
||||
});
|
||||
setVisualizeTriggerFieldContext(undefined);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [allLoaded]);
|
||||
|
||||
const getStateToUpdate: (
|
||||
arg: {
|
||||
filterableIndexPatterns: string[];
|
||||
doc: Document;
|
||||
isSaveable: boolean;
|
||||
},
|
||||
oldState: {
|
||||
isSaveable: boolean;
|
||||
indexPatternsForTopNav: IndexPattern[];
|
||||
persistedDoc?: Document;
|
||||
lastKnownDoc?: Document;
|
||||
}
|
||||
) => Promise<Partial<LensAppState> | undefined> = async (
|
||||
{ filterableIndexPatterns, doc, isSaveable: incomingIsSaveable },
|
||||
prevState
|
||||
) => {
|
||||
const batchedStateToUpdate: Partial<LensAppState> = {};
|
||||
|
||||
if (incomingIsSaveable !== prevState.isSaveable) {
|
||||
batchedStateToUpdate.isSaveable = incomingIsSaveable;
|
||||
}
|
||||
|
||||
if (!isEqual(prevState.persistedDoc, doc) && !isEqual(prevState.lastKnownDoc, doc)) {
|
||||
batchedStateToUpdate.lastKnownDoc = doc;
|
||||
}
|
||||
const hasIndexPatternsChanged =
|
||||
prevState.indexPatternsForTopNav.length !== filterableIndexPatterns.length ||
|
||||
filterableIndexPatterns.some(
|
||||
(id) => !prevState.indexPatternsForTopNav.find((indexPattern) => indexPattern.id === id)
|
||||
);
|
||||
// Update the cached index patterns if the user made a change to any of them
|
||||
if (hasIndexPatternsChanged) {
|
||||
const { indexPatterns } = await getAllIndexPatterns(
|
||||
filterableIndexPatterns,
|
||||
props.plugins.data.indexPatterns
|
||||
);
|
||||
if (indexPatterns) {
|
||||
batchedStateToUpdate.indexPatternsForTopNav = indexPatterns;
|
||||
}
|
||||
}
|
||||
if (Object.keys(batchedStateToUpdate).length) {
|
||||
return batchedStateToUpdate;
|
||||
}
|
||||
};
|
||||
|
||||
// The frame needs to call onChange every time its internal state changes
|
||||
useEffect(
|
||||
() => {
|
||||
const activeDatasource =
|
||||
state.activeDatasourceId && !state.datasourceStates[state.activeDatasourceId].isLoading
|
||||
? props.datasourceMap[state.activeDatasourceId]
|
||||
: undefined;
|
||||
|
||||
if (!activeDatasource || !activeVisualization) {
|
||||
return;
|
||||
}
|
||||
|
||||
const savedObjectFormat = getSavedObjectFormat({
|
||||
activeDatasources: Object.keys(state.datasourceStates).reduce(
|
||||
(datasourceMap, datasourceId) => ({
|
||||
...datasourceMap,
|
||||
[datasourceId]: props.datasourceMap[datasourceId],
|
||||
}),
|
||||
{}
|
||||
),
|
||||
visualization: activeVisualization,
|
||||
state,
|
||||
framePublicAPI,
|
||||
});
|
||||
|
||||
// Frame loader (app or embeddable) is expected to call this when it loads and updates
|
||||
// This should be replaced with a top-down state
|
||||
getStateToUpdate(savedObjectFormat, {
|
||||
isSaveable,
|
||||
persistedDoc,
|
||||
indexPatternsForTopNav,
|
||||
lastKnownDoc,
|
||||
}).then((batchedStateToUpdate) => {
|
||||
if (batchedStateToUpdate) {
|
||||
dispatchChange(batchedStateToUpdate);
|
||||
}
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
activeVisualization,
|
||||
state.datasourceStates,
|
||||
state.visualization,
|
||||
activeData,
|
||||
query,
|
||||
filters,
|
||||
savedQuery,
|
||||
state.title,
|
||||
dispatchChange,
|
||||
]
|
||||
[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 { activeDatasourceId, datasourceStates } = state;
|
||||
const activeVisualizationId = state.visualization.activeId;
|
||||
const visualizationState = state.visualization.state;
|
||||
const activeVisualizationId = visualization.activeId;
|
||||
const visualizationState = visualization.state;
|
||||
const { visualizationMap, datasourceMap } = props;
|
||||
|
||||
if (!field || !activeDatasourceId) {
|
||||
|
@ -379,93 +97,77 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
const suggestion = getSuggestionForField.current!(field);
|
||||
if (suggestion) {
|
||||
trackUiEvent('drop_onto_workspace');
|
||||
switchToSuggestion(dispatch, suggestion, 'SWITCH_VISUALIZATION');
|
||||
switchToSuggestion(dispatchLens, suggestion, 'SWITCH_VISUALIZATION');
|
||||
}
|
||||
},
|
||||
[getSuggestionForField]
|
||||
[getSuggestionForField, dispatchLens]
|
||||
);
|
||||
|
||||
return (
|
||||
<RootDragDropProvider>
|
||||
<FrameLayout
|
||||
isFullscreen={Boolean(state.isFullscreenDatasource)}
|
||||
isFullscreen={Boolean(isFullscreenDatasource)}
|
||||
dataPanel={
|
||||
<DataPanelWrapper
|
||||
datasourceMap={props.datasourceMap}
|
||||
activeDatasource={state.activeDatasourceId}
|
||||
datasourceState={
|
||||
state.activeDatasourceId
|
||||
? state.datasourceStates[state.activeDatasourceId].state
|
||||
: null
|
||||
}
|
||||
datasourceIsLoading={
|
||||
state.activeDatasourceId
|
||||
? state.datasourceStates[state.activeDatasourceId].isLoading
|
||||
: true
|
||||
}
|
||||
dispatch={dispatch}
|
||||
core={props.core}
|
||||
query={query}
|
||||
dateRange={dateRange}
|
||||
filters={filters}
|
||||
plugins={props.plugins}
|
||||
showNoDataPopover={props.showNoDataPopover}
|
||||
activeDatasource={activeDatasourceId}
|
||||
datasourceState={activeDatasourceId ? datasourceStates[activeDatasourceId].state : null}
|
||||
datasourceIsLoading={
|
||||
activeDatasourceId ? datasourceStates[activeDatasourceId].isLoading : true
|
||||
}
|
||||
dropOntoWorkspace={dropOntoWorkspace}
|
||||
hasSuggestionForField={hasSuggestionForField}
|
||||
plugins={props.plugins}
|
||||
/>
|
||||
}
|
||||
configPanel={
|
||||
allLoaded && (
|
||||
<ConfigPanelWrapper
|
||||
activeDatasourceId={state.activeDatasourceId!}
|
||||
activeDatasourceId={activeDatasourceId!}
|
||||
datasourceMap={props.datasourceMap}
|
||||
datasourceStates={state.datasourceStates}
|
||||
datasourceStates={datasourceStates}
|
||||
visualizationMap={props.visualizationMap}
|
||||
activeVisualizationId={state.visualization.activeId}
|
||||
dispatch={dispatch}
|
||||
visualizationState={state.visualization.state}
|
||||
activeVisualizationId={visualization.activeId}
|
||||
visualizationState={visualization.state}
|
||||
framePublicAPI={framePublicAPI}
|
||||
core={props.core}
|
||||
isFullscreen={Boolean(state.isFullscreenDatasource)}
|
||||
isFullscreen={Boolean(isFullscreenDatasource)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
workspacePanel={
|
||||
allLoaded && (
|
||||
<WorkspacePanel
|
||||
title={state.title}
|
||||
activeDatasourceId={state.activeDatasourceId}
|
||||
activeVisualizationId={state.visualization.activeId}
|
||||
activeDatasourceId={activeDatasourceId}
|
||||
activeVisualizationId={visualization.activeId}
|
||||
datasourceMap={props.datasourceMap}
|
||||
datasourceStates={state.datasourceStates}
|
||||
datasourceStates={datasourceStates}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationState={state.visualization.state}
|
||||
visualizationState={visualization.state}
|
||||
visualizationMap={props.visualizationMap}
|
||||
dispatch={dispatch}
|
||||
isFullscreen={Boolean(state.isFullscreenDatasource)}
|
||||
isFullscreen={Boolean(isFullscreenDatasource)}
|
||||
ExpressionRenderer={props.ExpressionRenderer}
|
||||
core={props.core}
|
||||
plugins={props.plugins}
|
||||
visualizeTriggerFieldContext={visualizeTriggerFieldContext}
|
||||
getSuggestionForField={getSuggestionForField.current}
|
||||
/>
|
||||
)
|
||||
}
|
||||
suggestionsPanel={
|
||||
allLoaded &&
|
||||
!state.isFullscreenDatasource && (
|
||||
!isFullscreenDatasource && (
|
||||
<SuggestionPanel
|
||||
frame={framePublicAPI}
|
||||
activeDatasourceId={state.activeDatasourceId}
|
||||
activeVisualizationId={state.visualization.activeId}
|
||||
datasourceMap={props.datasourceMap}
|
||||
datasourceStates={state.datasourceStates}
|
||||
visualizationState={state.visualization.state}
|
||||
visualizationMap={props.visualizationMap}
|
||||
dispatch={dispatch}
|
||||
datasourceMap={props.datasourceMap}
|
||||
ExpressionRenderer={props.ExpressionRenderer}
|
||||
stagedPreview={state.stagedPreview}
|
||||
plugins={props.plugins}
|
||||
stagedPreview={stagedPreview}
|
||||
frame={framePublicAPI}
|
||||
activeVisualizationId={visualization.activeId}
|
||||
activeDatasourceId={activeDatasourceId}
|
||||
datasourceStates={datasourceStates}
|
||||
visualizationState={visualization.state}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -7,4 +7,3 @@
|
|||
|
||||
export * from './editor_frame';
|
||||
export * from './state_helpers';
|
||||
export * from './state_management';
|
||||
|
|
|
@ -1,116 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getSavedObjectFormat, Props } from './save';
|
||||
import { createMockDatasource, createMockFramePublicAPI, createMockVisualization } from '../mocks';
|
||||
import { esFilters, IIndexPattern, IFieldType } from '../../../../../../src/plugins/data/public';
|
||||
|
||||
jest.mock('./expression_helpers');
|
||||
|
||||
describe('save editor frame state', () => {
|
||||
const mockVisualization = createMockVisualization();
|
||||
const mockDatasource = createMockDatasource('a');
|
||||
const mockIndexPattern = ({ id: 'indexpattern' } as unknown) as IIndexPattern;
|
||||
const mockField = ({ name: '@timestamp' } as unknown) as IFieldType;
|
||||
|
||||
mockDatasource.getPersistableState.mockImplementation((x) => ({
|
||||
state: x,
|
||||
savedObjectReferences: [],
|
||||
}));
|
||||
const saveArgs: Props = {
|
||||
activeDatasources: {
|
||||
indexpattern: mockDatasource,
|
||||
},
|
||||
visualization: mockVisualization,
|
||||
state: {
|
||||
title: 'aaa',
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
state: 'hello',
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
activeDatasourceId: 'indexpattern',
|
||||
visualization: { activeId: '2', state: {} },
|
||||
},
|
||||
framePublicAPI: {
|
||||
...createMockFramePublicAPI(),
|
||||
addNewLayer: jest.fn(),
|
||||
removeLayers: jest.fn(),
|
||||
datasourceLayers: {
|
||||
first: mockDatasource.publicAPIMock,
|
||||
},
|
||||
query: { query: '', language: 'lucene' },
|
||||
dateRange: { fromDate: 'now-7d', toDate: 'now' },
|
||||
filters: [esFilters.buildExistsFilter(mockField, mockIndexPattern)],
|
||||
},
|
||||
};
|
||||
|
||||
it('transforms from internal state to persisted doc format', async () => {
|
||||
const datasource = createMockDatasource('a');
|
||||
datasource.getPersistableState.mockImplementation((state) => ({
|
||||
state: {
|
||||
stuff: `${state}_datasource_persisted`,
|
||||
},
|
||||
savedObjectReferences: [],
|
||||
}));
|
||||
datasource.toExpression.mockReturnValue('my | expr');
|
||||
|
||||
const visualization = createMockVisualization();
|
||||
visualization.toExpression.mockReturnValue('vis | expr');
|
||||
|
||||
const { doc, filterableIndexPatterns, isSaveable } = await getSavedObjectFormat({
|
||||
...saveArgs,
|
||||
activeDatasources: {
|
||||
indexpattern: datasource,
|
||||
},
|
||||
state: {
|
||||
title: 'bbb',
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
state: '2',
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
activeDatasourceId: 'indexpattern',
|
||||
visualization: { activeId: '3', state: '4' },
|
||||
},
|
||||
visualization,
|
||||
});
|
||||
|
||||
expect(filterableIndexPatterns).toEqual([]);
|
||||
expect(isSaveable).toEqual(true);
|
||||
expect(doc).toEqual({
|
||||
id: undefined,
|
||||
state: {
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
stuff: '2_datasource_persisted',
|
||||
},
|
||||
},
|
||||
visualization: '4',
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [
|
||||
{
|
||||
meta: { indexRefName: 'filter-index-pattern-0' },
|
||||
exists: { field: '@timestamp' },
|
||||
},
|
||||
],
|
||||
},
|
||||
references: [
|
||||
{
|
||||
id: 'indexpattern',
|
||||
name: 'filter-index-pattern-0',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
title: 'bbb',
|
||||
type: 'lens',
|
||||
visualizationType: '3',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,79 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { uniq } from 'lodash';
|
||||
import { SavedObjectReference } from 'kibana/public';
|
||||
import { EditorFrameState } from './state_management';
|
||||
import { Document } from '../../persistence/saved_object_store';
|
||||
import { Datasource, Visualization, FramePublicAPI } from '../../types';
|
||||
import { extractFilterReferences } from '../../persistence';
|
||||
import { buildExpression } from './expression_helpers';
|
||||
|
||||
export interface Props {
|
||||
activeDatasources: Record<string, Datasource>;
|
||||
state: EditorFrameState;
|
||||
visualization: Visualization;
|
||||
framePublicAPI: FramePublicAPI;
|
||||
}
|
||||
|
||||
export function getSavedObjectFormat({
|
||||
activeDatasources,
|
||||
state,
|
||||
visualization,
|
||||
framePublicAPI,
|
||||
}: Props): {
|
||||
doc: Document;
|
||||
filterableIndexPatterns: string[];
|
||||
isSaveable: boolean;
|
||||
} {
|
||||
const datasourceStates: Record<string, unknown> = {};
|
||||
const references: SavedObjectReference[] = [];
|
||||
Object.entries(activeDatasources).forEach(([id, datasource]) => {
|
||||
const { state: persistableState, savedObjectReferences } = datasource.getPersistableState(
|
||||
state.datasourceStates[id].state
|
||||
);
|
||||
datasourceStates[id] = persistableState;
|
||||
references.push(...savedObjectReferences);
|
||||
});
|
||||
|
||||
const uniqueFilterableIndexPatternIds = uniq(
|
||||
references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id)
|
||||
);
|
||||
|
||||
const { persistableFilters, references: filterReferences } = extractFilterReferences(
|
||||
framePublicAPI.filters
|
||||
);
|
||||
|
||||
references.push(...filterReferences);
|
||||
|
||||
const expression = buildExpression({
|
||||
visualization,
|
||||
visualizationState: state.visualization.state,
|
||||
datasourceMap: activeDatasources,
|
||||
datasourceStates: state.datasourceStates,
|
||||
datasourceLayers: framePublicAPI.datasourceLayers,
|
||||
});
|
||||
|
||||
return {
|
||||
doc: {
|
||||
savedObjectId: state.persistedId,
|
||||
title: state.title,
|
||||
description: state.description,
|
||||
type: 'lens',
|
||||
visualizationType: state.visualization.activeId,
|
||||
state: {
|
||||
datasourceStates,
|
||||
visualization: state.visualization.state,
|
||||
query: framePublicAPI.query,
|
||||
filters: persistableFilters,
|
||||
},
|
||||
references,
|
||||
},
|
||||
filterableIndexPatterns: uniqueFilterableIndexPatternIds,
|
||||
isSaveable: expression !== null,
|
||||
};
|
||||
}
|
|
@ -19,7 +19,7 @@ import {
|
|||
import { buildExpression } from './expression_helpers';
|
||||
import { Document } from '../../persistence/saved_object_store';
|
||||
import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public';
|
||||
import { getActiveDatasourceIdFromDoc } from './state_management';
|
||||
import { getActiveDatasourceIdFromDoc } from '../../utils';
|
||||
import { ErrorMessage } from '../types';
|
||||
import {
|
||||
getMissingCurrentDatasource,
|
||||
|
|
|
@ -1,415 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getInitialState, reducer } from './state_management';
|
||||
import { EditorFrameProps } from './index';
|
||||
import { Datasource, Visualization } from '../../types';
|
||||
import { createExpressionRendererMock } from '../mocks';
|
||||
import { coreMock } from 'src/core/public/mocks';
|
||||
import { uiActionsPluginMock } from '../../../../../../src/plugins/ui_actions/public/mocks';
|
||||
import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks';
|
||||
import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks';
|
||||
import { chartPluginMock } from 'src/plugins/charts/public/mocks';
|
||||
|
||||
describe('editor_frame state management', () => {
|
||||
describe('initialization', () => {
|
||||
let props: EditorFrameProps;
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
onError: jest.fn(),
|
||||
datasourceMap: { testDatasource: ({} as unknown) as Datasource },
|
||||
visualizationMap: { testVis: ({ initialize: jest.fn() } as unknown) as Visualization },
|
||||
ExpressionRenderer: createExpressionRendererMock(),
|
||||
core: coreMock.createStart(),
|
||||
plugins: {
|
||||
uiActions: uiActionsPluginMock.createStartContract(),
|
||||
data: dataPluginMock.createStartContract(),
|
||||
expressions: expressionsPluginMock.createStartContract(),
|
||||
charts: chartPluginMock.createStartContract(),
|
||||
},
|
||||
palettes: chartPluginMock.createPaletteRegistry(),
|
||||
showNoDataPopover: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should store initial datasource and visualization', () => {
|
||||
const initialState = getInitialState(props);
|
||||
expect(initialState.activeDatasourceId).toEqual('testDatasource');
|
||||
expect(initialState.visualization.activeId).toEqual('testVis');
|
||||
});
|
||||
|
||||
it('should not initialize visualization but set active id', () => {
|
||||
const initialState = getInitialState(props);
|
||||
|
||||
expect(initialState.visualization.state).toBe(null);
|
||||
expect(initialState.visualization.activeId).toBe('testVis');
|
||||
expect(props.visualizationMap.testVis.initialize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prefill state if doc is passed in', () => {
|
||||
const initialState = getInitialState({
|
||||
...props,
|
||||
doc: {
|
||||
state: {
|
||||
datasourceStates: {
|
||||
testDatasource: { internalState1: '' },
|
||||
testDatasource2: { internalState2: '' },
|
||||
},
|
||||
visualization: {},
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
},
|
||||
references: [],
|
||||
title: '',
|
||||
visualizationType: 'testVis',
|
||||
},
|
||||
});
|
||||
|
||||
expect(initialState.datasourceStates).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"testDatasource": Object {
|
||||
"isLoading": true,
|
||||
"state": Object {
|
||||
"internalState1": "",
|
||||
},
|
||||
},
|
||||
"testDatasource2": Object {
|
||||
"isLoading": true,
|
||||
"state": Object {
|
||||
"internalState2": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(initialState.visualization).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"activeId": "testVis",
|
||||
"state": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not set active id if initiated with empty document and visualizationMap is empty', () => {
|
||||
const initialState = getInitialState({ ...props, visualizationMap: {} });
|
||||
|
||||
expect(initialState.visualization.state).toEqual(null);
|
||||
expect(initialState.visualization.activeId).toEqual(null);
|
||||
expect(props.visualizationMap.testVis.initialize).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('state update', () => {
|
||||
it('should update the corresponding visualization state on update', () => {
|
||||
const newVisState = {};
|
||||
const newState = reducer(
|
||||
{
|
||||
datasourceStates: {
|
||||
testDatasource: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
activeDatasourceId: 'testDatasource',
|
||||
title: 'aaa',
|
||||
visualization: {
|
||||
activeId: 'testVis',
|
||||
state: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'UPDATE_VISUALIZATION_STATE',
|
||||
visualizationId: 'testVis',
|
||||
updater: newVisState,
|
||||
}
|
||||
);
|
||||
|
||||
expect(newState.visualization.state).toBe(newVisState);
|
||||
});
|
||||
|
||||
it('should update the datasource state with passed in reducer', () => {
|
||||
const datasourceReducer = jest.fn(() => ({ changed: true }));
|
||||
const newState = reducer(
|
||||
{
|
||||
datasourceStates: {
|
||||
testDatasource: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
activeDatasourceId: 'testDatasource',
|
||||
title: 'bbb',
|
||||
visualization: {
|
||||
activeId: 'testVis',
|
||||
state: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'UPDATE_DATASOURCE_STATE',
|
||||
updater: datasourceReducer,
|
||||
datasourceId: 'testDatasource',
|
||||
}
|
||||
);
|
||||
|
||||
expect(newState.datasourceStates.testDatasource.state).toEqual({ changed: true });
|
||||
expect(datasourceReducer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should update the layer state with passed in reducer', () => {
|
||||
const newDatasourceState = {};
|
||||
const newState = reducer(
|
||||
{
|
||||
datasourceStates: {
|
||||
testDatasource: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
activeDatasourceId: 'testDatasource',
|
||||
title: 'bbb',
|
||||
visualization: {
|
||||
activeId: 'testVis',
|
||||
state: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'UPDATE_DATASOURCE_STATE',
|
||||
updater: newDatasourceState,
|
||||
datasourceId: 'testDatasource',
|
||||
}
|
||||
);
|
||||
|
||||
expect(newState.datasourceStates.testDatasource.state).toBe(newDatasourceState);
|
||||
});
|
||||
|
||||
it('should should switch active visualization', () => {
|
||||
const testVisState = {};
|
||||
const newVisState = {};
|
||||
const newState = reducer(
|
||||
{
|
||||
datasourceStates: {
|
||||
testDatasource: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
activeDatasourceId: 'testDatasource',
|
||||
title: 'ccc',
|
||||
visualization: {
|
||||
activeId: 'testVis',
|
||||
state: testVisState,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'SWITCH_VISUALIZATION',
|
||||
newVisualizationId: 'testVis2',
|
||||
initialState: newVisState,
|
||||
}
|
||||
);
|
||||
|
||||
expect(newState.visualization.state).toBe(newVisState);
|
||||
});
|
||||
|
||||
it('should should switch active visualization and update datasource state', () => {
|
||||
const testVisState = {};
|
||||
const newVisState = {};
|
||||
const newDatasourceState = {};
|
||||
const newState = reducer(
|
||||
{
|
||||
datasourceStates: {
|
||||
testDatasource: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
activeDatasourceId: 'testDatasource',
|
||||
title: 'ddd',
|
||||
visualization: {
|
||||
activeId: 'testVis',
|
||||
state: testVisState,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'SWITCH_VISUALIZATION',
|
||||
newVisualizationId: 'testVis2',
|
||||
initialState: newVisState,
|
||||
datasourceState: newDatasourceState,
|
||||
datasourceId: 'testDatasource',
|
||||
}
|
||||
);
|
||||
|
||||
expect(newState.visualization.state).toBe(newVisState);
|
||||
expect(newState.datasourceStates.testDatasource.state).toBe(newDatasourceState);
|
||||
});
|
||||
|
||||
it('should should switch active datasource and initialize new state', () => {
|
||||
const newState = reducer(
|
||||
{
|
||||
datasourceStates: {
|
||||
testDatasource: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
activeDatasourceId: 'testDatasource',
|
||||
title: 'eee',
|
||||
visualization: {
|
||||
activeId: 'testVis',
|
||||
state: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'SWITCH_DATASOURCE',
|
||||
newDatasourceId: 'testDatasource2',
|
||||
}
|
||||
);
|
||||
|
||||
expect(newState.activeDatasourceId).toEqual('testDatasource2');
|
||||
expect(newState.datasourceStates.testDatasource2.isLoading).toEqual(true);
|
||||
});
|
||||
|
||||
it('not initialize already initialized datasource on switch', () => {
|
||||
const datasource2State = {};
|
||||
const newState = reducer(
|
||||
{
|
||||
datasourceStates: {
|
||||
testDatasource: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
testDatasource2: {
|
||||
state: datasource2State,
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
activeDatasourceId: 'testDatasource',
|
||||
title: 'eee',
|
||||
visualization: {
|
||||
activeId: 'testVis',
|
||||
state: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'SWITCH_DATASOURCE',
|
||||
newDatasourceId: 'testDatasource2',
|
||||
}
|
||||
);
|
||||
|
||||
expect(newState.activeDatasourceId).toEqual('testDatasource2');
|
||||
expect(newState.datasourceStates.testDatasource2.state).toBe(datasource2State);
|
||||
});
|
||||
|
||||
it('should reset the state', () => {
|
||||
const newState = reducer(
|
||||
{
|
||||
datasourceStates: {
|
||||
a: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
activeDatasourceId: 'a',
|
||||
title: 'jjj',
|
||||
visualization: {
|
||||
activeId: 'b',
|
||||
state: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'RESET',
|
||||
state: {
|
||||
datasourceStates: {
|
||||
z: {
|
||||
isLoading: false,
|
||||
state: { hola: 'muchacho' },
|
||||
},
|
||||
},
|
||||
activeDatasourceId: 'z',
|
||||
persistedId: 'bar',
|
||||
title: 'lll',
|
||||
visualization: {
|
||||
activeId: 'q',
|
||||
state: { my: 'viz' },
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(newState).toMatchObject({
|
||||
datasourceStates: {
|
||||
z: {
|
||||
isLoading: false,
|
||||
state: { hola: 'muchacho' },
|
||||
},
|
||||
},
|
||||
activeDatasourceId: 'z',
|
||||
persistedId: 'bar',
|
||||
visualization: {
|
||||
activeId: 'q',
|
||||
state: { my: 'viz' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should load the state from the doc', () => {
|
||||
const newState = reducer(
|
||||
{
|
||||
datasourceStates: {
|
||||
a: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
activeDatasourceId: 'a',
|
||||
title: 'mmm',
|
||||
visualization: {
|
||||
activeId: 'b',
|
||||
state: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'VISUALIZATION_LOADED',
|
||||
doc: {
|
||||
savedObjectId: 'b',
|
||||
state: {
|
||||
datasourceStates: { a: { foo: 'c' } },
|
||||
visualization: { bar: 'd' },
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
},
|
||||
title: 'heyo!',
|
||||
description: 'My lens',
|
||||
type: 'lens',
|
||||
visualizationType: 'line',
|
||||
references: [],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(newState).toEqual({
|
||||
activeDatasourceId: 'a',
|
||||
datasourceStates: {
|
||||
a: {
|
||||
isLoading: true,
|
||||
state: {
|
||||
foo: 'c',
|
||||
},
|
||||
},
|
||||
},
|
||||
persistedId: 'b',
|
||||
title: 'heyo!',
|
||||
description: 'My lens',
|
||||
visualization: {
|
||||
activeId: 'line',
|
||||
state: {
|
||||
bar: 'd',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,293 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EditorFrameProps } from './index';
|
||||
import { Document } from '../../persistence/saved_object_store';
|
||||
|
||||
export interface PreviewState {
|
||||
visualization: {
|
||||
activeId: string | null;
|
||||
state: unknown;
|
||||
};
|
||||
datasourceStates: Record<string, { state: unknown; isLoading: boolean }>;
|
||||
}
|
||||
|
||||
export interface EditorFrameState extends PreviewState {
|
||||
persistedId?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
stagedPreview?: PreviewState;
|
||||
activeDatasourceId: string | null;
|
||||
isFullscreenDatasource?: boolean;
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| {
|
||||
type: 'RESET';
|
||||
state: EditorFrameState;
|
||||
}
|
||||
| {
|
||||
type: 'UPDATE_TITLE';
|
||||
title: string;
|
||||
}
|
||||
| {
|
||||
type: 'UPDATE_STATE';
|
||||
// Just for diagnostics, so we can determine what action
|
||||
// caused this update.
|
||||
subType: string;
|
||||
updater: (prevState: EditorFrameState) => EditorFrameState;
|
||||
}
|
||||
| {
|
||||
type: 'UPDATE_DATASOURCE_STATE';
|
||||
updater: unknown | ((prevState: unknown) => unknown);
|
||||
datasourceId: string;
|
||||
clearStagedPreview?: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'UPDATE_VISUALIZATION_STATE';
|
||||
visualizationId: string;
|
||||
updater: unknown | ((state: unknown) => unknown);
|
||||
clearStagedPreview?: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'UPDATE_LAYER';
|
||||
layerId: string;
|
||||
datasourceId: string;
|
||||
updater: (state: unknown, layerId: string) => unknown;
|
||||
}
|
||||
| {
|
||||
type: 'VISUALIZATION_LOADED';
|
||||
doc: Document;
|
||||
}
|
||||
| {
|
||||
type: 'SWITCH_VISUALIZATION';
|
||||
newVisualizationId: string;
|
||||
initialState: unknown;
|
||||
}
|
||||
| {
|
||||
type: 'SWITCH_VISUALIZATION';
|
||||
newVisualizationId: string;
|
||||
initialState: unknown;
|
||||
datasourceState: unknown;
|
||||
datasourceId: string;
|
||||
}
|
||||
| {
|
||||
type: 'SELECT_SUGGESTION';
|
||||
newVisualizationId: string;
|
||||
initialState: unknown;
|
||||
datasourceState: unknown;
|
||||
datasourceId: string;
|
||||
}
|
||||
| {
|
||||
type: 'ROLLBACK_SUGGESTION';
|
||||
}
|
||||
| {
|
||||
type: 'SUBMIT_SUGGESTION';
|
||||
}
|
||||
| {
|
||||
type: 'SWITCH_DATASOURCE';
|
||||
newDatasourceId: string;
|
||||
}
|
||||
| {
|
||||
type: 'TOGGLE_FULLSCREEN';
|
||||
};
|
||||
|
||||
export function getActiveDatasourceIdFromDoc(doc?: Document) {
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [firstDatasourceFromDoc] = Object.keys(doc.state.datasourceStates);
|
||||
return firstDatasourceFromDoc || null;
|
||||
}
|
||||
|
||||
export const getInitialState = (
|
||||
params: EditorFrameProps & { doc?: Document }
|
||||
): EditorFrameState => {
|
||||
const datasourceStates: EditorFrameState['datasourceStates'] = {};
|
||||
|
||||
const initialDatasourceId =
|
||||
getActiveDatasourceIdFromDoc(params.doc) || Object.keys(params.datasourceMap)[0] || null;
|
||||
|
||||
const initialVisualizationId =
|
||||
(params.doc && params.doc.visualizationType) || Object.keys(params.visualizationMap)[0] || null;
|
||||
|
||||
if (params.doc) {
|
||||
Object.entries(params.doc.state.datasourceStates).forEach(([datasourceId, state]) => {
|
||||
datasourceStates[datasourceId] = { isLoading: true, state };
|
||||
});
|
||||
} else if (initialDatasourceId) {
|
||||
datasourceStates[initialDatasourceId] = {
|
||||
state: null,
|
||||
isLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: '',
|
||||
datasourceStates,
|
||||
activeDatasourceId: initialDatasourceId,
|
||||
visualization: {
|
||||
state: null,
|
||||
activeId: initialVisualizationId,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const reducer = (state: EditorFrameState, action: Action): EditorFrameState => {
|
||||
switch (action.type) {
|
||||
case 'RESET':
|
||||
return action.state;
|
||||
case 'UPDATE_TITLE':
|
||||
return { ...state, title: action.title };
|
||||
case 'UPDATE_STATE':
|
||||
return action.updater(state);
|
||||
case 'UPDATE_LAYER':
|
||||
return {
|
||||
...state,
|
||||
datasourceStates: {
|
||||
...state.datasourceStates,
|
||||
[action.datasourceId]: {
|
||||
...state.datasourceStates[action.datasourceId],
|
||||
state: action.updater(
|
||||
state.datasourceStates[action.datasourceId].state,
|
||||
action.layerId
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
case 'VISUALIZATION_LOADED':
|
||||
return {
|
||||
...state,
|
||||
persistedId: action.doc.savedObjectId,
|
||||
title: action.doc.title,
|
||||
description: action.doc.description,
|
||||
datasourceStates: Object.entries(action.doc.state.datasourceStates).reduce(
|
||||
(stateMap, [datasourceId, datasourceState]) => ({
|
||||
...stateMap,
|
||||
[datasourceId]: {
|
||||
isLoading: true,
|
||||
state: datasourceState,
|
||||
},
|
||||
}),
|
||||
{}
|
||||
),
|
||||
activeDatasourceId: getActiveDatasourceIdFromDoc(action.doc),
|
||||
visualization: {
|
||||
...state.visualization,
|
||||
activeId: action.doc.visualizationType,
|
||||
state: action.doc.state.visualization,
|
||||
},
|
||||
};
|
||||
case 'SWITCH_DATASOURCE':
|
||||
return {
|
||||
...state,
|
||||
datasourceStates: {
|
||||
...state.datasourceStates,
|
||||
[action.newDatasourceId]: state.datasourceStates[action.newDatasourceId] || {
|
||||
state: null,
|
||||
isLoading: true,
|
||||
},
|
||||
},
|
||||
activeDatasourceId: action.newDatasourceId,
|
||||
};
|
||||
case 'SWITCH_VISUALIZATION':
|
||||
return {
|
||||
...state,
|
||||
datasourceStates:
|
||||
'datasourceId' in action && action.datasourceId
|
||||
? {
|
||||
...state.datasourceStates,
|
||||
[action.datasourceId]: {
|
||||
...state.datasourceStates[action.datasourceId],
|
||||
state: action.datasourceState,
|
||||
},
|
||||
}
|
||||
: state.datasourceStates,
|
||||
visualization: {
|
||||
...state.visualization,
|
||||
activeId: action.newVisualizationId,
|
||||
state: action.initialState,
|
||||
},
|
||||
stagedPreview: undefined,
|
||||
};
|
||||
case 'SELECT_SUGGESTION':
|
||||
return {
|
||||
...state,
|
||||
datasourceStates:
|
||||
'datasourceId' in action && action.datasourceId
|
||||
? {
|
||||
...state.datasourceStates,
|
||||
[action.datasourceId]: {
|
||||
...state.datasourceStates[action.datasourceId],
|
||||
state: action.datasourceState,
|
||||
},
|
||||
}
|
||||
: state.datasourceStates,
|
||||
visualization: {
|
||||
...state.visualization,
|
||||
activeId: action.newVisualizationId,
|
||||
state: action.initialState,
|
||||
},
|
||||
stagedPreview: state.stagedPreview || {
|
||||
datasourceStates: state.datasourceStates,
|
||||
visualization: state.visualization,
|
||||
},
|
||||
};
|
||||
case 'ROLLBACK_SUGGESTION':
|
||||
return {
|
||||
...state,
|
||||
...(state.stagedPreview || {}),
|
||||
stagedPreview: undefined,
|
||||
};
|
||||
case 'SUBMIT_SUGGESTION':
|
||||
return {
|
||||
...state,
|
||||
stagedPreview: undefined,
|
||||
};
|
||||
case 'UPDATE_DATASOURCE_STATE':
|
||||
return {
|
||||
...state,
|
||||
datasourceStates: {
|
||||
...state.datasourceStates,
|
||||
[action.datasourceId]: {
|
||||
state:
|
||||
typeof action.updater === 'function'
|
||||
? action.updater(state.datasourceStates[action.datasourceId].state)
|
||||
: action.updater,
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
stagedPreview: action.clearStagedPreview ? undefined : state.stagedPreview,
|
||||
};
|
||||
case 'UPDATE_VISUALIZATION_STATE':
|
||||
if (!state.visualization.activeId) {
|
||||
throw new Error('Invariant: visualization state got updated without active visualization');
|
||||
}
|
||||
// This is a safeguard that prevents us from accidentally updating the
|
||||
// wrong visualization. This occurs in some cases due to the uncoordinated
|
||||
// way we manage state across plugins.
|
||||
if (state.visualization.activeId !== action.visualizationId) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
visualization: {
|
||||
...state.visualization,
|
||||
state:
|
||||
typeof action.updater === 'function'
|
||||
? action.updater(state.visualization.state)
|
||||
: action.updater,
|
||||
},
|
||||
stagedPreview: action.clearStagedPreview ? undefined : state.stagedPreview,
|
||||
};
|
||||
case 'TOGGLE_FULLSCREEN':
|
||||
return { ...state, isFullscreenDatasource: !state.isFullscreenDatasource };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { getSuggestions, getTopSuggestionForField } from './suggestion_helpers';
|
||||
import { createMockVisualization, createMockDatasource, DatasourceMock } from '../mocks';
|
||||
import { createMockVisualization, createMockDatasource, DatasourceMock } from '../../mocks';
|
||||
import { TableSuggestion, DatasourceSuggestion, Visualization } from '../../types';
|
||||
import { PaletteOutput } from 'src/plugins/charts/public';
|
||||
|
||||
|
|
|
@ -19,8 +19,8 @@ import {
|
|||
DatasourceSuggestion,
|
||||
DatasourcePublicAPI,
|
||||
} from '../../types';
|
||||
import { Action } from './state_management';
|
||||
import { DragDropIdentifier } from '../../drag_drop';
|
||||
import { LensDispatch, selectSuggestion, switchVisualization } from '../../state_management';
|
||||
|
||||
export interface Suggestion {
|
||||
visualizationId: string;
|
||||
|
@ -132,14 +132,13 @@ export function getSuggestions({
|
|||
).sort((a, b) => b.score - a.score);
|
||||
}
|
||||
|
||||
export function applyVisualizeFieldSuggestions({
|
||||
export function getVisualizeFieldSuggestions({
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
visualizationMap,
|
||||
activeVisualizationId,
|
||||
visualizationState,
|
||||
visualizeTriggerFieldContext,
|
||||
dispatch,
|
||||
}: {
|
||||
datasourceMap: Record<string, Datasource>;
|
||||
datasourceStates: Record<
|
||||
|
@ -154,8 +153,7 @@ export function applyVisualizeFieldSuggestions({
|
|||
subVisualizationId?: string;
|
||||
visualizationState: unknown;
|
||||
visualizeTriggerFieldContext?: VisualizeFieldContext;
|
||||
dispatch: (action: Action) => void;
|
||||
}): void {
|
||||
}): Suggestion | undefined {
|
||||
const suggestions = getSuggestions({
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
|
@ -165,9 +163,7 @@ export function applyVisualizeFieldSuggestions({
|
|||
visualizeTriggerFieldContext,
|
||||
});
|
||||
if (suggestions.length) {
|
||||
const selectedSuggestion =
|
||||
suggestions.find((s) => s.visualizationId === activeVisualizationId) || suggestions[0];
|
||||
switchToSuggestion(dispatch, selectedSuggestion, 'SWITCH_VISUALIZATION');
|
||||
return suggestions.find((s) => s.visualizationId === activeVisualizationId) || suggestions[0];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -207,22 +203,25 @@ function getVisualizationSuggestions(
|
|||
}
|
||||
|
||||
export function switchToSuggestion(
|
||||
dispatch: (action: Action) => void,
|
||||
dispatchLens: LensDispatch,
|
||||
suggestion: Pick<
|
||||
Suggestion,
|
||||
'visualizationId' | 'visualizationState' | 'datasourceState' | 'datasourceId'
|
||||
>,
|
||||
type: 'SWITCH_VISUALIZATION' | 'SELECT_SUGGESTION' = 'SELECT_SUGGESTION'
|
||||
) {
|
||||
const action: Action = {
|
||||
type,
|
||||
const pickedSuggestion = {
|
||||
newVisualizationId: suggestion.visualizationId,
|
||||
initialState: suggestion.visualizationState,
|
||||
datasourceState: suggestion.datasourceState,
|
||||
datasourceId: suggestion.datasourceId!,
|
||||
};
|
||||
|
||||
dispatch(action);
|
||||
dispatchLens(
|
||||
type === 'SELECT_SUGGESTION'
|
||||
? selectSuggestion(pickedSuggestion)
|
||||
: switchVisualization(pickedSuggestion)
|
||||
);
|
||||
}
|
||||
|
||||
export function getTopSuggestionForField(
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mountWithIntl as mount } from '@kbn/test/jest';
|
||||
import { Visualization } from '../../types';
|
||||
import {
|
||||
createMockVisualization,
|
||||
|
@ -14,15 +13,15 @@ import {
|
|||
createExpressionRendererMock,
|
||||
DatasourceMock,
|
||||
createMockFramePublicAPI,
|
||||
} from '../mocks';
|
||||
} from '../../mocks';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public';
|
||||
import { esFilters, IFieldType, IIndexPattern } from '../../../../../../src/plugins/data/public';
|
||||
import { SuggestionPanel, SuggestionPanelProps } from './suggestion_panel';
|
||||
import { getSuggestions, Suggestion } from './suggestion_helpers';
|
||||
import { EuiIcon, EuiPanel, EuiToolTip } from '@elastic/eui';
|
||||
import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks';
|
||||
import { LensIconChartDatatable } from '../../assets/chart_datatable';
|
||||
import { mountWithProvider } from '../../mocks';
|
||||
|
||||
jest.mock('./suggestion_helpers');
|
||||
|
||||
|
@ -33,7 +32,6 @@ describe('suggestion_panel', () => {
|
|||
let mockDatasource: DatasourceMock;
|
||||
|
||||
let expressionRendererMock: ReactExpressionRendererType;
|
||||
let dispatchMock: jest.Mock;
|
||||
|
||||
const suggestion1State = { suggestion1: true };
|
||||
const suggestion2State = { suggestion2: true };
|
||||
|
@ -44,7 +42,6 @@ describe('suggestion_panel', () => {
|
|||
mockVisualization = createMockVisualization();
|
||||
mockDatasource = createMockDatasource('a');
|
||||
expressionRendererMock = createExpressionRendererMock();
|
||||
dispatchMock = jest.fn();
|
||||
|
||||
getSuggestionsMock.mockReturnValue([
|
||||
{
|
||||
|
@ -84,18 +81,16 @@ describe('suggestion_panel', () => {
|
|||
vis2: createMockVisualization(),
|
||||
},
|
||||
visualizationState: {},
|
||||
dispatch: dispatchMock,
|
||||
ExpressionRenderer: expressionRendererMock,
|
||||
frame: createMockFramePublicAPI(),
|
||||
plugins: { data: dataPluginMock.createStartContract() },
|
||||
};
|
||||
});
|
||||
|
||||
it('should list passed in suggestions', () => {
|
||||
const wrapper = mount(<SuggestionPanel {...defaultProps} />);
|
||||
it('should list passed in suggestions', async () => {
|
||||
const { instance } = await mountWithProvider(<SuggestionPanel {...defaultProps} />);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
instance
|
||||
.find('[data-test-subj="lnsSuggestion"]')
|
||||
.find(EuiPanel)
|
||||
.map((el) => el.parents(EuiToolTip).prop('content'))
|
||||
|
@ -129,84 +124,148 @@ describe('suggestion_panel', () => {
|
|||
};
|
||||
});
|
||||
|
||||
it('should not update suggestions if current state is moved to staged preview', () => {
|
||||
const wrapper = mount(<SuggestionPanel {...defaultProps} />);
|
||||
it('should not update suggestions if current state is moved to staged preview', async () => {
|
||||
const { instance } = await mountWithProvider(<SuggestionPanel {...defaultProps} />);
|
||||
getSuggestionsMock.mockClear();
|
||||
wrapper.setProps({
|
||||
instance.setProps({
|
||||
stagedPreview,
|
||||
...suggestionState,
|
||||
});
|
||||
wrapper.update();
|
||||
instance.update();
|
||||
expect(getSuggestionsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update suggestions if staged preview is removed', () => {
|
||||
const wrapper = mount(<SuggestionPanel {...defaultProps} />);
|
||||
it('should update suggestions if staged preview is removed', async () => {
|
||||
const { instance } = await mountWithProvider(<SuggestionPanel {...defaultProps} />);
|
||||
getSuggestionsMock.mockClear();
|
||||
wrapper.setProps({
|
||||
instance.setProps({
|
||||
stagedPreview,
|
||||
...suggestionState,
|
||||
});
|
||||
wrapper.update();
|
||||
wrapper.setProps({
|
||||
instance.update();
|
||||
instance.setProps({
|
||||
stagedPreview: undefined,
|
||||
...suggestionState,
|
||||
});
|
||||
wrapper.update();
|
||||
instance.update();
|
||||
expect(getSuggestionsMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should highlight currently active suggestion', () => {
|
||||
const wrapper = mount(<SuggestionPanel {...defaultProps} />);
|
||||
it('should highlight currently active suggestion', async () => {
|
||||
const { instance } = await mountWithProvider(<SuggestionPanel {...defaultProps} />);
|
||||
|
||||
act(() => {
|
||||
wrapper.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click');
|
||||
instance.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click');
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
instance.update();
|
||||
|
||||
expect(wrapper.find('[data-test-subj="lnsSuggestion"]').at(2).prop('className')).toContain(
|
||||
expect(instance.find('[data-test-subj="lnsSuggestion"]').at(2).prop('className')).toContain(
|
||||
'lnsSuggestionPanel__button-isSelected'
|
||||
);
|
||||
});
|
||||
|
||||
it('should rollback suggestion if current panel is clicked', () => {
|
||||
const wrapper = mount(<SuggestionPanel {...defaultProps} />);
|
||||
it('should rollback suggestion if current panel is clicked', async () => {
|
||||
const { instance, lensStore } = await mountWithProvider(
|
||||
<SuggestionPanel {...defaultProps} />
|
||||
);
|
||||
|
||||
act(() => {
|
||||
wrapper.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click');
|
||||
instance.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click');
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
instance.update();
|
||||
|
||||
act(() => {
|
||||
wrapper.find('[data-test-subj="lnsSuggestion"]').at(0).simulate('click');
|
||||
instance.find('[data-test-subj="lnsSuggestion"]').at(0).simulate('click');
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
instance.update();
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledWith({
|
||||
type: 'ROLLBACK_SUGGESTION',
|
||||
expect(lensStore.dispatch).toHaveBeenCalledWith({
|
||||
type: 'lens/rollbackSuggestion',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch visualization switch action if suggestion is clicked', () => {
|
||||
const wrapper = mount(<SuggestionPanel {...defaultProps} />);
|
||||
it('should dispatch visualization switch action if suggestion is clicked', async () => {
|
||||
const { instance, lensStore } = await mountWithProvider(<SuggestionPanel {...defaultProps} />);
|
||||
|
||||
act(() => {
|
||||
wrapper.find('button[data-test-subj="lnsSuggestion"]').at(1).simulate('click');
|
||||
instance.find('button[data-test-subj="lnsSuggestion"]').at(1).simulate('click');
|
||||
});
|
||||
wrapper.update();
|
||||
instance.update();
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledWith(
|
||||
expect(lensStore.dispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'SELECT_SUGGESTION',
|
||||
initialState: suggestion1State,
|
||||
type: 'lens/selectSuggestion',
|
||||
payload: {
|
||||
datasourceId: undefined,
|
||||
datasourceState: {},
|
||||
initialState: { suggestion1: true },
|
||||
newVisualizationId: 'vis',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should render render icon if there is no preview expression', async () => {
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
getSuggestionsMock.mockReturnValue([
|
||||
{
|
||||
datasourceState: {},
|
||||
previewIcon: LensIconChartDatatable,
|
||||
score: 0.5,
|
||||
visualizationState: suggestion1State,
|
||||
visualizationId: 'vis',
|
||||
title: 'Suggestion1',
|
||||
},
|
||||
{
|
||||
datasourceState: {},
|
||||
previewIcon: 'empty',
|
||||
score: 0.5,
|
||||
visualizationState: suggestion2State,
|
||||
visualizationId: 'vis',
|
||||
title: 'Suggestion2',
|
||||
previewExpression: 'test | expression',
|
||||
},
|
||||
] as Suggestion[]);
|
||||
|
||||
(mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce(undefined);
|
||||
(mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce('test | expression');
|
||||
|
||||
// this call will go to the currently active visualization
|
||||
(mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce('current | preview');
|
||||
|
||||
mockDatasource.toExpression.mockReturnValue('datasource_expression');
|
||||
|
||||
const { instance } = await mountWithProvider(<SuggestionPanel {...defaultProps} />);
|
||||
|
||||
expect(instance.find(EuiIcon)).toHaveLength(1);
|
||||
expect(instance.find(EuiIcon).prop('type')).toEqual(LensIconChartDatatable);
|
||||
});
|
||||
|
||||
it('should return no suggestion if visualization has missing index-patterns', async () => {
|
||||
// create a layer that is referencing an indexPatterns not retrieved by the datasource
|
||||
const missingIndexPatternsState = {
|
||||
layers: { indexPatternId: 'a' },
|
||||
indexPatterns: {},
|
||||
};
|
||||
mockDatasource.checkIntegrity.mockReturnValue(['a']);
|
||||
const newProps = {
|
||||
...defaultProps,
|
||||
datasourceStates: {
|
||||
mock: {
|
||||
...defaultProps.datasourceStates.mock,
|
||||
state: missingIndexPatternsState,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { instance } = await mountWithProvider(<SuggestionPanel {...newProps} />);
|
||||
expect(instance.html()).toEqual(null);
|
||||
});
|
||||
|
||||
it('should render preview expression if there is one', () => {
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
(getSuggestions as jest.Mock).mockReturnValue([
|
||||
|
@ -235,7 +294,7 @@ describe('suggestion_panel', () => {
|
|||
const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern;
|
||||
const field = ({ name: 'myfield' } as unknown) as IFieldType;
|
||||
|
||||
mount(
|
||||
mountWithProvider(
|
||||
<SuggestionPanel
|
||||
{...defaultProps}
|
||||
frame={{
|
||||
|
@ -255,60 +314,4 @@ describe('suggestion_panel', () => {
|
|||
| expression"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should render render icon if there is no preview expression', () => {
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
getSuggestionsMock.mockReturnValue([
|
||||
{
|
||||
datasourceState: {},
|
||||
previewIcon: LensIconChartDatatable,
|
||||
score: 0.5,
|
||||
visualizationState: suggestion1State,
|
||||
visualizationId: 'vis',
|
||||
title: 'Suggestion1',
|
||||
},
|
||||
{
|
||||
datasourceState: {},
|
||||
previewIcon: 'empty',
|
||||
score: 0.5,
|
||||
visualizationState: suggestion2State,
|
||||
visualizationId: 'vis',
|
||||
title: 'Suggestion2',
|
||||
previewExpression: 'test | expression',
|
||||
},
|
||||
] as Suggestion[]);
|
||||
|
||||
(mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce(undefined);
|
||||
(mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce('test | expression');
|
||||
|
||||
// this call will go to the currently active visualization
|
||||
(mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce('current | preview');
|
||||
|
||||
mockDatasource.toExpression.mockReturnValue('datasource_expression');
|
||||
|
||||
const wrapper = mount(<SuggestionPanel {...defaultProps} />);
|
||||
|
||||
expect(wrapper.find(EuiIcon)).toHaveLength(1);
|
||||
expect(wrapper.find(EuiIcon).prop('type')).toEqual(LensIconChartDatatable);
|
||||
});
|
||||
|
||||
it('should return no suggestion if visualization has missing index-patterns', () => {
|
||||
// create a layer that is referencing an indexPatterns not retrieved by the datasource
|
||||
const missingIndexPatternsState = {
|
||||
layers: { indexPatternId: 'a' },
|
||||
indexPatterns: {},
|
||||
};
|
||||
mockDatasource.checkIntegrity.mockReturnValue(['a']);
|
||||
const newProps = {
|
||||
...defaultProps,
|
||||
datasourceStates: {
|
||||
mock: {
|
||||
...defaultProps.datasourceStates.mock,
|
||||
state: missingIndexPatternsState,
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = mount(<SuggestionPanel {...newProps} />);
|
||||
expect(wrapper.html()).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,8 +24,7 @@ import { IconType } from '@elastic/eui/src/components/icon/icon';
|
|||
import { Ast, toExpression } from '@kbn/interpreter/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import classNames from 'classnames';
|
||||
import { DataPublicPluginStart, ExecutionContextSearch } from 'src/plugins/data/public';
|
||||
import { Action, PreviewState } from './state_management';
|
||||
import { ExecutionContextSearch } from 'src/plugins/data/public';
|
||||
import { Datasource, Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../types';
|
||||
import { getSuggestions, switchToSuggestion } from './suggestion_helpers';
|
||||
import {
|
||||
|
@ -35,6 +34,12 @@ import {
|
|||
import { prependDatasourceExpression } from './expression_helpers';
|
||||
import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry';
|
||||
import { getMissingIndexPattern, validateDatasourceAndVisualization } from './state_helpers';
|
||||
import {
|
||||
PreviewState,
|
||||
rollbackSuggestion,
|
||||
submitSuggestion,
|
||||
useLensDispatch,
|
||||
} from '../../state_management';
|
||||
|
||||
const MAX_SUGGESTIONS_DISPLAYED = 5;
|
||||
|
||||
|
@ -51,11 +56,9 @@ export interface SuggestionPanelProps {
|
|||
activeVisualizationId: string | null;
|
||||
visualizationMap: Record<string, Visualization>;
|
||||
visualizationState: unknown;
|
||||
dispatch: (action: Action) => void;
|
||||
ExpressionRenderer: ReactExpressionRendererType;
|
||||
frame: FramePublicAPI;
|
||||
stagedPreview?: PreviewState;
|
||||
plugins: { data: DataPublicPluginStart };
|
||||
}
|
||||
|
||||
const PreviewRenderer = ({
|
||||
|
@ -170,12 +173,12 @@ export function SuggestionPanel({
|
|||
activeVisualizationId,
|
||||
visualizationMap,
|
||||
visualizationState,
|
||||
dispatch,
|
||||
frame,
|
||||
ExpressionRenderer: ExpressionRendererComponent,
|
||||
stagedPreview,
|
||||
plugins,
|
||||
}: SuggestionPanelProps) {
|
||||
const dispatchLens = useLensDispatch();
|
||||
|
||||
const currentDatasourceStates = stagedPreview ? stagedPreview.datasourceStates : datasourceStates;
|
||||
const currentVisualizationState = stagedPreview
|
||||
? stagedPreview.visualization.state
|
||||
|
@ -320,9 +323,7 @@ export function SuggestionPanel({
|
|||
if (lastSelectedSuggestion !== -1) {
|
||||
trackSuggestionEvent('back_to_current');
|
||||
setLastSelectedSuggestion(-1);
|
||||
dispatch({
|
||||
type: 'ROLLBACK_SUGGESTION',
|
||||
});
|
||||
dispatchLens(rollbackSuggestion());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -352,9 +353,7 @@ export function SuggestionPanel({
|
|||
iconType="refresh"
|
||||
onClick={() => {
|
||||
trackUiEvent('suggestion_confirmed');
|
||||
dispatch({
|
||||
type: 'SUBMIT_SUGGESTION',
|
||||
});
|
||||
dispatchLens(submitSuggestion());
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.lens.sugegstion.refreshSuggestionLabel', {
|
||||
|
@ -401,7 +400,7 @@ export function SuggestionPanel({
|
|||
rollbackToCurrentVisualization();
|
||||
} else {
|
||||
setLastSelectedSuggestion(index);
|
||||
switchToSuggestion(dispatch, suggestion);
|
||||
switchToSuggestion(dispatchLens, suggestion);
|
||||
}
|
||||
}}
|
||||
selected={index === lastSelectedSuggestion}
|
||||
|
|
|
@ -11,7 +11,8 @@ import {
|
|||
createMockVisualization,
|
||||
createMockFramePublicAPI,
|
||||
createMockDatasource,
|
||||
} from '../../mocks';
|
||||
} from '../../../mocks';
|
||||
import { mountWithProvider } from '../../../mocks';
|
||||
|
||||
// Tests are executed in a jsdom environment who does not have sizing methods,
|
||||
// thus the AutoSizer will always compute a 0x0 size space
|
||||
|
@ -25,9 +26,7 @@ jest.mock('react-virtualized-auto-sizer', () => {
|
|||
};
|
||||
});
|
||||
|
||||
import { mountWithIntl as mount } from '@kbn/test/jest';
|
||||
import { Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../../types';
|
||||
import { Action } from '../state_management';
|
||||
import { ChartSwitch } from './chart_switch';
|
||||
import { PaletteOutput } from 'src/plugins/charts/public';
|
||||
|
||||
|
@ -157,6 +156,8 @@ describe('chart_switch', () => {
|
|||
keptLayerIds: ['a'],
|
||||
},
|
||||
]);
|
||||
|
||||
datasource.getLayers.mockReturnValue(['a']);
|
||||
return {
|
||||
testDatasource: datasource,
|
||||
};
|
||||
|
@ -171,78 +172,94 @@ describe('chart_switch', () => {
|
|||
};
|
||||
}
|
||||
|
||||
function showFlyout(component: ReactWrapper) {
|
||||
component.find('[data-test-subj="lnsChartSwitchPopover"]').first().simulate('click');
|
||||
function showFlyout(instance: ReactWrapper) {
|
||||
instance.find('[data-test-subj="lnsChartSwitchPopover"]').first().simulate('click');
|
||||
}
|
||||
|
||||
function switchTo(subType: string, component: ReactWrapper) {
|
||||
showFlyout(component);
|
||||
component.find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`).first().simulate('click');
|
||||
function switchTo(subType: string, instance: ReactWrapper) {
|
||||
showFlyout(instance);
|
||||
instance.find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`).first().simulate('click');
|
||||
}
|
||||
|
||||
function getMenuItem(subType: string, component: ReactWrapper) {
|
||||
showFlyout(component);
|
||||
return component.find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`).first();
|
||||
function getMenuItem(subType: string, instance: ReactWrapper) {
|
||||
showFlyout(instance);
|
||||
return instance.find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`).first();
|
||||
}
|
||||
|
||||
it('should use suggested state if there is a suggestion from the target visualization', () => {
|
||||
const dispatch = jest.fn();
|
||||
it('should use suggested state if there is a suggestion from the target visualization', async () => {
|
||||
const visualizations = mockVisualizations();
|
||||
const component = mount(
|
||||
const { instance, lensStore } = await mountWithProvider(
|
||||
<ChartSwitch
|
||||
visualizationId="visA"
|
||||
visualizationState={'state from a'}
|
||||
visualizationMap={visualizations}
|
||||
dispatch={dispatch}
|
||||
framePublicAPI={mockFrame(['a'])}
|
||||
datasourceMap={mockDatasourceMap()}
|
||||
datasourceStates={mockDatasourceStates()}
|
||||
/>
|
||||
/>,
|
||||
{
|
||||
preloadedState: {
|
||||
visualization: {
|
||||
activeId: 'visA',
|
||||
state: 'state from a',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
switchTo('visB', component);
|
||||
switchTo('visB', instance);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
initialState: 'suggestion visB',
|
||||
newVisualizationId: 'visB',
|
||||
type: 'SWITCH_VISUALIZATION',
|
||||
datasourceId: 'testDatasource',
|
||||
datasourceState: {},
|
||||
expect(lensStore.dispatch).toHaveBeenCalledWith({
|
||||
type: 'lens/switchVisualization',
|
||||
payload: {
|
||||
initialState: 'suggestion visB',
|
||||
newVisualizationId: 'visB',
|
||||
datasourceId: 'testDatasource',
|
||||
datasourceState: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should use initial state if there is no suggestion from the target visualization', () => {
|
||||
const dispatch = jest.fn();
|
||||
it('should use initial state if there is no suggestion from the target visualization', async () => {
|
||||
const visualizations = mockVisualizations();
|
||||
visualizations.visB.getSuggestions.mockReturnValueOnce([]);
|
||||
const frame = mockFrame(['a']);
|
||||
(frame.datasourceLayers.a.getTableSpec as jest.Mock).mockReturnValue([]);
|
||||
|
||||
const component = mount(
|
||||
const datasourceMap = mockDatasourceMap();
|
||||
const datasourceStates = mockDatasourceStates();
|
||||
const { instance, lensStore } = await mountWithProvider(
|
||||
<ChartSwitch
|
||||
visualizationId="visA"
|
||||
visualizationState={{}}
|
||||
visualizationMap={visualizations}
|
||||
dispatch={dispatch}
|
||||
framePublicAPI={frame}
|
||||
datasourceMap={mockDatasourceMap()}
|
||||
datasourceStates={mockDatasourceStates()}
|
||||
/>
|
||||
datasourceMap={datasourceMap}
|
||||
/>,
|
||||
{
|
||||
preloadedState: {
|
||||
datasourceStates,
|
||||
activeDatasourceId: 'testDatasource',
|
||||
visualization: {
|
||||
activeId: 'visA',
|
||||
state: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
switchTo('visB', component);
|
||||
|
||||
expect(frame.removeLayers).toHaveBeenCalledWith(['a']);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
initialState: 'visB initial state',
|
||||
newVisualizationId: 'visB',
|
||||
type: 'SWITCH_VISUALIZATION',
|
||||
switchTo('visB', instance);
|
||||
expect(datasourceMap.testDatasource.removeLayer).toHaveBeenCalledWith({}, 'a'); // from preloaded state
|
||||
expect(lensStore.dispatch).toHaveBeenCalledWith({
|
||||
type: 'lens/switchVisualization',
|
||||
payload: {
|
||||
initialState: 'visB initial state',
|
||||
newVisualizationId: 'visB',
|
||||
},
|
||||
});
|
||||
expect(lensStore.dispatch).toHaveBeenCalledWith({
|
||||
type: 'lens/updateLayer',
|
||||
payload: expect.objectContaining({
|
||||
datasourceId: 'testDatasource',
|
||||
layerId: 'a',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('should indicate data loss if not all columns will be used', () => {
|
||||
const dispatch = jest.fn();
|
||||
it('should indicate data loss if not all columns will be used', async () => {
|
||||
const visualizations = mockVisualizations();
|
||||
const frame = mockFrame(['a']);
|
||||
|
||||
|
@ -282,53 +299,59 @@ describe('chart_switch', () => {
|
|||
{ columnId: 'col3' },
|
||||
]);
|
||||
|
||||
const component = mount(
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChartSwitch
|
||||
visualizationId="visA"
|
||||
visualizationState={{}}
|
||||
visualizationMap={visualizations}
|
||||
dispatch={dispatch}
|
||||
framePublicAPI={frame}
|
||||
datasourceMap={mockDatasourceMap()}
|
||||
datasourceStates={mockDatasourceStates()}
|
||||
/>
|
||||
/>,
|
||||
{
|
||||
preloadedState: {
|
||||
visualization: {
|
||||
activeId: 'visA',
|
||||
state: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(
|
||||
getMenuItem('visB', component)
|
||||
getMenuItem('visB', instance)
|
||||
.find('[data-test-subj="lnsChartSwitchPopoverAlert_visB"]')
|
||||
.first()
|
||||
.props().type
|
||||
).toEqual('alert');
|
||||
});
|
||||
|
||||
it('should indicate data loss if not all layers will be used', () => {
|
||||
const dispatch = jest.fn();
|
||||
it('should indicate data loss if not all layers will be used', async () => {
|
||||
const visualizations = mockVisualizations();
|
||||
const frame = mockFrame(['a', 'b']);
|
||||
|
||||
const component = mount(
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChartSwitch
|
||||
visualizationId="visA"
|
||||
visualizationState={{}}
|
||||
visualizationMap={visualizations}
|
||||
dispatch={dispatch}
|
||||
framePublicAPI={frame}
|
||||
datasourceMap={mockDatasourceMap()}
|
||||
datasourceStates={mockDatasourceStates()}
|
||||
/>
|
||||
/>,
|
||||
{
|
||||
preloadedState: {
|
||||
visualization: {
|
||||
activeId: 'visA',
|
||||
state: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(
|
||||
getMenuItem('visB', component)
|
||||
getMenuItem('visB', instance)
|
||||
.find('[data-test-subj="lnsChartSwitchPopoverAlert_visB"]')
|
||||
.first()
|
||||
.props().type
|
||||
).toEqual('alert');
|
||||
});
|
||||
|
||||
it('should support multi-layer suggestions without data loss', () => {
|
||||
const dispatch = jest.fn();
|
||||
it('should support multi-layer suggestions without data loss', async () => {
|
||||
const visualizations = mockVisualizations();
|
||||
const frame = mockFrame(['a', 'b']);
|
||||
|
||||
|
@ -355,75 +378,85 @@ describe('chart_switch', () => {
|
|||
},
|
||||
]);
|
||||
|
||||
const component = mount(
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChartSwitch
|
||||
visualizationId="visA"
|
||||
visualizationState={{}}
|
||||
visualizationMap={visualizations}
|
||||
dispatch={dispatch}
|
||||
framePublicAPI={frame}
|
||||
datasourceMap={datasourceMap}
|
||||
datasourceStates={mockDatasourceStates()}
|
||||
/>
|
||||
/>,
|
||||
{
|
||||
preloadedState: {
|
||||
visualization: {
|
||||
activeId: 'visA',
|
||||
state: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(
|
||||
getMenuItem('visB', component).find('[data-test-subj="lnsChartSwitchPopoverAlert_visB"]')
|
||||
getMenuItem('visB', instance).find('[data-test-subj="lnsChartSwitchPopoverAlert_visB"]')
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should indicate data loss if no data will be used', () => {
|
||||
const dispatch = jest.fn();
|
||||
it('should indicate data loss if no data will be used', async () => {
|
||||
const visualizations = mockVisualizations();
|
||||
visualizations.visB.getSuggestions.mockReturnValueOnce([]);
|
||||
const frame = mockFrame(['a']);
|
||||
|
||||
const component = mount(
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChartSwitch
|
||||
visualizationId="visA"
|
||||
visualizationState={{}}
|
||||
visualizationMap={visualizations}
|
||||
dispatch={dispatch}
|
||||
framePublicAPI={frame}
|
||||
datasourceMap={mockDatasourceMap()}
|
||||
datasourceStates={mockDatasourceStates()}
|
||||
/>
|
||||
/>,
|
||||
{
|
||||
preloadedState: {
|
||||
visualization: {
|
||||
activeId: 'visA',
|
||||
state: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(
|
||||
getMenuItem('visB', component)
|
||||
getMenuItem('visB', instance)
|
||||
.find('[data-test-subj="lnsChartSwitchPopoverAlert_visB"]')
|
||||
.first()
|
||||
.props().type
|
||||
).toEqual('alert');
|
||||
});
|
||||
|
||||
it('should not indicate data loss if there is no data', () => {
|
||||
const dispatch = jest.fn();
|
||||
it('should not indicate data loss if there is no data', async () => {
|
||||
const visualizations = mockVisualizations();
|
||||
visualizations.visB.getSuggestions.mockReturnValueOnce([]);
|
||||
const frame = mockFrame(['a']);
|
||||
(frame.datasourceLayers.a.getTableSpec as jest.Mock).mockReturnValue([]);
|
||||
|
||||
const component = mount(
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChartSwitch
|
||||
visualizationId="visA"
|
||||
visualizationState={{}}
|
||||
visualizationMap={visualizations}
|
||||
dispatch={dispatch}
|
||||
framePublicAPI={frame}
|
||||
datasourceMap={mockDatasourceMap()}
|
||||
datasourceStates={mockDatasourceStates()}
|
||||
/>
|
||||
/>,
|
||||
|
||||
{
|
||||
preloadedState: {
|
||||
visualization: {
|
||||
activeId: 'visA',
|
||||
state: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(
|
||||
getMenuItem('visB', component).find('[data-test-subj="lnsChartSwitchPopoverAlert_visB"]')
|
||||
getMenuItem('visB', instance).find('[data-test-subj="lnsChartSwitchPopoverAlert_visB"]')
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not show a warning when the subvisualization is the same', () => {
|
||||
const dispatch = jest.fn();
|
||||
it('should not show a warning when the subvisualization is the same', async () => {
|
||||
const frame = mockFrame(['a', 'b', 'c']);
|
||||
const visualizations = mockVisualizations();
|
||||
visualizations.visC.getVisualizationTypeId.mockReturnValue('subvisC2');
|
||||
|
@ -431,64 +464,81 @@ describe('chart_switch', () => {
|
|||
|
||||
visualizations.visC.switchVisualizationType = switchVisualizationType;
|
||||
|
||||
const component = mount(
|
||||
const datasourceMap = mockDatasourceMap();
|
||||
const datasourceStates = mockDatasourceStates();
|
||||
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChartSwitch
|
||||
visualizationId="visC"
|
||||
visualizationState={{ type: 'subvisC2' }}
|
||||
visualizationMap={visualizations}
|
||||
dispatch={dispatch}
|
||||
framePublicAPI={frame}
|
||||
datasourceMap={mockDatasourceMap()}
|
||||
datasourceStates={mockDatasourceStates()}
|
||||
/>
|
||||
datasourceMap={datasourceMap}
|
||||
/>,
|
||||
{
|
||||
preloadedState: {
|
||||
datasourceStates,
|
||||
activeDatasourceId: 'testDatasource',
|
||||
visualization: {
|
||||
activeId: 'visC',
|
||||
state: { type: 'subvisC2' },
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(
|
||||
getMenuItem('subvisC2', component).find(
|
||||
getMenuItem('subvisC2', instance).find(
|
||||
'[data-test-subj="lnsChartSwitchPopoverAlert_subvisC2"]'
|
||||
)
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should get suggestions when switching subvisualization', () => {
|
||||
const dispatch = jest.fn();
|
||||
it('should get suggestions when switching subvisualization', async () => {
|
||||
const visualizations = mockVisualizations();
|
||||
visualizations.visB.getSuggestions.mockReturnValueOnce([]);
|
||||
const frame = mockFrame(['a', 'b', 'c']);
|
||||
const datasourceMap = mockDatasourceMap();
|
||||
datasourceMap.testDatasource.getLayers.mockReturnValue(['a', 'b', 'c']);
|
||||
const datasourceStates = mockDatasourceStates();
|
||||
|
||||
const component = mount(
|
||||
const { instance, lensStore } = await mountWithProvider(
|
||||
<ChartSwitch
|
||||
visualizationId="visA"
|
||||
visualizationState={{}}
|
||||
visualizationMap={visualizations}
|
||||
dispatch={dispatch}
|
||||
framePublicAPI={frame}
|
||||
datasourceMap={mockDatasourceMap()}
|
||||
datasourceStates={mockDatasourceStates()}
|
||||
/>
|
||||
datasourceMap={datasourceMap}
|
||||
/>,
|
||||
{
|
||||
preloadedState: {
|
||||
datasourceStates,
|
||||
visualization: {
|
||||
activeId: 'visA',
|
||||
state: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
switchTo('visB', component);
|
||||
|
||||
expect(frame.removeLayers).toHaveBeenCalledTimes(1);
|
||||
expect(frame.removeLayers).toHaveBeenCalledWith(['a', 'b', 'c']);
|
||||
|
||||
switchTo('visB', instance);
|
||||
expect(datasourceMap.testDatasource.removeLayer).toHaveBeenCalledWith({}, 'a');
|
||||
expect(datasourceMap.testDatasource.removeLayer).toHaveBeenCalledWith(undefined, 'b');
|
||||
expect(datasourceMap.testDatasource.removeLayer).toHaveBeenCalledWith(undefined, 'c');
|
||||
expect(visualizations.visB.getSuggestions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
keptLayerIds: ['a'],
|
||||
})
|
||||
);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'SWITCH_VISUALIZATION',
|
||||
expect(lensStore.dispatch).toHaveBeenCalledWith({
|
||||
type: 'lens/switchVisualization',
|
||||
payload: {
|
||||
datasourceId: undefined,
|
||||
datasourceState: undefined,
|
||||
initialState: 'visB initial state',
|
||||
})
|
||||
);
|
||||
newVisualizationId: 'visB',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should query main palette from active chart and pass into suggestions', () => {
|
||||
const dispatch = jest.fn();
|
||||
it('should query main palette from active chart and pass into suggestions', async () => {
|
||||
const visualizations = mockVisualizations();
|
||||
const mockPalette: PaletteOutput = { type: 'palette', name: 'mock' };
|
||||
visualizations.visA.getMainPalette = jest.fn(() => mockPalette);
|
||||
|
@ -496,19 +546,26 @@ describe('chart_switch', () => {
|
|||
const frame = mockFrame(['a', 'b', 'c']);
|
||||
const currentVisState = {};
|
||||
|
||||
const component = mount(
|
||||
const datasourceMap = mockDatasourceMap();
|
||||
datasourceMap.testDatasource.getLayers.mockReturnValue(['a', 'b', 'c']);
|
||||
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChartSwitch
|
||||
visualizationId="visA"
|
||||
visualizationState={currentVisState}
|
||||
visualizationMap={visualizations}
|
||||
dispatch={dispatch}
|
||||
framePublicAPI={frame}
|
||||
datasourceMap={mockDatasourceMap()}
|
||||
datasourceStates={mockDatasourceStates()}
|
||||
/>
|
||||
datasourceMap={datasourceMap}
|
||||
/>,
|
||||
{
|
||||
preloadedState: {
|
||||
visualization: {
|
||||
activeId: 'visA',
|
||||
state: currentVisState,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
switchTo('visB', component);
|
||||
switchTo('visB', instance);
|
||||
|
||||
expect(visualizations.visA.getMainPalette).toHaveBeenCalledWith(currentVisState);
|
||||
|
||||
|
@ -520,67 +577,76 @@ describe('chart_switch', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should not remove layers when switching between subtypes', () => {
|
||||
const dispatch = jest.fn();
|
||||
it('should not remove layers when switching between subtypes', async () => {
|
||||
const frame = mockFrame(['a', 'b', 'c']);
|
||||
const visualizations = mockVisualizations();
|
||||
const switchVisualizationType = jest.fn(() => 'switched');
|
||||
|
||||
visualizations.visC.switchVisualizationType = switchVisualizationType;
|
||||
|
||||
const component = mount(
|
||||
const datasourceMap = mockDatasourceMap();
|
||||
const { instance, lensStore } = await mountWithProvider(
|
||||
<ChartSwitch
|
||||
visualizationId="visC"
|
||||
visualizationState={{ type: 'subvisC1' }}
|
||||
visualizationMap={visualizations}
|
||||
dispatch={dispatch}
|
||||
framePublicAPI={frame}
|
||||
datasourceMap={mockDatasourceMap()}
|
||||
datasourceStates={mockDatasourceStates()}
|
||||
/>
|
||||
datasourceMap={datasourceMap}
|
||||
/>,
|
||||
{
|
||||
preloadedState: {
|
||||
visualization: {
|
||||
activeId: 'visC',
|
||||
state: { type: 'subvisC1' },
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
switchTo('subvisC3', component);
|
||||
switchTo('subvisC3', instance);
|
||||
expect(switchVisualizationType).toHaveBeenCalledWith('subvisC3', { type: 'subvisC3' });
|
||||
expect(dispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'SWITCH_VISUALIZATION',
|
||||
|
||||
expect(lensStore.dispatch).toHaveBeenCalledWith({
|
||||
type: 'lens/switchVisualization',
|
||||
payload: {
|
||||
datasourceId: 'testDatasource',
|
||||
datasourceState: {},
|
||||
initialState: 'switched',
|
||||
})
|
||||
);
|
||||
expect(frame.removeLayers).not.toHaveBeenCalled();
|
||||
newVisualizationId: 'visC',
|
||||
},
|
||||
});
|
||||
expect(datasourceMap.testDatasource.removeLayer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not remove layers and initialize with existing state when switching between subtypes without data', () => {
|
||||
const dispatch = jest.fn();
|
||||
it('should not remove layers and initialize with existing state when switching between subtypes without data', async () => {
|
||||
const frame = mockFrame(['a']);
|
||||
frame.datasourceLayers.a.getTableSpec = jest.fn().mockReturnValue([]);
|
||||
const visualizations = mockVisualizations();
|
||||
visualizations.visC.getSuggestions = jest.fn().mockReturnValue([]);
|
||||
visualizations.visC.switchVisualizationType = jest.fn(() => 'switched');
|
||||
|
||||
const component = mount(
|
||||
const datasourceMap = mockDatasourceMap();
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChartSwitch
|
||||
visualizationId="visC"
|
||||
visualizationState={{ type: 'subvisC1' }}
|
||||
visualizationMap={visualizations}
|
||||
dispatch={dispatch}
|
||||
framePublicAPI={frame}
|
||||
datasourceMap={mockDatasourceMap()}
|
||||
datasourceStates={mockDatasourceStates()}
|
||||
/>
|
||||
datasourceMap={datasourceMap}
|
||||
/>,
|
||||
{
|
||||
preloadedState: {
|
||||
visualization: {
|
||||
activeId: 'visC',
|
||||
state: { type: 'subvisC1' },
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
switchTo('subvisC3', component);
|
||||
switchTo('subvisC3', instance);
|
||||
|
||||
expect(visualizations.visC.switchVisualizationType).toHaveBeenCalledWith('subvisC3', {
|
||||
type: 'subvisC1',
|
||||
});
|
||||
expect(frame.removeLayers).not.toHaveBeenCalled();
|
||||
expect(datasourceMap.testDatasource.removeLayer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should switch to the updated datasource state', () => {
|
||||
const dispatch = jest.fn();
|
||||
it('should switch to the updated datasource state', async () => {
|
||||
const visualizations = mockVisualizations();
|
||||
const frame = mockFrame(['a', 'b']);
|
||||
|
||||
|
@ -615,31 +681,36 @@ describe('chart_switch', () => {
|
|||
},
|
||||
]);
|
||||
|
||||
const component = mount(
|
||||
const { instance, lensStore } = await mountWithProvider(
|
||||
<ChartSwitch
|
||||
visualizationId="visA"
|
||||
visualizationState={{}}
|
||||
visualizationMap={visualizations}
|
||||
dispatch={dispatch}
|
||||
framePublicAPI={frame}
|
||||
datasourceMap={datasourceMap}
|
||||
datasourceStates={mockDatasourceStates()}
|
||||
/>
|
||||
/>,
|
||||
{
|
||||
preloadedState: {
|
||||
visualization: {
|
||||
activeId: 'visA',
|
||||
state: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
switchTo('visB', component);
|
||||
switchTo('visB', instance);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'SWITCH_VISUALIZATION',
|
||||
newVisualizationId: 'visB',
|
||||
datasourceId: 'testDatasource',
|
||||
datasourceState: 'testDatasource suggestion',
|
||||
initialState: 'suggestion visB',
|
||||
} as Action);
|
||||
expect(lensStore.dispatch).toHaveBeenCalledWith({
|
||||
type: 'lens/switchVisualization',
|
||||
payload: {
|
||||
newVisualizationId: 'visB',
|
||||
datasourceId: 'testDatasource',
|
||||
datasourceState: 'testDatasource suggestion',
|
||||
initialState: 'suggestion visB',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should ensure the new visualization has the proper subtype', () => {
|
||||
const dispatch = jest.fn();
|
||||
it('should ensure the new visualization has the proper subtype', async () => {
|
||||
const visualizations = mockVisualizations();
|
||||
const switchVisualizationType = jest.fn(
|
||||
(visualizationType, state) => `${state} ${visualizationType}`
|
||||
|
@ -647,72 +718,85 @@ describe('chart_switch', () => {
|
|||
|
||||
visualizations.visB.switchVisualizationType = switchVisualizationType;
|
||||
|
||||
const component = mount(
|
||||
const { instance, lensStore } = await mountWithProvider(
|
||||
<ChartSwitch
|
||||
visualizationId="visA"
|
||||
visualizationState={{}}
|
||||
visualizationMap={visualizations}
|
||||
dispatch={dispatch}
|
||||
framePublicAPI={mockFrame(['a'])}
|
||||
datasourceMap={mockDatasourceMap()}
|
||||
datasourceStates={mockDatasourceStates()}
|
||||
/>
|
||||
/>,
|
||||
{
|
||||
preloadedState: {
|
||||
visualization: {
|
||||
activeId: 'visA',
|
||||
state: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
switchTo('visB', component);
|
||||
switchTo('visB', instance);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
initialState: 'suggestion visB visB',
|
||||
newVisualizationId: 'visB',
|
||||
type: 'SWITCH_VISUALIZATION',
|
||||
datasourceId: 'testDatasource',
|
||||
datasourceState: {},
|
||||
expect(lensStore.dispatch).toHaveBeenCalledWith({
|
||||
type: 'lens/switchVisualization',
|
||||
payload: {
|
||||
initialState: 'suggestion visB visB',
|
||||
newVisualizationId: 'visB',
|
||||
datasourceId: 'testDatasource',
|
||||
datasourceState: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should use the suggestion that matches the subtype', () => {
|
||||
const dispatch = jest.fn();
|
||||
it('should use the suggestion that matches the subtype', async () => {
|
||||
const visualizations = mockVisualizations();
|
||||
const switchVisualizationType = jest.fn();
|
||||
|
||||
visualizations.visC.switchVisualizationType = switchVisualizationType;
|
||||
|
||||
const component = mount(
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChartSwitch
|
||||
visualizationId="visC"
|
||||
visualizationState={{ type: 'subvisC3' }}
|
||||
visualizationMap={visualizations}
|
||||
dispatch={dispatch}
|
||||
framePublicAPI={mockFrame(['a'])}
|
||||
datasourceMap={mockDatasourceMap()}
|
||||
datasourceStates={mockDatasourceStates()}
|
||||
/>
|
||||
/>,
|
||||
{
|
||||
preloadedState: {
|
||||
visualization: {
|
||||
activeId: 'visC',
|
||||
state: { type: 'subvisC3' },
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
switchTo('subvisC1', component);
|
||||
switchTo('subvisC1', instance);
|
||||
expect(switchVisualizationType).toHaveBeenCalledWith('subvisC1', {
|
||||
type: 'subvisC1',
|
||||
notPrimary: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should show all visualization types', () => {
|
||||
const component = mount(
|
||||
it('should show all visualization types', async () => {
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChartSwitch
|
||||
visualizationId="visA"
|
||||
visualizationState={{}}
|
||||
visualizationMap={mockVisualizations()}
|
||||
dispatch={jest.fn()}
|
||||
framePublicAPI={mockFrame(['a', 'b'])}
|
||||
datasourceMap={mockDatasourceMap()}
|
||||
datasourceStates={mockDatasourceStates()}
|
||||
/>
|
||||
/>,
|
||||
{
|
||||
preloadedState: {
|
||||
visualization: {
|
||||
activeId: 'visA',
|
||||
state: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
showFlyout(component);
|
||||
showFlyout(instance);
|
||||
|
||||
const allDisplayed = ['visA', 'visB', 'subvisC1', 'subvisC2', 'subvisC3'].every(
|
||||
(subType) => component.find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`).length > 0
|
||||
(subType) => instance.find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`).length > 0
|
||||
);
|
||||
|
||||
expect(allDisplayed).toBeTruthy();
|
||||
|
|
|
@ -21,10 +21,16 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { Visualization, FramePublicAPI, Datasource, VisualizationType } from '../../../types';
|
||||
import { Action } from '../state_management';
|
||||
import { getSuggestions, switchToSuggestion, Suggestion } from '../suggestion_helpers';
|
||||
import { trackUiEvent } from '../../../lens_ui_telemetry';
|
||||
import { ToolbarButton } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import {
|
||||
updateLayer,
|
||||
updateVisualizationState,
|
||||
useLensDispatch,
|
||||
useLensSelector,
|
||||
} from '../../../state_management';
|
||||
import { generateId } from '../../../id_generator/id_generator';
|
||||
|
||||
interface VisualizationSelection {
|
||||
visualizationId: string;
|
||||
|
@ -38,27 +44,26 @@ interface VisualizationSelection {
|
|||
}
|
||||
|
||||
interface Props {
|
||||
dispatch: (action: Action) => void;
|
||||
visualizationMap: Record<string, Visualization>;
|
||||
visualizationId: string | null;
|
||||
visualizationState: unknown;
|
||||
framePublicAPI: FramePublicAPI;
|
||||
datasourceMap: Record<string, Datasource>;
|
||||
datasourceStates: Record<
|
||||
string,
|
||||
{
|
||||
isLoading: boolean;
|
||||
state: unknown;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
type SelectableEntry = EuiSelectableOption<{ value: string }>;
|
||||
|
||||
function VisualizationSummary(props: Props) {
|
||||
const visualization = props.visualizationMap[props.visualizationId || ''];
|
||||
function VisualizationSummary({
|
||||
visualizationMap,
|
||||
visualization,
|
||||
}: {
|
||||
visualizationMap: Record<string, Visualization>;
|
||||
visualization: {
|
||||
activeId: string | null;
|
||||
state: unknown;
|
||||
};
|
||||
}) {
|
||||
const activeVisualization = visualizationMap[visualization.activeId || ''];
|
||||
|
||||
if (!visualization) {
|
||||
if (!activeVisualization) {
|
||||
return (
|
||||
<>
|
||||
{i18n.translate('xpack.lens.configPanel.selectVisualization', {
|
||||
|
@ -68,7 +73,7 @@ function VisualizationSummary(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
const description = visualization.getDescription(props.visualizationState);
|
||||
const description = activeVisualization.getDescription(visualization.state);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -99,6 +104,44 @@ 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);
|
||||
|
||||
function removeLayers(layerIds: string[]) {
|
||||
const activeVisualization =
|
||||
visualization.activeId && props.visualizationMap[visualization.activeId];
|
||||
if (activeVisualization && activeVisualization.removeLayer && visualization.state) {
|
||||
dispatchLens(
|
||||
updateVisualizationState({
|
||||
visualizationId: activeVisualization.id,
|
||||
updater: layerIds.reduce(
|
||||
(acc, layerId) =>
|
||||
activeVisualization.removeLayer ? activeVisualization.removeLayer(acc, layerId) : acc,
|
||||
visualization.state
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
layerIds.forEach((layerId) => {
|
||||
const layerDatasourceId = Object.entries(props.datasourceMap).find(
|
||||
([datasourceId, datasource]) => {
|
||||
return (
|
||||
datasourceStates[datasourceId] &&
|
||||
datasource.getLayers(datasourceStates[datasourceId].state).includes(layerId)
|
||||
);
|
||||
}
|
||||
)![0];
|
||||
dispatchLens(
|
||||
updateLayer({
|
||||
layerId,
|
||||
datasourceId: layerDatasourceId,
|
||||
updater: props.datasourceMap[layerDatasourceId].removeLayer,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const commitSelection = (selection: VisualizationSelection) => {
|
||||
setFlyoutOpen(false);
|
||||
|
@ -106,7 +149,7 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
|
|||
trackUiEvent(`chart_switch`);
|
||||
|
||||
switchToSuggestion(
|
||||
props.dispatch,
|
||||
dispatchLens,
|
||||
{
|
||||
...selection,
|
||||
visualizationState: selection.getVisualizationState(),
|
||||
|
@ -118,7 +161,7 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
|
|||
(!selection.datasourceId && !selection.sameDatasources) ||
|
||||
selection.dataLoss === 'everything'
|
||||
) {
|
||||
props.framePublicAPI.removeLayers(Object.keys(props.framePublicAPI.datasourceLayers));
|
||||
removeLayers(Object.keys(props.framePublicAPI.datasourceLayers));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -136,16 +179,16 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
|
|||
);
|
||||
// Always show the active visualization as a valid selection
|
||||
if (
|
||||
props.visualizationId === visualizationId &&
|
||||
props.visualizationState &&
|
||||
newVisualization.getVisualizationTypeId(props.visualizationState) === subVisualizationId
|
||||
visualization.activeId === visualizationId &&
|
||||
visualization.state &&
|
||||
newVisualization.getVisualizationTypeId(visualization.state) === subVisualizationId
|
||||
) {
|
||||
return {
|
||||
visualizationId,
|
||||
subVisualizationId,
|
||||
dataLoss: 'nothing',
|
||||
keptLayerIds: Object.keys(props.framePublicAPI.datasourceLayers),
|
||||
getVisualizationState: () => switchVisType(subVisualizationId, props.visualizationState),
|
||||
getVisualizationState: () => switchVisType(subVisualizationId, visualization.state),
|
||||
sameDatasources: true,
|
||||
};
|
||||
}
|
||||
|
@ -153,6 +196,8 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
|
|||
const topSuggestion = getTopSuggestion(
|
||||
props,
|
||||
visualizationId,
|
||||
datasourceStates,
|
||||
visualization,
|
||||
newVisualization,
|
||||
subVisualizationId
|
||||
);
|
||||
|
@ -171,6 +216,19 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
|
|||
dataLoss = 'nothing';
|
||||
}
|
||||
|
||||
function addNewLayer() {
|
||||
const newLayerId = generateId();
|
||||
dispatchLens(
|
||||
updateLayer({
|
||||
datasourceId: activeDatasourceId!,
|
||||
layerId: newLayerId,
|
||||
updater: props.datasourceMap[activeDatasourceId!].insertLayer,
|
||||
})
|
||||
);
|
||||
|
||||
return newLayerId;
|
||||
}
|
||||
|
||||
return {
|
||||
visualizationId,
|
||||
subVisualizationId,
|
||||
|
@ -179,29 +237,26 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
|
|||
? () =>
|
||||
switchVisType(
|
||||
subVisualizationId,
|
||||
newVisualization.initialize(props.framePublicAPI, topSuggestion.visualizationState)
|
||||
newVisualization.initialize(addNewLayer, topSuggestion.visualizationState)
|
||||
)
|
||||
: () => {
|
||||
return switchVisType(
|
||||
: () =>
|
||||
switchVisType(
|
||||
subVisualizationId,
|
||||
newVisualization.initialize(
|
||||
props.framePublicAPI,
|
||||
props.visualizationId === newVisualization.id
|
||||
? props.visualizationState
|
||||
: undefined,
|
||||
props.visualizationId &&
|
||||
props.visualizationMap[props.visualizationId].getMainPalette
|
||||
? props.visualizationMap[props.visualizationId].getMainPalette!(
|
||||
props.visualizationState
|
||||
addNewLayer,
|
||||
visualization.activeId === newVisualization.id ? visualization.state : undefined,
|
||||
visualization.activeId &&
|
||||
props.visualizationMap[visualization.activeId].getMainPalette
|
||||
? props.visualizationMap[visualization.activeId].getMainPalette!(
|
||||
visualization.state
|
||||
)
|
||||
: undefined
|
||||
)
|
||||
);
|
||||
},
|
||||
),
|
||||
keptLayerIds: topSuggestion ? topSuggestion.keptLayerIds : [],
|
||||
datasourceState: topSuggestion ? topSuggestion.datasourceState : undefined,
|
||||
datasourceId: topSuggestion ? topSuggestion.datasourceId : undefined,
|
||||
sameDatasources: dataLoss === 'nothing' && props.visualizationId === newVisualization.id,
|
||||
sameDatasources: dataLoss === 'nothing' && visualization.activeId === newVisualization.id,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -213,8 +268,8 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
|
|||
return { visualizationTypes: [], visualizationsLookup: {} };
|
||||
}
|
||||
const subVisualizationId = getCurrentVisualizationId(
|
||||
props.visualizationMap[props.visualizationId || ''],
|
||||
props.visualizationState
|
||||
props.visualizationMap[visualization.activeId || ''],
|
||||
visualization.state
|
||||
);
|
||||
const lowercasedSearchTerm = searchTerm.toLowerCase();
|
||||
// reorganize visualizations in groups
|
||||
|
@ -351,8 +406,8 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
|
|||
flyoutOpen,
|
||||
props.visualizationMap,
|
||||
props.framePublicAPI,
|
||||
props.visualizationId,
|
||||
props.visualizationState,
|
||||
visualization.activeId,
|
||||
visualization.state,
|
||||
searchTerm,
|
||||
]
|
||||
);
|
||||
|
@ -371,7 +426,10 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
|
|||
data-test-subj="lnsChartSwitchPopover"
|
||||
fontWeight="bold"
|
||||
>
|
||||
<VisualizationSummary {...props} />
|
||||
<VisualizationSummary
|
||||
visualization={visualization}
|
||||
visualizationMap={props.visualizationMap}
|
||||
/>
|
||||
</ToolbarButton>
|
||||
}
|
||||
isOpen={flyoutOpen}
|
||||
|
@ -402,7 +460,7 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
|
|||
}}
|
||||
options={visualizationTypes}
|
||||
onChange={(newOptions) => {
|
||||
const chosenType = newOptions.find(({ checked }) => checked === 'on')!;
|
||||
const chosenType = newOptions.find(({ checked }) => checked === 'on');
|
||||
if (!chosenType) {
|
||||
return;
|
||||
}
|
||||
|
@ -434,21 +492,26 @@ 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;
|
||||
},
|
||||
newVisualization: Visualization<unknown>,
|
||||
subVisualizationId?: string
|
||||
): Suggestion | undefined {
|
||||
const mainPalette =
|
||||
props.visualizationId &&
|
||||
props.visualizationMap[props.visualizationId] &&
|
||||
props.visualizationMap[props.visualizationId].getMainPalette
|
||||
? props.visualizationMap[props.visualizationId].getMainPalette!(props.visualizationState)
|
||||
visualization.activeId &&
|
||||
props.visualizationMap[visualization.activeId] &&
|
||||
props.visualizationMap[visualization.activeId].getMainPalette
|
||||
? props.visualizationMap[visualization.activeId].getMainPalette!(visualization.state)
|
||||
: undefined;
|
||||
const unfilteredSuggestions = getSuggestions({
|
||||
datasourceMap: props.datasourceMap,
|
||||
datasourceStates: props.datasourceStates,
|
||||
datasourceStates,
|
||||
visualizationMap: { [visualizationId]: newVisualization },
|
||||
activeVisualizationId: props.visualizationId,
|
||||
visualizationState: props.visualizationState,
|
||||
activeVisualizationId: visualization.activeId,
|
||||
visualizationState: visualization.state,
|
||||
subVisualizationId,
|
||||
activeData: props.framePublicAPI.activeData,
|
||||
mainPalette,
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 './workspace_panel_wrapper.scss';
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiScreenReaderOnly } from '@elastic/eui';
|
||||
import { LensState, useLensSelector } from '../../../state_management';
|
||||
|
||||
export function WorkspaceTitle() {
|
||||
const title = useLensSelector((state: LensState) => state.lens.persistedDoc?.title);
|
||||
return (
|
||||
<EuiScreenReaderOnly>
|
||||
<h1 id="lns_ChartTitle" data-test-subj="lns_ChartTitle">
|
||||
{title ||
|
||||
i18n.translate('xpack.lens.chartTitle.unsaved', {
|
||||
defaultMessage: 'Unsaved visualization',
|
||||
})}
|
||||
</h1>
|
||||
</EuiScreenReaderOnly>
|
||||
);
|
||||
}
|
|
@ -15,7 +15,7 @@ import {
|
|||
createExpressionRendererMock,
|
||||
DatasourceMock,
|
||||
createMockFramePublicAPI,
|
||||
} from '../../mocks';
|
||||
} from '../../../mocks';
|
||||
import { mockDataPlugin, mountWithProvider } from '../../../mocks';
|
||||
jest.mock('../../../debounced_component', () => {
|
||||
return {
|
||||
|
@ -24,7 +24,6 @@ jest.mock('../../../debounced_component', () => {
|
|||
});
|
||||
|
||||
import { WorkspacePanel } from './workspace_panel';
|
||||
import { mountWithIntl as mount } from '@kbn/test/jest';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { DragDrop, ChildDragDropProvider } from '../../../drag_drop';
|
||||
import { fromExpression } from '@kbn/interpreter/common';
|
||||
|
@ -56,7 +55,6 @@ const defaultProps = {
|
|||
framePublicAPI: createMockFramePublicAPI(),
|
||||
activeVisualizationId: 'vis',
|
||||
visualizationState: {},
|
||||
dispatch: () => {},
|
||||
ExpressionRenderer: createExpressionRendererMock(),
|
||||
core: createCoreStartWithPermissions(),
|
||||
plugins: {
|
||||
|
@ -104,7 +102,8 @@ describe('workspace_panel', () => {
|
|||
}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
/>,
|
||||
defaultProps.plugins.data
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
);
|
||||
instance = mounted.instance;
|
||||
expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2);
|
||||
|
@ -119,7 +118,8 @@ describe('workspace_panel', () => {
|
|||
vis: { ...mockVisualization, toExpression: () => null },
|
||||
}}
|
||||
/>,
|
||||
defaultProps.plugins.data
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
);
|
||||
instance = mounted.instance;
|
||||
|
||||
|
@ -135,7 +135,8 @@ describe('workspace_panel', () => {
|
|||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
}}
|
||||
/>,
|
||||
defaultProps.plugins.data
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
);
|
||||
instance = mounted.instance;
|
||||
|
||||
|
@ -169,7 +170,7 @@ describe('workspace_panel', () => {
|
|||
}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
/>,
|
||||
defaultProps.plugins.data
|
||||
{ data: defaultProps.plugins.data }
|
||||
);
|
||||
|
||||
instance = mounted.instance;
|
||||
|
@ -209,7 +210,8 @@ describe('workspace_panel', () => {
|
|||
ExpressionRenderer={expressionRendererMock}
|
||||
plugins={{ ...props.plugins, uiActions: uiActionsMock }}
|
||||
/>,
|
||||
defaultProps.plugins.data
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
);
|
||||
instance = mounted.instance;
|
||||
|
||||
|
@ -229,7 +231,6 @@ describe('workspace_panel', () => {
|
|||
};
|
||||
mockDatasource.toExpression.mockReturnValue('datasource');
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
const dispatch = jest.fn();
|
||||
|
||||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
|
@ -247,10 +248,10 @@ describe('workspace_panel', () => {
|
|||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
}}
|
||||
dispatch={dispatch}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
/>,
|
||||
defaultProps.plugins.data
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
);
|
||||
|
||||
instance = mounted.instance;
|
||||
|
@ -261,8 +262,8 @@ describe('workspace_panel', () => {
|
|||
onData(undefined, { tables: { tables: tableData } });
|
||||
|
||||
expect(mounted.lensStore.dispatch).toHaveBeenCalledWith({
|
||||
type: 'app/onActiveDataChange',
|
||||
payload: { activeData: tableData },
|
||||
type: 'lens/onActiveDataChange',
|
||||
payload: tableData,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -302,7 +303,8 @@ describe('workspace_panel', () => {
|
|||
}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
/>,
|
||||
defaultProps.plugins.data
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
);
|
||||
instance = mounted.instance;
|
||||
|
||||
|
@ -377,7 +379,8 @@ describe('workspace_panel', () => {
|
|||
}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
/>,
|
||||
defaultProps.plugins.data
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
);
|
||||
instance = mounted.instance;
|
||||
});
|
||||
|
@ -430,7 +433,8 @@ describe('workspace_panel', () => {
|
|||
}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
/>,
|
||||
defaultProps.plugins.data
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
);
|
||||
instance = mounted.instance;
|
||||
});
|
||||
|
@ -481,7 +485,8 @@ describe('workspace_panel', () => {
|
|||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
}}
|
||||
/>,
|
||||
defaultProps.plugins.data
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
);
|
||||
instance = mounted.instance;
|
||||
|
||||
|
@ -520,7 +525,8 @@ describe('workspace_panel', () => {
|
|||
management: { kibana: { indexPatterns: true } },
|
||||
})}
|
||||
/>,
|
||||
defaultProps.plugins.data
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
);
|
||||
instance = mounted.instance;
|
||||
|
||||
|
@ -559,7 +565,8 @@ describe('workspace_panel', () => {
|
|||
management: { kibana: { indexPatterns: false } },
|
||||
})}
|
||||
/>,
|
||||
defaultProps.plugins.data
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
);
|
||||
instance = mounted.instance;
|
||||
|
||||
|
@ -595,7 +602,8 @@ describe('workspace_panel', () => {
|
|||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
}}
|
||||
/>,
|
||||
defaultProps.plugins.data
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
);
|
||||
instance = mounted.instance;
|
||||
|
||||
|
@ -632,7 +640,8 @@ describe('workspace_panel', () => {
|
|||
vis: mockVisualization,
|
||||
}}
|
||||
/>,
|
||||
defaultProps.plugins.data
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
);
|
||||
instance = mounted.instance;
|
||||
|
||||
|
@ -671,7 +680,8 @@ describe('workspace_panel', () => {
|
|||
vis: mockVisualization,
|
||||
}}
|
||||
/>,
|
||||
defaultProps.plugins.data
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
);
|
||||
instance = mounted.instance;
|
||||
|
||||
|
@ -707,7 +717,8 @@ describe('workspace_panel', () => {
|
|||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
}}
|
||||
/>,
|
||||
defaultProps.plugins.data
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
);
|
||||
instance = mounted.instance;
|
||||
|
||||
|
@ -742,7 +753,8 @@ describe('workspace_panel', () => {
|
|||
}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
/>,
|
||||
defaultProps.plugins.data
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
);
|
||||
instance = mounted.instance;
|
||||
});
|
||||
|
@ -783,7 +795,8 @@ describe('workspace_panel', () => {
|
|||
}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
/>,
|
||||
defaultProps.plugins.data
|
||||
|
||||
{ data: defaultProps.plugins.data }
|
||||
);
|
||||
instance = mounted.instance;
|
||||
});
|
||||
|
@ -805,7 +818,6 @@ describe('workspace_panel', () => {
|
|||
});
|
||||
|
||||
describe('suggestions from dropping in workspace panel', () => {
|
||||
let mockDispatch: jest.Mock;
|
||||
let mockGetSuggestionForField: jest.Mock;
|
||||
let frame: jest.Mocked<FramePublicAPI>;
|
||||
|
||||
|
@ -813,12 +825,11 @@ describe('workspace_panel', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
frame = createMockFramePublicAPI();
|
||||
mockDispatch = jest.fn();
|
||||
mockGetSuggestionForField = jest.fn();
|
||||
});
|
||||
|
||||
function initComponent(draggingContext = draggedField) {
|
||||
instance = mount(
|
||||
async function initComponent(draggingContext = draggedField) {
|
||||
const mounted = await mountWithProvider(
|
||||
<ChildDragDropProvider
|
||||
dragging={draggingContext}
|
||||
setDragging={() => {}}
|
||||
|
@ -846,11 +857,12 @@ describe('workspace_panel', () => {
|
|||
vis: mockVisualization,
|
||||
vis2: mockVisualization2,
|
||||
}}
|
||||
dispatch={mockDispatch}
|
||||
getSuggestionForField={mockGetSuggestionForField}
|
||||
/>
|
||||
</ChildDragDropProvider>
|
||||
);
|
||||
instance = mounted.instance;
|
||||
return mounted;
|
||||
}
|
||||
|
||||
it('should immediately transition if exactly one suggestion is returned', async () => {
|
||||
|
@ -860,32 +872,34 @@ describe('workspace_panel', () => {
|
|||
datasourceId: 'mock',
|
||||
visualizationState: {},
|
||||
});
|
||||
initComponent();
|
||||
const { lensStore } = await initComponent();
|
||||
|
||||
instance.find(DragDrop).prop('onDrop')!(draggedField, 'field_replace');
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
type: 'SWITCH_VISUALIZATION',
|
||||
newVisualizationId: 'vis',
|
||||
initialState: {},
|
||||
datasourceState: {},
|
||||
datasourceId: 'mock',
|
||||
expect(lensStore.dispatch).toHaveBeenCalledWith({
|
||||
type: 'lens/switchVisualization',
|
||||
payload: {
|
||||
newVisualizationId: 'vis',
|
||||
initialState: {},
|
||||
datasourceState: {},
|
||||
datasourceId: 'mock',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow to drop if there are suggestions', () => {
|
||||
it('should allow to drop if there are suggestions', async () => {
|
||||
mockGetSuggestionForField.mockReturnValue({
|
||||
visualizationId: 'vis',
|
||||
datasourceState: {},
|
||||
datasourceId: 'mock',
|
||||
visualizationState: {},
|
||||
});
|
||||
initComponent();
|
||||
await initComponent();
|
||||
expect(instance.find(DragDrop).prop('dropTypes')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should refuse to drop if there are no suggestions', () => {
|
||||
initComponent();
|
||||
it('should refuse to drop if there are no suggestions', async () => {
|
||||
await initComponent();
|
||||
expect(instance.find(DragDrop).prop('dropType')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -33,7 +33,6 @@ import {
|
|||
ExpressionRenderError,
|
||||
ReactExpressionRendererType,
|
||||
} from '../../../../../../../src/plugins/expressions/public';
|
||||
import { Action } from '../state_management';
|
||||
import {
|
||||
Datasource,
|
||||
Visualization,
|
||||
|
@ -46,17 +45,20 @@ import { DragDrop, DragContext, DragDropIdentifier } from '../../../drag_drop';
|
|||
import { Suggestion, switchToSuggestion } from '../suggestion_helpers';
|
||||
import { buildExpression } from '../expression_helpers';
|
||||
import { trackUiEvent } from '../../../lens_ui_telemetry';
|
||||
import {
|
||||
UiActionsStart,
|
||||
VisualizeFieldContext,
|
||||
} from '../../../../../../../src/plugins/ui_actions/public';
|
||||
import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/public';
|
||||
import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public';
|
||||
import { WorkspacePanelWrapper } from './workspace_panel_wrapper';
|
||||
import { DropIllustration } from '../../../assets/drop_illustration';
|
||||
import { getOriginalRequestErrorMessages } from '../../error_helper';
|
||||
import { getMissingIndexPattern, validateDatasourceAndVisualization } from '../state_helpers';
|
||||
import { DefaultInspectorAdapters } from '../../../../../../../src/plugins/expressions/common';
|
||||
import { onActiveDataChange, useLensDispatch } from '../../../state_management';
|
||||
import {
|
||||
onActiveDataChange,
|
||||
useLensDispatch,
|
||||
updateVisualizationState,
|
||||
updateDatasourceState,
|
||||
setSaveable,
|
||||
} from '../../../state_management';
|
||||
|
||||
export interface WorkspacePanelProps {
|
||||
activeVisualizationId: string | null;
|
||||
|
@ -72,12 +74,9 @@ export interface WorkspacePanelProps {
|
|||
}
|
||||
>;
|
||||
framePublicAPI: FramePublicAPI;
|
||||
dispatch: (action: Action) => void;
|
||||
ExpressionRenderer: ReactExpressionRendererType;
|
||||
core: CoreStart;
|
||||
plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart };
|
||||
title?: string;
|
||||
visualizeTriggerFieldContext?: VisualizeFieldContext;
|
||||
getSuggestionForField: (field: DragDropIdentifier) => Suggestion | undefined;
|
||||
isFullscreen: boolean;
|
||||
}
|
||||
|
@ -128,17 +127,15 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
datasourceMap,
|
||||
datasourceStates,
|
||||
framePublicAPI,
|
||||
dispatch,
|
||||
core,
|
||||
plugins,
|
||||
ExpressionRenderer: ExpressionRendererComponent,
|
||||
title,
|
||||
visualizeTriggerFieldContext,
|
||||
suggestionForDraggedField,
|
||||
isFullscreen,
|
||||
}: Omit<WorkspacePanelProps, 'getSuggestionForField'> & {
|
||||
suggestionForDraggedField: Suggestion | undefined;
|
||||
}) {
|
||||
const dispatchLens = useLensDispatch();
|
||||
const [localState, setLocalState] = useState<WorkspaceState>({
|
||||
expressionBuildError: undefined,
|
||||
expandError: false,
|
||||
|
@ -196,6 +193,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
datasourceStates,
|
||||
datasourceLayers: framePublicAPI.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
|
||||
|
@ -233,6 +231,14 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
);
|
||||
|
||||
const expressionExists = Boolean(expression);
|
||||
const hasLoaded = Boolean(
|
||||
activeVisualization && visualizationState && datasourceMap && datasourceStates
|
||||
);
|
||||
useEffect(() => {
|
||||
if (hasLoaded) {
|
||||
dispatchLens(setSaveable(expressionExists));
|
||||
}
|
||||
}, [hasLoaded, expressionExists, dispatchLens]);
|
||||
|
||||
const onEvent = useCallback(
|
||||
(event: ExpressionRendererEvent) => {
|
||||
|
@ -251,14 +257,15 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
});
|
||||
}
|
||||
if (isLensEditEvent(event) && activeVisualization?.onEditAction) {
|
||||
dispatch({
|
||||
type: 'UPDATE_VISUALIZATION_STATE',
|
||||
visualizationId: activeVisualization.id,
|
||||
updater: (oldState: unknown) => activeVisualization.onEditAction!(oldState, event),
|
||||
});
|
||||
dispatchLens(
|
||||
updateVisualizationState({
|
||||
visualizationId: activeVisualization.id,
|
||||
updater: (oldState: unknown) => activeVisualization.onEditAction!(oldState, event),
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[plugins.uiActions, dispatch, activeVisualization]
|
||||
[plugins.uiActions, activeVisualization, dispatchLens]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -275,9 +282,9 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
if (suggestionForDraggedField) {
|
||||
trackUiEvent('drop_onto_workspace');
|
||||
trackUiEvent(expressionExists ? 'drop_non_empty' : 'drop_empty');
|
||||
switchToSuggestion(dispatch, suggestionForDraggedField, 'SWITCH_VISUALIZATION');
|
||||
switchToSuggestion(dispatchLens, suggestionForDraggedField, 'SWITCH_VISUALIZATION');
|
||||
}
|
||||
}, [suggestionForDraggedField, expressionExists, dispatch]);
|
||||
}, [suggestionForDraggedField, expressionExists, dispatchLens]);
|
||||
|
||||
const renderEmptyWorkspace = () => {
|
||||
return (
|
||||
|
@ -327,9 +334,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
};
|
||||
|
||||
const renderVisualization = () => {
|
||||
// we don't want to render the emptyWorkspace on visualizing field from Discover
|
||||
// as it is specific for the drag and drop functionality and can confuse the users
|
||||
if (expression === null && !visualizeTriggerFieldContext) {
|
||||
if (expression === null) {
|
||||
return renderEmptyWorkspace();
|
||||
}
|
||||
return (
|
||||
|
@ -337,7 +342,6 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
expression={expression}
|
||||
framePublicAPI={framePublicAPI}
|
||||
timefilter={plugins.data.query.timefilter.timefilter}
|
||||
dispatch={dispatch}
|
||||
onEvent={onEvent}
|
||||
setLocalState={setLocalState}
|
||||
localState={{ ...localState, configurationValidationError, missingRefsErrors }}
|
||||
|
@ -387,9 +391,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
|
||||
return (
|
||||
<WorkspacePanelWrapper
|
||||
title={title}
|
||||
framePublicAPI={framePublicAPI}
|
||||
dispatch={dispatch}
|
||||
visualizationState={visualizationState}
|
||||
visualizationId={activeVisualizationId}
|
||||
datasourceStates={datasourceStates}
|
||||
|
@ -410,7 +412,6 @@ export const VisualizationWrapper = ({
|
|||
setLocalState,
|
||||
localState,
|
||||
ExpressionRendererComponent,
|
||||
dispatch,
|
||||
application,
|
||||
activeDatasourceId,
|
||||
}: {
|
||||
|
@ -418,7 +419,6 @@ export const VisualizationWrapper = ({
|
|||
framePublicAPI: FramePublicAPI;
|
||||
timefilter: TimefilterContract;
|
||||
onEvent: (event: ExpressionRendererEvent) => void;
|
||||
dispatch: (action: Action) => void;
|
||||
setLocalState: (dispatch: (prevState: WorkspaceState) => WorkspaceState) => void;
|
||||
localState: WorkspaceState & {
|
||||
configurationValidationError?: Array<{
|
||||
|
@ -454,7 +454,7 @@ export const VisualizationWrapper = ({
|
|||
const onData$ = useCallback(
|
||||
(data: unknown, inspectorAdapters?: Partial<DefaultInspectorAdapters>) => {
|
||||
if (inspectorAdapters && inspectorAdapters.tables) {
|
||||
dispatchLens(onActiveDataChange({ activeData: { ...inspectorAdapters.tables.tables } }));
|
||||
dispatchLens(onActiveDataChange({ ...inspectorAdapters.tables.tables }));
|
||||
}
|
||||
},
|
||||
[dispatchLens]
|
||||
|
@ -480,11 +480,12 @@ export const VisualizationWrapper = ({
|
|||
data-test-subj="errorFixAction"
|
||||
onClick={async () => {
|
||||
const newState = await validationError.fixAction?.newState(framePublicAPI);
|
||||
dispatch({
|
||||
type: 'UPDATE_DATASOURCE_STATE',
|
||||
datasourceId: activeDatasourceId,
|
||||
updater: newState,
|
||||
});
|
||||
dispatchLens(
|
||||
updateDatasourceState({
|
||||
updater: newState,
|
||||
datasourceId: activeDatasourceId,
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{validationError.fixAction.label}
|
||||
|
|
|
@ -7,30 +7,23 @@
|
|||
|
||||
import React from 'react';
|
||||
import { Visualization } from '../../../types';
|
||||
import { createMockVisualization, createMockFramePublicAPI, FrameMock } from '../../mocks';
|
||||
import { mountWithIntl as mount } from '@kbn/test/jest';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { WorkspacePanelWrapper, WorkspacePanelWrapperProps } from './workspace_panel_wrapper';
|
||||
import { createMockVisualization, createMockFramePublicAPI, FrameMock } from '../../../mocks';
|
||||
import { WorkspacePanelWrapper } from './workspace_panel_wrapper';
|
||||
import { mountWithProvider } from '../../../mocks';
|
||||
|
||||
describe('workspace_panel_wrapper', () => {
|
||||
let mockVisualization: jest.Mocked<Visualization>;
|
||||
let mockFrameAPI: FrameMock;
|
||||
let instance: ReactWrapper<WorkspacePanelWrapperProps>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockVisualization = createMockVisualization();
|
||||
mockFrameAPI = createMockFramePublicAPI();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
instance.unmount();
|
||||
});
|
||||
|
||||
it('should render its children', () => {
|
||||
it('should render its children', async () => {
|
||||
const MyChild = () => <span>The child elements</span>;
|
||||
instance = mount(
|
||||
const { instance } = await mountWithProvider(
|
||||
<WorkspacePanelWrapper
|
||||
dispatch={jest.fn()}
|
||||
framePublicAPI={mockFrameAPI}
|
||||
visualizationState={{}}
|
||||
visualizationId="myVis"
|
||||
|
@ -46,12 +39,11 @@ describe('workspace_panel_wrapper', () => {
|
|||
expect(instance.find(MyChild)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should call the toolbar renderer if provided', () => {
|
||||
it('should call the toolbar renderer if provided', async () => {
|
||||
const renderToolbarMock = jest.fn();
|
||||
const visState = { internalState: 123 };
|
||||
instance = mount(
|
||||
await mountWithProvider(
|
||||
<WorkspacePanelWrapper
|
||||
dispatch={jest.fn()}
|
||||
framePublicAPI={mockFrameAPI}
|
||||
visualizationState={visState}
|
||||
children={<span />}
|
||||
|
|
|
@ -8,21 +8,19 @@
|
|||
import './workspace_panel_wrapper.scss';
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiPageContent, EuiFlexGroup, EuiFlexItem, EuiScreenReaderOnly } from '@elastic/eui';
|
||||
import { EuiPageContent, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import classNames from 'classnames';
|
||||
import { Datasource, FramePublicAPI, Visualization } from '../../../types';
|
||||
import { NativeRenderer } from '../../../native_renderer';
|
||||
import { Action } from '../state_management';
|
||||
import { ChartSwitch } from './chart_switch';
|
||||
import { WarningsPopover } from './warnings_popover';
|
||||
import { useLensDispatch, updateVisualizationState } from '../../../state_management';
|
||||
import { WorkspaceTitle } from './title';
|
||||
|
||||
export interface WorkspacePanelWrapperProps {
|
||||
children: React.ReactNode | React.ReactNode[];
|
||||
framePublicAPI: FramePublicAPI;
|
||||
visualizationState: unknown;
|
||||
dispatch: (action: Action) => void;
|
||||
title?: string;
|
||||
visualizationMap: Record<string, Visualization>;
|
||||
visualizationId: string | null;
|
||||
datasourceMap: Record<string, Datasource>;
|
||||
|
@ -40,28 +38,29 @@ export function WorkspacePanelWrapper({
|
|||
children,
|
||||
framePublicAPI,
|
||||
visualizationState,
|
||||
dispatch,
|
||||
title,
|
||||
visualizationId,
|
||||
visualizationMap,
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
isFullscreen,
|
||||
}: WorkspacePanelWrapperProps) {
|
||||
const dispatchLens = useLensDispatch();
|
||||
|
||||
const activeVisualization = visualizationId ? visualizationMap[visualizationId] : null;
|
||||
const setVisualizationState = useCallback(
|
||||
(newState: unknown) => {
|
||||
if (!activeVisualization) {
|
||||
return;
|
||||
}
|
||||
dispatch({
|
||||
type: 'UPDATE_VISUALIZATION_STATE',
|
||||
visualizationId: activeVisualization.id,
|
||||
updater: newState,
|
||||
clearStagedPreview: false,
|
||||
});
|
||||
dispatchLens(
|
||||
updateVisualizationState({
|
||||
visualizationId: activeVisualization.id,
|
||||
updater: newState,
|
||||
clearStagedPreview: false,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, activeVisualization]
|
||||
[dispatchLens, activeVisualization]
|
||||
);
|
||||
const warningMessages: React.ReactNode[] = [];
|
||||
if (activeVisualization?.getWarningMessages) {
|
||||
|
@ -101,11 +100,7 @@ export function WorkspacePanelWrapper({
|
|||
<ChartSwitch
|
||||
data-test-subj="lnsChartSwitcher"
|
||||
visualizationMap={visualizationMap}
|
||||
visualizationId={visualizationId}
|
||||
visualizationState={visualizationState}
|
||||
datasourceMap={datasourceMap}
|
||||
datasourceStates={datasourceStates}
|
||||
dispatch={dispatch}
|
||||
framePublicAPI={framePublicAPI}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -136,14 +131,7 @@ export function WorkspacePanelWrapper({
|
|||
'lnsWorkspacePanelWrapper--fullscreen': isFullscreen,
|
||||
})}
|
||||
>
|
||||
<EuiScreenReaderOnly>
|
||||
<h1 id="lns_ChartTitle" data-test-subj="lns_ChartTitle">
|
||||
{title ||
|
||||
i18n.translate('xpack.lens.chartTitle.unsaved', {
|
||||
defaultMessage: 'Unsaved visualization',
|
||||
})}
|
||||
</h1>
|
||||
</EuiScreenReaderOnly>
|
||||
<WorkspaceTitle />
|
||||
{children}
|
||||
</EuiPageContent>
|
||||
</>
|
||||
|
|
|
@ -5,105 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { PaletteDefinition } from 'src/plugins/charts/public';
|
||||
import {
|
||||
ReactExpressionRendererProps,
|
||||
ExpressionsSetup,
|
||||
ExpressionsStart,
|
||||
} from '../../../../../src/plugins/expressions/public';
|
||||
import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public';
|
||||
import { embeddablePluginMock } from '../../../../../src/plugins/embeddable/public/mocks';
|
||||
import { expressionsPluginMock } from '../../../../../src/plugins/expressions/public/mocks';
|
||||
import { DatasourcePublicAPI, FramePublicAPI, Datasource, Visualization } from '../types';
|
||||
import { EditorFrameSetupPlugins, EditorFrameStartPlugins } from './service';
|
||||
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
|
||||
import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
|
||||
|
||||
export function createMockVisualization(): jest.Mocked<Visualization> {
|
||||
return {
|
||||
id: 'TEST_VIS',
|
||||
clearLayer: jest.fn((state, _layerId) => state),
|
||||
removeLayer: jest.fn(),
|
||||
getLayerIds: jest.fn((_state) => ['layer1']),
|
||||
visualizationTypes: [
|
||||
{
|
||||
icon: 'empty',
|
||||
id: 'TEST_VIS',
|
||||
label: 'TEST',
|
||||
groupLabel: 'TEST_VISGroup',
|
||||
},
|
||||
],
|
||||
getVisualizationTypeId: jest.fn((_state) => 'empty'),
|
||||
getDescription: jest.fn((_state) => ({ label: '' })),
|
||||
switchVisualizationType: jest.fn((_, x) => x),
|
||||
getSuggestions: jest.fn((_options) => []),
|
||||
initialize: jest.fn((_frame, _state?) => ({})),
|
||||
getConfiguration: jest.fn((props) => ({
|
||||
groups: [
|
||||
{
|
||||
groupId: 'a',
|
||||
groupLabel: 'a',
|
||||
layerId: 'layer1',
|
||||
supportsMoreColumns: true,
|
||||
accessors: [],
|
||||
filterOperations: jest.fn(() => true),
|
||||
dataTestSubj: 'mockVisA',
|
||||
},
|
||||
],
|
||||
})),
|
||||
toExpression: jest.fn((_state, _frame) => null),
|
||||
toPreviewExpression: jest.fn((_state, _frame) => null),
|
||||
|
||||
setDimension: jest.fn(),
|
||||
removeDimension: jest.fn(),
|
||||
getErrorMessages: jest.fn((_state) => undefined),
|
||||
renderDimensionEditor: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
export type DatasourceMock = jest.Mocked<Datasource> & {
|
||||
publicAPIMock: jest.Mocked<DatasourcePublicAPI>;
|
||||
};
|
||||
|
||||
export function createMockDatasource(id: string): DatasourceMock {
|
||||
const publicAPIMock: jest.Mocked<DatasourcePublicAPI> = {
|
||||
datasourceId: id,
|
||||
getTableSpec: jest.fn(() => []),
|
||||
getOperationForColumnId: jest.fn(),
|
||||
};
|
||||
|
||||
return {
|
||||
id: 'mockindexpattern',
|
||||
clearLayer: jest.fn((state, _layerId) => state),
|
||||
getDatasourceSuggestionsForField: jest.fn((_state, _item) => []),
|
||||
getDatasourceSuggestionsForVisualizeField: jest.fn((_state, _indexpatternId, _fieldName) => []),
|
||||
getDatasourceSuggestionsFromCurrentState: jest.fn((_state) => []),
|
||||
getPersistableState: jest.fn((x) => ({ state: x, savedObjectReferences: [] })),
|
||||
getPublicAPI: jest.fn().mockReturnValue(publicAPIMock),
|
||||
initialize: jest.fn((_state?) => Promise.resolve()),
|
||||
renderDataPanel: jest.fn(),
|
||||
renderLayerPanel: jest.fn(),
|
||||
toExpression: jest.fn((_frame, _state) => null),
|
||||
insertLayer: jest.fn((_state, _newLayerId) => {}),
|
||||
removeLayer: jest.fn((_state, _layerId) => {}),
|
||||
removeColumn: jest.fn((props) => {}),
|
||||
getLayers: jest.fn((_state) => []),
|
||||
uniqueLabels: jest.fn((_state) => ({})),
|
||||
renderDimensionTrigger: jest.fn(),
|
||||
renderDimensionEditor: jest.fn(),
|
||||
getDropProps: jest.fn(),
|
||||
onDrop: jest.fn(),
|
||||
|
||||
// this is an additional property which doesn't exist on real datasources
|
||||
// but can be used to validate whether specific API mock functions are called
|
||||
publicAPIMock,
|
||||
getErrorMessages: jest.fn((_state) => undefined),
|
||||
checkIntegrity: jest.fn((_state) => []),
|
||||
};
|
||||
}
|
||||
|
||||
export type FrameMock = jest.Mocked<FramePublicAPI>;
|
||||
|
||||
export function createMockPaletteDefinition(): jest.Mocked<PaletteDefinition> {
|
||||
return {
|
||||
getCategoricalColors: jest.fn((_) => ['#ff0000', '#00ff00']),
|
||||
|
@ -123,23 +32,6 @@ export function createMockPaletteDefinition(): jest.Mocked<PaletteDefinition> {
|
|||
};
|
||||
}
|
||||
|
||||
export function createMockFramePublicAPI(): FrameMock {
|
||||
const palette = createMockPaletteDefinition();
|
||||
return {
|
||||
datasourceLayers: {},
|
||||
addNewLayer: jest.fn(() => ''),
|
||||
removeLayers: jest.fn(),
|
||||
dateRange: { fromDate: 'now-7d', toDate: 'now' },
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
availablePalettes: {
|
||||
get: () => palette,
|
||||
getAll: () => [palette],
|
||||
},
|
||||
searchSessionId: 'sessionId',
|
||||
};
|
||||
}
|
||||
|
||||
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
|
||||
|
||||
export type MockedSetupDependencies = Omit<EditorFrameSetupPlugins, 'expressions'> & {
|
||||
|
@ -150,13 +42,6 @@ export type MockedStartDependencies = Omit<EditorFrameStartPlugins, 'expressions
|
|||
expressions: jest.Mocked<ExpressionsStart>;
|
||||
};
|
||||
|
||||
export function createExpressionRendererMock(): jest.Mock<
|
||||
React.ReactElement,
|
||||
[ReactExpressionRendererProps]
|
||||
> {
|
||||
return jest.fn((_) => <span />);
|
||||
}
|
||||
|
||||
export function createMockSetupDependencies() {
|
||||
return ({
|
||||
data: dataPluginMock.createSetupContract(),
|
||||
|
|
|
@ -105,27 +105,25 @@ export class EditorFrameService {
|
|||
]);
|
||||
|
||||
const { EditorFrame } = await import('../async_services');
|
||||
const palettes = await plugins.charts.palettes.getPalettes();
|
||||
|
||||
return {
|
||||
EditorFrameContainer: ({ onError, showNoDataPopover, initialContext }) => {
|
||||
EditorFrameContainer: ({ showNoDataPopover }) => {
|
||||
return (
|
||||
<div className="lnsApp__frame">
|
||||
<EditorFrame
|
||||
data-test-subj="lnsEditorFrame"
|
||||
onError={onError}
|
||||
datasourceMap={resolvedDatasources}
|
||||
visualizationMap={resolvedVisualizations}
|
||||
core={core}
|
||||
plugins={plugins}
|
||||
ExpressionRenderer={plugins.expressions.ReactExpressionRenderer}
|
||||
palettes={palettes}
|
||||
showNoDataPopover={showNoDataPopover}
|
||||
initialContext={initialContext}
|
||||
datasourceMap={resolvedDatasources}
|
||||
visualizationMap={resolvedVisualizations}
|
||||
ExpressionRenderer={plugins.expressions.ReactExpressionRenderer}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
datasourceMap: resolvedDatasources,
|
||||
visualizationMap: resolvedVisualizations,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
getHeatmapVisualization,
|
||||
isCellValueSupported,
|
||||
} from './visualization';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../mocks';
|
||||
import {
|
||||
CHART_SHAPES,
|
||||
FUNCTION_NAME,
|
||||
|
@ -49,8 +49,8 @@ describe('heatmap', () => {
|
|||
|
||||
describe('#intialize', () => {
|
||||
test('returns a default state', () => {
|
||||
expect(getHeatmapVisualization({}).initialize(frame)).toEqual({
|
||||
layerId: '',
|
||||
expect(getHeatmapVisualization({}).initialize(() => 'l1')).toEqual({
|
||||
layerId: 'l1',
|
||||
title: 'Empty Heatmap chart',
|
||||
shape: CHART_SHAPES.HEATMAP,
|
||||
legend: {
|
||||
|
@ -68,7 +68,9 @@ describe('heatmap', () => {
|
|||
});
|
||||
|
||||
test('returns persisted state', () => {
|
||||
expect(getHeatmapVisualization({}).initialize(frame, exampleState())).toEqual(exampleState());
|
||||
expect(getHeatmapVisualization({}).initialize(() => 'test-layer', exampleState())).toEqual(
|
||||
exampleState()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -119,10 +119,10 @@ export const getHeatmapVisualization = ({
|
|||
return CHART_NAMES.heatmap;
|
||||
},
|
||||
|
||||
initialize(frame, state, mainPalette) {
|
||||
initialize(addNewLayer, state, mainPalette) {
|
||||
return (
|
||||
state || {
|
||||
layerId: frame.addNewLayer(),
|
||||
layerId: addNewLayer(),
|
||||
title: 'Empty Heatmap chart',
|
||||
...getInitialState(),
|
||||
}
|
||||
|
|
|
@ -446,10 +446,13 @@ export async function syncExistingFields({
|
|||
isFirstExistenceFetch: false,
|
||||
existenceFetchFailed: false,
|
||||
existenceFetchTimeout: false,
|
||||
existingFields: emptinessInfo.reduce((acc, info) => {
|
||||
acc[info.indexPatternTitle] = booleanMap(info.existingFieldNames);
|
||||
return acc;
|
||||
}, state.existingFields),
|
||||
existingFields: emptinessInfo.reduce(
|
||||
(acc, info) => {
|
||||
acc[info.indexPatternTitle] = booleanMap(info.existingFieldNames);
|
||||
return acc;
|
||||
},
|
||||
{ ...state.existingFields }
|
||||
),
|
||||
}));
|
||||
} catch (e) {
|
||||
// show all fields as available if fetch failed or timed out
|
||||
|
@ -457,10 +460,13 @@ export async function syncExistingFields({
|
|||
...state,
|
||||
existenceFetchFailed: e.res?.status !== 408,
|
||||
existenceFetchTimeout: e.res?.status === 408,
|
||||
existingFields: indexPatterns.reduce((acc, pattern) => {
|
||||
acc[pattern.title] = booleanMap(pattern.fields.map((field) => field.name));
|
||||
return acc;
|
||||
}, state.existingFields),
|
||||
existingFields: indexPatterns.reduce(
|
||||
(acc, pattern) => {
|
||||
acc[pattern.title] = booleanMap(pattern.fields.map((field) => field.name));
|
||||
return acc;
|
||||
},
|
||||
{ ...state.existingFields }
|
||||
),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { metricVisualization } from './visualization';
|
||||
import { MetricState } from './types';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../mocks';
|
||||
import { generateId } from '../id_generator';
|
||||
import { DatasourcePublicAPI, FramePublicAPI } from '../types';
|
||||
|
||||
|
@ -23,7 +23,6 @@ function exampleState(): MetricState {
|
|||
function mockFrame(): FramePublicAPI {
|
||||
return {
|
||||
...createMockFramePublicAPI(),
|
||||
addNewLayer: () => 'l42',
|
||||
datasourceLayers: {
|
||||
l1: createMockDatasource('l1').publicAPIMock,
|
||||
l42: createMockDatasource('l42').publicAPIMock,
|
||||
|
@ -35,19 +34,19 @@ describe('metric_visualization', () => {
|
|||
describe('#initialize', () => {
|
||||
it('loads default state', () => {
|
||||
(generateId as jest.Mock).mockReturnValueOnce('test-id1');
|
||||
const initialState = metricVisualization.initialize(mockFrame());
|
||||
const initialState = metricVisualization.initialize(() => 'test-id1');
|
||||
|
||||
expect(initialState.accessor).not.toBeDefined();
|
||||
expect(initialState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"accessor": undefined,
|
||||
"layerId": "l42",
|
||||
}
|
||||
`);
|
||||
Object {
|
||||
"accessor": undefined,
|
||||
"layerId": "test-id1",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('loads from persisted state', () => {
|
||||
expect(metricVisualization.initialize(mockFrame(), exampleState())).toEqual(exampleState());
|
||||
expect(metricVisualization.initialize(() => 'l1', exampleState())).toEqual(exampleState());
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -85,10 +85,10 @@ export const metricVisualization: Visualization<MetricState> = {
|
|||
|
||||
getSuggestions,
|
||||
|
||||
initialize(frame, state) {
|
||||
initialize(addNewLayer, state) {
|
||||
return (
|
||||
state || {
|
||||
layerId: frame.addNewLayer(),
|
||||
layerId: addNewLayer(),
|
||||
accessor: undefined,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -15,6 +15,7 @@ import { coreMock } from 'src/core/public/mocks';
|
|||
import moment from 'moment';
|
||||
import { Provider } from 'react-redux';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { ReactExpressionRendererProps } from 'src/plugins/expressions/public';
|
||||
import { LensPublicStart } from '.';
|
||||
import { visualizationTypes } from './xy_visualization/types';
|
||||
import { navigationPluginMock } from '../../../../src/plugins/navigation/public/mocks';
|
||||
|
@ -37,6 +38,111 @@ import { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/publ
|
|||
import { makeConfigureStore, getPreloadedState, LensAppState } from './state_management/index';
|
||||
import { getResolvedDateRange } from './utils';
|
||||
import { presentationUtilPluginMock } from '../../../../src/plugins/presentation_util/public/mocks';
|
||||
import { DatasourcePublicAPI, Datasource, Visualization, FramePublicAPI } from './types';
|
||||
|
||||
export function createMockVisualization(): jest.Mocked<Visualization> {
|
||||
return {
|
||||
id: 'TEST_VIS',
|
||||
clearLayer: jest.fn((state, _layerId) => state),
|
||||
removeLayer: jest.fn(),
|
||||
getLayerIds: jest.fn((_state) => ['layer1']),
|
||||
visualizationTypes: [
|
||||
{
|
||||
icon: 'empty',
|
||||
id: 'TEST_VIS',
|
||||
label: 'TEST',
|
||||
groupLabel: 'TEST_VISGroup',
|
||||
},
|
||||
],
|
||||
getVisualizationTypeId: jest.fn((_state) => 'empty'),
|
||||
getDescription: jest.fn((_state) => ({ label: '' })),
|
||||
switchVisualizationType: jest.fn((_, x) => x),
|
||||
getSuggestions: jest.fn((_options) => []),
|
||||
initialize: jest.fn((_frame, _state?) => ({})),
|
||||
getConfiguration: jest.fn((props) => ({
|
||||
groups: [
|
||||
{
|
||||
groupId: 'a',
|
||||
groupLabel: 'a',
|
||||
layerId: 'layer1',
|
||||
supportsMoreColumns: true,
|
||||
accessors: [],
|
||||
filterOperations: jest.fn(() => true),
|
||||
dataTestSubj: 'mockVisA',
|
||||
},
|
||||
],
|
||||
})),
|
||||
toExpression: jest.fn((_state, _frame) => null),
|
||||
toPreviewExpression: jest.fn((_state, _frame) => null),
|
||||
|
||||
setDimension: jest.fn(),
|
||||
removeDimension: jest.fn(),
|
||||
getErrorMessages: jest.fn((_state) => undefined),
|
||||
renderDimensionEditor: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
export type DatasourceMock = jest.Mocked<Datasource> & {
|
||||
publicAPIMock: jest.Mocked<DatasourcePublicAPI>;
|
||||
};
|
||||
|
||||
export function createMockDatasource(id: string): DatasourceMock {
|
||||
const publicAPIMock: jest.Mocked<DatasourcePublicAPI> = {
|
||||
datasourceId: id,
|
||||
getTableSpec: jest.fn(() => []),
|
||||
getOperationForColumnId: jest.fn(),
|
||||
};
|
||||
|
||||
return {
|
||||
id: 'mockindexpattern',
|
||||
clearLayer: jest.fn((state, _layerId) => state),
|
||||
getDatasourceSuggestionsForField: jest.fn((_state, _item) => []),
|
||||
getDatasourceSuggestionsForVisualizeField: jest.fn((_state, _indexpatternId, _fieldName) => []),
|
||||
getDatasourceSuggestionsFromCurrentState: jest.fn((_state) => []),
|
||||
getPersistableState: jest.fn((x) => ({
|
||||
state: x,
|
||||
savedObjectReferences: [{ type: 'index-pattern', id: 'mockip', name: 'mockip' }],
|
||||
})),
|
||||
getPublicAPI: jest.fn().mockReturnValue(publicAPIMock),
|
||||
initialize: jest.fn((_state?) => Promise.resolve()),
|
||||
renderDataPanel: jest.fn(),
|
||||
renderLayerPanel: jest.fn(),
|
||||
toExpression: jest.fn((_frame, _state) => null),
|
||||
insertLayer: jest.fn((_state, _newLayerId) => {}),
|
||||
removeLayer: jest.fn((_state, _layerId) => {}),
|
||||
removeColumn: jest.fn((props) => {}),
|
||||
getLayers: jest.fn((_state) => []),
|
||||
uniqueLabels: jest.fn((_state) => ({})),
|
||||
renderDimensionTrigger: jest.fn(),
|
||||
renderDimensionEditor: jest.fn(),
|
||||
getDropProps: jest.fn(),
|
||||
onDrop: jest.fn(),
|
||||
|
||||
// this is an additional property which doesn't exist on real datasources
|
||||
// but can be used to validate whether specific API mock functions are called
|
||||
publicAPIMock,
|
||||
getErrorMessages: jest.fn((_state) => undefined),
|
||||
checkIntegrity: jest.fn((_state) => []),
|
||||
};
|
||||
}
|
||||
|
||||
export function createExpressionRendererMock(): jest.Mock<
|
||||
React.ReactElement,
|
||||
[ReactExpressionRendererProps]
|
||||
> {
|
||||
return jest.fn((_) => <span />);
|
||||
}
|
||||
|
||||
export type FrameMock = jest.Mocked<FramePublicAPI>;
|
||||
export function createMockFramePublicAPI(): FrameMock {
|
||||
return {
|
||||
datasourceLayers: {},
|
||||
dateRange: { fromDate: 'now-7d', toDate: 'now' },
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
searchSessionId: 'sessionId',
|
||||
};
|
||||
}
|
||||
|
||||
export type Start = jest.Mocked<LensPublicStart>;
|
||||
|
||||
|
@ -66,6 +172,9 @@ export const defaultDoc = ({
|
|||
state: {
|
||||
query: 'kuery',
|
||||
filters: [{ query: { match_phrase: { src: 'test' } } }],
|
||||
datasourceStates: {
|
||||
testDatasource: 'datasource',
|
||||
},
|
||||
},
|
||||
references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }],
|
||||
} as unknown) as Document;
|
||||
|
@ -257,20 +366,48 @@ export function makeDefaultServices(
|
|||
};
|
||||
}
|
||||
|
||||
export function mockLensStore({
|
||||
export const defaultState = {
|
||||
searchSessionId: 'sessionId-1',
|
||||
filters: [],
|
||||
query: { language: 'lucene', query: '' },
|
||||
resolvedDateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' },
|
||||
isFullscreenDatasource: false,
|
||||
isSaveable: false,
|
||||
isLoading: false,
|
||||
isLinkedToOriginatingApp: false,
|
||||
activeDatasourceId: 'testDatasource',
|
||||
visualization: {
|
||||
state: {},
|
||||
activeId: 'testVis',
|
||||
},
|
||||
datasourceStates: {
|
||||
testDatasource: {
|
||||
isLoading: false,
|
||||
state: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function makeLensStore({
|
||||
data,
|
||||
storePreloadedState,
|
||||
preloadedState,
|
||||
dispatch,
|
||||
}: {
|
||||
data: DataPublicPluginStart;
|
||||
storePreloadedState?: Partial<LensAppState>;
|
||||
data?: DataPublicPluginStart;
|
||||
preloadedState?: Partial<LensAppState>;
|
||||
dispatch?: jest.Mock;
|
||||
}) {
|
||||
if (!data) {
|
||||
data = mockDataPlugin();
|
||||
}
|
||||
const lensStore = makeConfigureStore(
|
||||
getPreloadedState({
|
||||
...defaultState,
|
||||
searchSessionId: data.search.session.start(),
|
||||
query: data.query.queryString.getQuery(),
|
||||
filters: data.query.filterManager.getGlobalFilters(),
|
||||
searchSessionId: data.search.session.start(),
|
||||
resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter),
|
||||
...storePreloadedState,
|
||||
...preloadedState,
|
||||
}),
|
||||
{
|
||||
data,
|
||||
|
@ -278,36 +415,52 @@ export function mockLensStore({
|
|||
);
|
||||
|
||||
const origDispatch = lensStore.dispatch;
|
||||
lensStore.dispatch = jest.fn(origDispatch);
|
||||
lensStore.dispatch = jest.fn(dispatch || origDispatch);
|
||||
return lensStore;
|
||||
}
|
||||
|
||||
export const mountWithProvider = async (
|
||||
component: React.ReactElement,
|
||||
data: DataPublicPluginStart,
|
||||
storePreloadedState?: Partial<LensAppState>,
|
||||
extraWrappingComponent?: React.FC<{
|
||||
children: React.ReactNode;
|
||||
}>
|
||||
store?: {
|
||||
data?: DataPublicPluginStart;
|
||||
preloadedState?: Partial<LensAppState>;
|
||||
dispatch?: jest.Mock;
|
||||
},
|
||||
options?: {
|
||||
wrappingComponent?: React.FC<{
|
||||
children: React.ReactNode;
|
||||
}>;
|
||||
attachTo?: HTMLElement;
|
||||
}
|
||||
) => {
|
||||
const lensStore = mockLensStore({ data, storePreloadedState });
|
||||
const lensStore = makeLensStore(store || {});
|
||||
|
||||
const wrappingComponent: React.FC<{
|
||||
let wrappingComponent: React.FC<{
|
||||
children: React.ReactNode;
|
||||
}> = ({ children }) => {
|
||||
if (extraWrappingComponent) {
|
||||
return extraWrappingComponent({
|
||||
children: <Provider store={lensStore}>{children}</Provider>,
|
||||
});
|
||||
}
|
||||
return <Provider store={lensStore}>{children}</Provider>;
|
||||
}> = ({ children }) => <Provider store={lensStore}>{children}</Provider>;
|
||||
|
||||
let restOptions: {
|
||||
attachTo?: HTMLElement | undefined;
|
||||
};
|
||||
if (options) {
|
||||
const { wrappingComponent: _wrappingComponent, ...rest } = options;
|
||||
restOptions = rest;
|
||||
|
||||
if (_wrappingComponent) {
|
||||
wrappingComponent = ({ children }) => {
|
||||
return _wrappingComponent({
|
||||
children: <Provider store={lensStore}>{children}</Provider>,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let instance: ReactWrapper = {} as ReactWrapper;
|
||||
|
||||
await act(async () => {
|
||||
instance = mount(component, ({
|
||||
wrappingComponent,
|
||||
...restOptions,
|
||||
} as unknown) as ReactWrapper);
|
||||
});
|
||||
return { instance, lensStore };
|
||||
|
|
|
@ -91,11 +91,11 @@ export const getPieVisualization = ({
|
|||
shape: visualizationTypeId as PieVisualizationState['shape'],
|
||||
}),
|
||||
|
||||
initialize(frame, state, mainPalette) {
|
||||
initialize(addNewLayer, state, mainPalette) {
|
||||
return (
|
||||
state || {
|
||||
shape: 'donut',
|
||||
layers: [newLayerState(frame.addNewLayer())],
|
||||
layers: [newLayerState(addNewLayer())],
|
||||
palette: mainPalette,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { LensAppState } from './types';
|
||||
|
||||
export const initialState: LensAppState = {
|
||||
searchSessionId: '',
|
||||
filters: [],
|
||||
query: { language: 'kuery', query: '' },
|
||||
resolvedDateRange: { fromDate: '', toDate: '' },
|
||||
|
||||
indexPatternsForTopNav: [],
|
||||
isSaveable: false,
|
||||
isAppLoading: false,
|
||||
isLinkedToOriginatingApp: false,
|
||||
};
|
||||
|
||||
export const appSlice = createSlice({
|
||||
name: 'app',
|
||||
initialState,
|
||||
reducers: {
|
||||
setState: (state, { payload }: PayloadAction<Partial<LensAppState>>) => {
|
||||
return {
|
||||
...state,
|
||||
...payload,
|
||||
};
|
||||
},
|
||||
onChangeFromEditorFrame: (state, { payload }: PayloadAction<Partial<LensAppState>>) => {
|
||||
return {
|
||||
...state,
|
||||
...payload,
|
||||
};
|
||||
},
|
||||
onActiveDataChange: (state, { payload }: PayloadAction<Partial<LensAppState>>) => {
|
||||
if (!isEqual(state.activeData, payload?.activeData)) {
|
||||
return {
|
||||
...state,
|
||||
...payload,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
},
|
||||
navigateAway: (state) => state,
|
||||
},
|
||||
});
|
||||
|
||||
export const reducer = {
|
||||
app: appSlice.reducer,
|
||||
};
|
|
@ -27,7 +27,7 @@ export const externalContextMiddleware = (data: DataPublicPluginStart) => (
|
|||
store.dispatch
|
||||
);
|
||||
return (next: Dispatch) => (action: PayloadAction<Partial<LensAppState>>) => {
|
||||
if (action.type === 'app/navigateAway') {
|
||||
if (action.type === 'lens/navigateAway') {
|
||||
unsubscribeFromExternalContext();
|
||||
}
|
||||
next(action);
|
||||
|
@ -44,7 +44,7 @@ function subscribeToExternalContext(
|
|||
|
||||
const dispatchFromExternal = (searchSessionId = search.session.start()) => {
|
||||
const globalFilters = filterManager.getFilters();
|
||||
const filters = isEqual(getState().app.filters, globalFilters)
|
||||
const filters = isEqual(getState().lens.filters, globalFilters)
|
||||
? null
|
||||
: { filters: globalFilters };
|
||||
dispatch(
|
||||
|
@ -64,7 +64,7 @@ function subscribeToExternalContext(
|
|||
.pipe(delay(0))
|
||||
// then update if it didn't get updated yet
|
||||
.subscribe((newSessionId?: string) => {
|
||||
if (newSessionId && getState().app.searchSessionId !== newSessionId) {
|
||||
if (newSessionId && getState().lens.searchSessionId !== newSessionId) {
|
||||
debounceDispatchFromExternal(newSessionId);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -8,8 +8,9 @@
|
|||
import { configureStore, DeepPartial, getDefaultMiddleware } from '@reduxjs/toolkit';
|
||||
import logger from 'redux-logger';
|
||||
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
|
||||
import { appSlice, initialState } from './app_slice';
|
||||
import { lensSlice, initialState } from './lens_slice';
|
||||
import { timeRangeMiddleware } from './time_range_middleware';
|
||||
import { optimizingMiddleware } from './optimizing_middleware';
|
||||
import { externalContextMiddleware } from './external_context_middleware';
|
||||
|
||||
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
|
||||
|
@ -17,19 +18,29 @@ import { LensAppState, LensState } from './types';
|
|||
export * from './types';
|
||||
|
||||
export const reducer = {
|
||||
app: appSlice.reducer,
|
||||
lens: lensSlice.reducer,
|
||||
};
|
||||
|
||||
export const {
|
||||
setState,
|
||||
navigateAway,
|
||||
onChangeFromEditorFrame,
|
||||
setSaveable,
|
||||
onActiveDataChange,
|
||||
} = appSlice.actions;
|
||||
updateState,
|
||||
updateDatasourceState,
|
||||
updateVisualizationState,
|
||||
updateLayer,
|
||||
switchVisualization,
|
||||
selectSuggestion,
|
||||
rollbackSuggestion,
|
||||
submitSuggestion,
|
||||
switchDatasource,
|
||||
setToggleFullscreen,
|
||||
} = lensSlice.actions;
|
||||
|
||||
export const getPreloadedState = (initializedState: Partial<LensAppState>) => {
|
||||
const state = {
|
||||
app: {
|
||||
lens: {
|
||||
...initialState,
|
||||
...initializedState,
|
||||
},
|
||||
|
@ -45,15 +56,9 @@ export const makeConfigureStore = (
|
|||
) => {
|
||||
const middleware = [
|
||||
...getDefaultMiddleware({
|
||||
serializableCheck: {
|
||||
ignoredActions: [
|
||||
'app/setState',
|
||||
'app/onChangeFromEditorFrame',
|
||||
'app/onActiveDataChange',
|
||||
'app/navigateAway',
|
||||
],
|
||||
},
|
||||
serializableCheck: false,
|
||||
}),
|
||||
optimizingMiddleware(),
|
||||
timeRangeMiddleware(data),
|
||||
externalContextMiddleware(data),
|
||||
];
|
||||
|
|
148
x-pack/plugins/lens/public/state_management/lens_slice.test.ts
Normal file
148
x-pack/plugins/lens/public/state_management/lens_slice.test.ts
Normal file
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* 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 { Query } from 'src/plugins/data/public';
|
||||
import {
|
||||
switchDatasource,
|
||||
switchVisualization,
|
||||
setState,
|
||||
updateState,
|
||||
updateDatasourceState,
|
||||
updateVisualizationState,
|
||||
} from '.';
|
||||
import { makeLensStore, defaultState } from '../mocks';
|
||||
|
||||
describe('lensSlice', () => {
|
||||
const store = makeLensStore({});
|
||||
const customQuery = { query: 'custom' } as Query;
|
||||
|
||||
// TODO: need to move some initialization logic from mounter
|
||||
// describe('initialization', () => {
|
||||
// })
|
||||
|
||||
describe('state update', () => {
|
||||
it('setState: updates state ', () => {
|
||||
const lensState = store.getState().lens;
|
||||
expect(lensState).toEqual(defaultState);
|
||||
store.dispatch(setState({ query: customQuery }));
|
||||
const changedState = store.getState().lens;
|
||||
expect(changedState).toEqual({ ...defaultState, query: customQuery });
|
||||
});
|
||||
|
||||
it('updateState: updates state with updater', () => {
|
||||
const customUpdater = jest.fn((state) => ({ ...state, query: customQuery }));
|
||||
store.dispatch(updateState({ subType: 'UPDATE', updater: customUpdater }));
|
||||
const changedState = store.getState().lens;
|
||||
expect(changedState).toEqual({ ...defaultState, query: customQuery });
|
||||
});
|
||||
it('should update the corresponding visualization state on update', () => {
|
||||
const newVisState = {};
|
||||
store.dispatch(
|
||||
updateVisualizationState({
|
||||
visualizationId: 'testVis',
|
||||
updater: newVisState,
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.getState().lens.visualization.state).toBe(newVisState);
|
||||
});
|
||||
it('should update the datasource state with passed in reducer', () => {
|
||||
const datasourceUpdater = jest.fn(() => ({ changed: true }));
|
||||
store.dispatch(
|
||||
updateDatasourceState({
|
||||
datasourceId: 'testDatasource',
|
||||
updater: datasourceUpdater,
|
||||
})
|
||||
);
|
||||
expect(store.getState().lens.datasourceStates.testDatasource.state).toStrictEqual({
|
||||
changed: true,
|
||||
});
|
||||
expect(datasourceUpdater).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should update the layer state with passed in reducer', () => {
|
||||
const newDatasourceState = {};
|
||||
store.dispatch(
|
||||
updateDatasourceState({
|
||||
datasourceId: 'testDatasource',
|
||||
updater: newDatasourceState,
|
||||
})
|
||||
);
|
||||
expect(store.getState().lens.datasourceStates.testDatasource.state).toStrictEqual(
|
||||
newDatasourceState
|
||||
);
|
||||
});
|
||||
it('should should switch active visualization', () => {
|
||||
const newVisState = {};
|
||||
store.dispatch(
|
||||
switchVisualization({
|
||||
newVisualizationId: 'testVis2',
|
||||
initialState: newVisState,
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.getState().lens.visualization.state).toBe(newVisState);
|
||||
});
|
||||
|
||||
it('should should switch active visualization and update datasource state', () => {
|
||||
const newVisState = {};
|
||||
const newDatasourceState = {};
|
||||
|
||||
store.dispatch(
|
||||
switchVisualization({
|
||||
newVisualizationId: 'testVis2',
|
||||
initialState: newVisState,
|
||||
datasourceState: newDatasourceState,
|
||||
datasourceId: 'testDatasource',
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.getState().lens.visualization.state).toBe(newVisState);
|
||||
expect(store.getState().lens.datasourceStates.testDatasource.state).toBe(newDatasourceState);
|
||||
});
|
||||
|
||||
it('should switch active datasource and initialize new state', () => {
|
||||
store.dispatch(
|
||||
switchDatasource({
|
||||
newDatasourceId: 'testDatasource2',
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.getState().lens.activeDatasourceId).toEqual('testDatasource2');
|
||||
expect(store.getState().lens.datasourceStates.testDatasource2.isLoading).toEqual(true);
|
||||
});
|
||||
|
||||
it('not initialize already initialized datasource on switch', () => {
|
||||
const datasource2State = {};
|
||||
const customStore = makeLensStore({
|
||||
preloadedState: {
|
||||
datasourceStates: {
|
||||
testDatasource: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
testDatasource2: {
|
||||
state: datasource2State,
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
customStore.dispatch(
|
||||
switchDatasource({
|
||||
newDatasourceId: 'testDatasource2',
|
||||
})
|
||||
);
|
||||
|
||||
expect(customStore.getState().lens.activeDatasourceId).toEqual('testDatasource2');
|
||||
expect(customStore.getState().lens.datasourceStates.testDatasource2.isLoading).toEqual(false);
|
||||
expect(customStore.getState().lens.datasourceStates.testDatasource2.state).toBe(
|
||||
datasource2State
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
262
x-pack/plugins/lens/public/state_management/lens_slice.ts
Normal file
262
x-pack/plugins/lens/public/state_management/lens_slice.ts
Normal file
|
@ -0,0 +1,262 @@
|
|||
/*
|
||||
* 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 { createSlice, current, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { TableInspectorAdapter } from '../editor_frame_service/types';
|
||||
import { LensAppState } from './types';
|
||||
|
||||
export const initialState: LensAppState = {
|
||||
searchSessionId: '',
|
||||
filters: [],
|
||||
query: { language: 'kuery', query: '' },
|
||||
resolvedDateRange: { fromDate: '', toDate: '' },
|
||||
isFullscreenDatasource: false,
|
||||
isSaveable: false,
|
||||
isLoading: false,
|
||||
isLinkedToOriginatingApp: false,
|
||||
activeDatasourceId: null,
|
||||
datasourceStates: {},
|
||||
visualization: {
|
||||
state: null,
|
||||
activeId: null,
|
||||
},
|
||||
};
|
||||
|
||||
export const lensSlice = createSlice({
|
||||
name: 'lens',
|
||||
initialState,
|
||||
reducers: {
|
||||
setState: (state, { payload }: PayloadAction<Partial<LensAppState>>) => {
|
||||
return {
|
||||
...state,
|
||||
...payload,
|
||||
};
|
||||
},
|
||||
onActiveDataChange: (state, { payload }: PayloadAction<TableInspectorAdapter>) => {
|
||||
return {
|
||||
...state,
|
||||
activeData: payload,
|
||||
};
|
||||
},
|
||||
setSaveable: (state, { payload }: PayloadAction<boolean>) => {
|
||||
return {
|
||||
...state,
|
||||
isSaveable: payload,
|
||||
};
|
||||
},
|
||||
updateState: (
|
||||
state,
|
||||
action: {
|
||||
payload: {
|
||||
subType: string;
|
||||
updater: (prevState: LensAppState) => LensAppState;
|
||||
};
|
||||
}
|
||||
) => {
|
||||
return action.payload.updater(current(state) as LensAppState);
|
||||
},
|
||||
updateDatasourceState: (
|
||||
state,
|
||||
{
|
||||
payload,
|
||||
}: {
|
||||
payload: {
|
||||
updater: unknown | ((prevState: unknown) => unknown);
|
||||
datasourceId: string;
|
||||
clearStagedPreview?: boolean;
|
||||
};
|
||||
}
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
datasourceStates: {
|
||||
...state.datasourceStates,
|
||||
[payload.datasourceId]: {
|
||||
state:
|
||||
typeof payload.updater === 'function'
|
||||
? payload.updater(current(state).datasourceStates[payload.datasourceId].state)
|
||||
: payload.updater,
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
stagedPreview: payload.clearStagedPreview ? undefined : state.stagedPreview,
|
||||
};
|
||||
},
|
||||
updateVisualizationState: (
|
||||
state,
|
||||
{
|
||||
payload,
|
||||
}: {
|
||||
payload: {
|
||||
visualizationId: string;
|
||||
updater: unknown | ((state: unknown) => unknown);
|
||||
clearStagedPreview?: boolean;
|
||||
};
|
||||
}
|
||||
) => {
|
||||
if (!state.visualization.activeId) {
|
||||
throw new Error('Invariant: visualization state got updated without active visualization');
|
||||
}
|
||||
// This is a safeguard that prevents us from accidentally updating the
|
||||
// wrong visualization. This occurs in some cases due to the uncoordinated
|
||||
// way we manage state across plugins.
|
||||
if (state.visualization.activeId !== payload.visualizationId) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
visualization: {
|
||||
...state.visualization,
|
||||
state:
|
||||
typeof payload.updater === 'function'
|
||||
? payload.updater(current(state.visualization.state))
|
||||
: payload.updater,
|
||||
},
|
||||
stagedPreview: payload.clearStagedPreview ? undefined : state.stagedPreview,
|
||||
};
|
||||
},
|
||||
updateLayer: (
|
||||
state,
|
||||
{
|
||||
payload,
|
||||
}: {
|
||||
payload: {
|
||||
layerId: string;
|
||||
datasourceId: string;
|
||||
updater: (state: unknown, layerId: string) => unknown;
|
||||
};
|
||||
}
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
datasourceStates: {
|
||||
...state.datasourceStates,
|
||||
[payload.datasourceId]: {
|
||||
...state.datasourceStates[payload.datasourceId],
|
||||
state: payload.updater(
|
||||
current(state).datasourceStates[payload.datasourceId].state,
|
||||
payload.layerId
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
switchVisualization: (
|
||||
state,
|
||||
{
|
||||
payload,
|
||||
}: {
|
||||
payload: {
|
||||
newVisualizationId: string;
|
||||
initialState: unknown;
|
||||
datasourceState?: unknown;
|
||||
datasourceId?: string;
|
||||
};
|
||||
}
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
datasourceStates:
|
||||
'datasourceId' in payload && payload.datasourceId
|
||||
? {
|
||||
...state.datasourceStates,
|
||||
[payload.datasourceId]: {
|
||||
...state.datasourceStates[payload.datasourceId],
|
||||
state: payload.datasourceState,
|
||||
},
|
||||
}
|
||||
: state.datasourceStates,
|
||||
visualization: {
|
||||
...state.visualization,
|
||||
activeId: payload.newVisualizationId,
|
||||
state: payload.initialState,
|
||||
},
|
||||
stagedPreview: undefined,
|
||||
};
|
||||
},
|
||||
selectSuggestion: (
|
||||
state,
|
||||
{
|
||||
payload,
|
||||
}: {
|
||||
payload: {
|
||||
newVisualizationId: string;
|
||||
initialState: unknown;
|
||||
datasourceState: unknown;
|
||||
datasourceId: string;
|
||||
};
|
||||
}
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
datasourceStates:
|
||||
'datasourceId' in payload && payload.datasourceId
|
||||
? {
|
||||
...state.datasourceStates,
|
||||
[payload.datasourceId]: {
|
||||
...state.datasourceStates[payload.datasourceId],
|
||||
state: payload.datasourceState,
|
||||
},
|
||||
}
|
||||
: state.datasourceStates,
|
||||
visualization: {
|
||||
...state.visualization,
|
||||
activeId: payload.newVisualizationId,
|
||||
state: payload.initialState,
|
||||
},
|
||||
stagedPreview: state.stagedPreview || {
|
||||
datasourceStates: state.datasourceStates,
|
||||
visualization: state.visualization,
|
||||
},
|
||||
};
|
||||
},
|
||||
rollbackSuggestion: (state) => {
|
||||
return {
|
||||
...state,
|
||||
...(state.stagedPreview || {}),
|
||||
stagedPreview: undefined,
|
||||
};
|
||||
},
|
||||
setToggleFullscreen: (state) => {
|
||||
return { ...state, isFullscreenDatasource: !state.isFullscreenDatasource };
|
||||
},
|
||||
submitSuggestion: (state) => {
|
||||
return {
|
||||
...state,
|
||||
stagedPreview: undefined,
|
||||
};
|
||||
},
|
||||
switchDatasource: (
|
||||
state,
|
||||
{
|
||||
payload,
|
||||
}: {
|
||||
payload: {
|
||||
newDatasourceId: string;
|
||||
};
|
||||
}
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
datasourceStates: {
|
||||
...state.datasourceStates,
|
||||
[payload.newDatasourceId]: state.datasourceStates[payload.newDatasourceId] || {
|
||||
state: null,
|
||||
isLoading: true,
|
||||
},
|
||||
},
|
||||
activeDatasourceId: payload.newDatasourceId,
|
||||
};
|
||||
},
|
||||
navigateAway: (state) => state,
|
||||
},
|
||||
});
|
||||
|
||||
export const reducer = {
|
||||
lens: lensSlice.reducer,
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { Dispatch, MiddlewareAPI, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { LensAppState } from './types';
|
||||
|
||||
/** cancels updates to the store that don't change the state */
|
||||
export const optimizingMiddleware = () => (store: MiddlewareAPI) => {
|
||||
return (next: Dispatch) => (action: PayloadAction<Partial<LensAppState>>) => {
|
||||
if (action.type === 'lens/onActiveDataChange') {
|
||||
if (isEqual(store.getState().lens.activeData, action.payload)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
next(action);
|
||||
};
|
||||
};
|
|
@ -17,10 +17,9 @@ import { timeRangeMiddleware } from './time_range_middleware';
|
|||
import { Observable, Subject } from 'rxjs';
|
||||
import { DataPublicPluginStart, esFilters } from '../../../../../src/plugins/data/public';
|
||||
import moment from 'moment';
|
||||
import { initialState } from './app_slice';
|
||||
import { initialState } from './lens_slice';
|
||||
import { LensAppState } from './types';
|
||||
import { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { Document } from '../persistence';
|
||||
|
||||
const sessionIdSubject = new Subject<string>();
|
||||
|
||||
|
@ -132,7 +131,7 @@ function makeDefaultData(): jest.Mocked<DataPublicPluginStart> {
|
|||
const createMiddleware = (data: DataPublicPluginStart) => {
|
||||
const middleware = timeRangeMiddleware(data);
|
||||
const store = {
|
||||
getState: jest.fn(() => ({ app: initialState })),
|
||||
getState: jest.fn(() => ({ lens: initialState })),
|
||||
dispatch: jest.fn(),
|
||||
};
|
||||
const next = jest.fn();
|
||||
|
@ -157,8 +156,13 @@ describe('timeRangeMiddleware', () => {
|
|||
});
|
||||
const { next, invoke, store } = createMiddleware(data);
|
||||
const action = {
|
||||
type: 'app/setState',
|
||||
payload: { lastKnownDoc: ('new' as unknown) as Document },
|
||||
type: 'lens/setState',
|
||||
payload: {
|
||||
visualization: {
|
||||
state: {},
|
||||
activeId: 'id2',
|
||||
},
|
||||
},
|
||||
};
|
||||
invoke(action);
|
||||
expect(store.dispatch).toHaveBeenCalledWith({
|
||||
|
@ -169,7 +173,7 @@ describe('timeRangeMiddleware', () => {
|
|||
},
|
||||
searchSessionId: 'sessionId-1',
|
||||
},
|
||||
type: 'app/setState',
|
||||
type: 'lens/setState',
|
||||
});
|
||||
expect(next).toHaveBeenCalledWith(action);
|
||||
});
|
||||
|
@ -187,8 +191,39 @@ describe('timeRangeMiddleware', () => {
|
|||
});
|
||||
const { next, invoke, store } = createMiddleware(data);
|
||||
const action = {
|
||||
type: 'app/setState',
|
||||
payload: { lastKnownDoc: ('new' as unknown) as Document },
|
||||
type: 'lens/setState',
|
||||
payload: {
|
||||
visualization: {
|
||||
state: {},
|
||||
activeId: 'id2',
|
||||
},
|
||||
},
|
||||
};
|
||||
invoke(action);
|
||||
expect(store.dispatch).not.toHaveBeenCalled();
|
||||
expect(next).toHaveBeenCalledWith(action);
|
||||
});
|
||||
it('does not trigger another update when the update already contains searchSessionId', () => {
|
||||
const data = makeDefaultData();
|
||||
(data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 30000));
|
||||
(data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({
|
||||
from: 'now-2m',
|
||||
to: 'now',
|
||||
});
|
||||
(data.query.timefilter.timefilter.getBounds as jest.Mock).mockReturnValue({
|
||||
min: moment(Date.now() - 100000),
|
||||
max: moment(Date.now() - 30000),
|
||||
});
|
||||
const { next, invoke, store } = createMiddleware(data);
|
||||
const action = {
|
||||
type: 'lens/setState',
|
||||
payload: {
|
||||
visualization: {
|
||||
state: {},
|
||||
activeId: 'id2',
|
||||
},
|
||||
searchSessionId: 'searchSessionId',
|
||||
},
|
||||
};
|
||||
invoke(action);
|
||||
expect(store.dispatch).not.toHaveBeenCalled();
|
||||
|
|
|
@ -5,27 +5,26 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
import { Dispatch, MiddlewareAPI, PayloadAction } from '@reduxjs/toolkit';
|
||||
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';
|
||||
|
||||
/**
|
||||
* checks if TIME_LAG_PERCENTAGE_LIMIT passed to renew searchSessionId
|
||||
* and request new data.
|
||||
*/
|
||||
export const timeRangeMiddleware = (data: DataPublicPluginStart) => (store: MiddlewareAPI) => {
|
||||
return (next: Dispatch) => (action: PayloadAction<Partial<LensAppState>>) => {
|
||||
// if document was modified or sessionId check if too much time passed to update searchSessionId
|
||||
if (
|
||||
action.payload?.lastKnownDoc &&
|
||||
!isEqual(action.payload?.lastKnownDoc, store.getState().app.lastKnownDoc)
|
||||
) {
|
||||
if (!action.payload?.searchSessionId) {
|
||||
updateTimeRange(data, store.dispatch);
|
||||
}
|
||||
next(action);
|
||||
};
|
||||
};
|
||||
|
||||
function updateTimeRange(data: DataPublicPluginStart, dispatch: LensDispatch) {
|
||||
const timefilter = data.query.timefilter.timefilter;
|
||||
const unresolvedTimeRange = timefilter.getTime();
|
||||
|
|
|
@ -5,24 +5,33 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Filter, IndexPattern, Query, SavedQuery } from '../../../../../src/plugins/data/public';
|
||||
import { Filter, Query, SavedQuery } from '../../../../../src/plugins/data/public';
|
||||
import { Document } from '../persistence';
|
||||
|
||||
import { TableInspectorAdapter } from '../editor_frame_service/types';
|
||||
import { DateRange } from '../../common';
|
||||
|
||||
export interface LensAppState {
|
||||
export interface PreviewState {
|
||||
visualization: {
|
||||
activeId: string | null;
|
||||
state: unknown;
|
||||
};
|
||||
datasourceStates: Record<string, { state: unknown; isLoading: boolean }>;
|
||||
}
|
||||
export interface EditorFrameState extends PreviewState {
|
||||
activeDatasourceId: string | null;
|
||||
stagedPreview?: PreviewState;
|
||||
isFullscreenDatasource?: boolean;
|
||||
}
|
||||
export interface LensAppState extends EditorFrameState {
|
||||
persistedDoc?: Document;
|
||||
lastKnownDoc?: Document;
|
||||
|
||||
// index patterns used to determine which filters are available in the top nav.
|
||||
indexPatternsForTopNav: IndexPattern[];
|
||||
// Determines whether the lens editor shows the 'save and return' button, and the originating app breadcrumb.
|
||||
isLinkedToOriginatingApp?: boolean;
|
||||
isSaveable: boolean;
|
||||
activeData?: TableInspectorAdapter;
|
||||
|
||||
isAppLoading: boolean;
|
||||
isLoading: boolean;
|
||||
query: Query;
|
||||
filters: Filter[];
|
||||
savedQuery?: SavedQuery;
|
||||
|
@ -38,5 +47,5 @@ export type DispatchSetState = (
|
|||
};
|
||||
|
||||
export interface LensState {
|
||||
app: LensAppState;
|
||||
lens: LensAppState;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { IconType } from '@elastic/eui/src/components/icon/icon';
|
||||
import { CoreSetup } from 'kibana/public';
|
||||
import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
|
||||
import { PaletteOutput } from 'src/plugins/charts/public';
|
||||
import { SavedObjectReference } from 'kibana/public';
|
||||
import { MutableRefObject } from 'react';
|
||||
import { RowClickContext } from '../../../../src/plugins/ui_actions/public';
|
||||
|
@ -45,13 +45,13 @@ export interface PublicAPIProps<T> {
|
|||
}
|
||||
|
||||
export interface EditorFrameProps {
|
||||
onError: ErrorCallback;
|
||||
initialContext?: VisualizeFieldContext;
|
||||
showNoDataPopover: () => void;
|
||||
}
|
||||
|
||||
export interface EditorFrameInstance {
|
||||
EditorFrameContainer: (props: EditorFrameProps) => React.ReactElement;
|
||||
datasourceMap: Record<string, Datasource>;
|
||||
visualizationMap: Record<string, Visualization>;
|
||||
}
|
||||
|
||||
export interface EditorFrameSetup {
|
||||
|
@ -525,20 +525,10 @@ export interface FramePublicAPI {
|
|||
* If accessing, make sure to check whether expected columns actually exist.
|
||||
*/
|
||||
activeData?: Record<string, Datatable>;
|
||||
|
||||
dateRange: DateRange;
|
||||
query: Query;
|
||||
filters: Filter[];
|
||||
searchSessionId: string;
|
||||
|
||||
/**
|
||||
* A map of all available palettes (keys being the ids).
|
||||
*/
|
||||
availablePalettes: PaletteRegistry;
|
||||
|
||||
// Adds a new layer. This has a side effect of updating the datasource state
|
||||
addNewLayer: () => string;
|
||||
removeLayers: (layerIds: string[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -586,7 +576,7 @@ export interface Visualization<T = unknown> {
|
|||
* - Loadingn from a saved visualization
|
||||
* - When using suggestions, the suggested state is passed in
|
||||
*/
|
||||
initialize: (frame: FramePublicAPI, state?: T, mainPalette?: PaletteOutput) => T;
|
||||
initialize: (addNewLayer: () => string, state?: T, mainPalette?: PaletteOutput) => T;
|
||||
|
||||
getMainPalette?: (state: T) => undefined | PaletteOutput;
|
||||
|
||||
|
|
|
@ -9,6 +9,12 @@ import { i18n } from '@kbn/i18n';
|
|||
import { IndexPattern, IndexPatternsContract, TimefilterContract } from 'src/plugins/data/public';
|
||||
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 } from './types';
|
||||
import { extractFilterReferences } from './persistence';
|
||||
|
||||
export function getVisualizeGeoFieldMessage(fieldType: string) {
|
||||
return i18n.translate('xpack.lens.visualizeGeoFieldMessage', {
|
||||
|
@ -32,7 +38,105 @@ export function containsDynamicMath(dateMathString: string) {
|
|||
|
||||
export const TIME_LAG_PERCENTAGE_LIMIT = 0.02;
|
||||
|
||||
export async function getAllIndexPatterns(
|
||||
export function getTimeZone(uiSettings: IUiSettingsClient) {
|
||||
const configuredTimeZone = uiSettings.get('dateFormat:tz');
|
||||
if (configuredTimeZone === 'Browser') {
|
||||
return moment.tz.guess();
|
||||
}
|
||||
|
||||
return configuredTimeZone;
|
||||
}
|
||||
export function getActiveDatasourceIdFromDoc(doc?: Document) {
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [firstDatasourceFromDoc] = Object.keys(doc.state.datasourceStates);
|
||||
return firstDatasourceFromDoc || null;
|
||||
}
|
||||
|
||||
export const getInitialDatasourceId = (
|
||||
datasourceMap: Record<string, Datasource>,
|
||||
doc?: Document
|
||||
) => {
|
||||
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 }>;
|
||||
}): string[] {
|
||||
const references: SavedObjectReference[] = [];
|
||||
Object.entries(activeDatasources).forEach(([id, datasource]) => {
|
||||
const { savedObjectReferences } = datasource.getPersistableState(datasourceStates[id].state);
|
||||
references.push(...savedObjectReferences);
|
||||
});
|
||||
|
||||
const uniqueFilterableIndexPatternIds = uniq(
|
||||
references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id)
|
||||
);
|
||||
|
||||
return uniqueFilterableIndexPatternIds;
|
||||
}
|
||||
|
||||
export async function getIndexPatternsObjects(
|
||||
ids: string[],
|
||||
indexPatternsService: IndexPatternsContract
|
||||
): Promise<{ indexPatterns: IndexPattern[]; rejectedIds: string[] }> {
|
||||
|
@ -46,12 +150,3 @@ export async function getAllIndexPatterns(
|
|||
// return also the rejected ids in case we want to show something later on
|
||||
return { indexPatterns: fullfilled.map((response) => response.value), rejectedIds };
|
||||
}
|
||||
|
||||
export function getTimeZone(uiSettings: IUiSettingsClient) {
|
||||
const configuredTimeZone = uiSettings.get('dateFormat:tz');
|
||||
if (configuredTimeZone === 'Browser') {
|
||||
return moment.tz.guess();
|
||||
}
|
||||
|
||||
return configuredTimeZone;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import { Position } from '@elastic/charts';
|
|||
import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
|
||||
import { getXyVisualization } from './xy_visualization';
|
||||
import { Operation } from '../types';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../mocks';
|
||||
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
|
||||
|
||||
describe('#toExpression', () => {
|
||||
|
|
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
import { shallowWithIntl as shallow } from '@kbn/test/jest';
|
||||
import { Position } from '@elastic/charts';
|
||||
import { FramePublicAPI } from '../../types';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../../editor_frame_service/mocks';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../../mocks';
|
||||
import { State } from '../types';
|
||||
import { VisualOptionsPopover } from './visual_options_popover';
|
||||
import { ToolbarPopover } from '../../shared_components';
|
||||
|
|
|
@ -9,7 +9,7 @@ import { getXyVisualization } from './visualization';
|
|||
import { Position } from '@elastic/charts';
|
||||
import { Operation } from '../types';
|
||||
import { State, SeriesType, XYLayerConfig } from './types';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../mocks';
|
||||
import { LensIconChartBar } from '../assets/chart_bar';
|
||||
import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
|
||||
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
|
||||
|
@ -132,8 +132,7 @@ describe('xy_visualization', () => {
|
|||
|
||||
describe('#initialize', () => {
|
||||
it('loads default state', () => {
|
||||
const mockFrame = createMockFramePublicAPI();
|
||||
const initialState = xyVisualization.initialize(mockFrame);
|
||||
const initialState = xyVisualization.initialize(() => 'l1');
|
||||
|
||||
expect(initialState.layers).toHaveLength(1);
|
||||
expect(initialState.layers[0].xAccessor).not.toBeDefined();
|
||||
|
@ -144,7 +143,7 @@ describe('xy_visualization', () => {
|
|||
"layers": Array [
|
||||
Object {
|
||||
"accessors": Array [],
|
||||
"layerId": "",
|
||||
"layerId": "l1",
|
||||
"position": "top",
|
||||
"seriesType": "bar_stacked",
|
||||
"showGridlines": false,
|
||||
|
@ -162,9 +161,7 @@ describe('xy_visualization', () => {
|
|||
});
|
||||
|
||||
it('loads from persisted state', () => {
|
||||
expect(xyVisualization.initialize(createMockFramePublicAPI(), exampleState())).toEqual(
|
||||
exampleState()
|
||||
);
|
||||
expect(xyVisualization.initialize(() => 'first', exampleState())).toEqual(exampleState());
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -152,7 +152,7 @@ export const getXyVisualization = ({
|
|||
|
||||
getSuggestions,
|
||||
|
||||
initialize(frame, state) {
|
||||
initialize(addNewLayer, state) {
|
||||
return (
|
||||
state || {
|
||||
title: 'Empty XY chart',
|
||||
|
@ -161,7 +161,7 @@ export const getXyVisualization = ({
|
|||
preferredSeriesType: defaultSeriesType,
|
||||
layers: [
|
||||
{
|
||||
layerId: frame.addNewLayer(),
|
||||
layerId: addNewLayer(),
|
||||
accessors: [],
|
||||
position: Position.Top,
|
||||
seriesType: defaultSeriesType,
|
||||
|
|
|
@ -13,7 +13,7 @@ import { AxisSettingsPopover } from './axis_settings_popover';
|
|||
import { FramePublicAPI } from '../types';
|
||||
import { State } from './types';
|
||||
import { Position } from '@elastic/charts';
|
||||
import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks';
|
||||
import { createMockFramePublicAPI, createMockDatasource } from '../mocks';
|
||||
import { chartPluginMock } from 'src/plugins/charts/public/mocks';
|
||||
import { EuiColorPicker } from '@elastic/eui';
|
||||
|
||||
|
|
|
@ -99,7 +99,6 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
|
|||
|
||||
{isSaveOpen && lensAttributes && (
|
||||
<LensSaveModalComponent
|
||||
isVisible={isSaveOpen}
|
||||
initialInput={(lensAttributes as unknown) as LensEmbeddableInput}
|
||||
onClose={() => setIsSaveOpen(false)}
|
||||
onSave={() => {}}
|
||||
|
|
|
@ -142,8 +142,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await PageObjects.lens.configureDimension(
|
||||
{
|
||||
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
|
||||
operation: 'terms',
|
||||
field: 'ip',
|
||||
operation: 'date_histogram',
|
||||
field: '@timestamp',
|
||||
},
|
||||
1
|
||||
);
|
||||
|
|
|
@ -223,10 +223,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
// remove the x dimension to trigger the validation error
|
||||
await PageObjects.lens.removeDimension('lnsXY_xDimensionPanel');
|
||||
await PageObjects.lens.saveAndReturn();
|
||||
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await testSubjects.existOrFail('embeddable-lens-failure');
|
||||
await PageObjects.lens.expectSaveAndReturnButtonDisabled();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -491,6 +491,12 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
await testSubjects.click('lnsApp_saveAndReturnButton');
|
||||
},
|
||||
|
||||
async expectSaveAndReturnButtonDisabled() {
|
||||
const button = await testSubjects.find('lnsApp_saveAndReturnButton', 10000);
|
||||
const disabledAttr = await button.getAttribute('disabled');
|
||||
expect(disabledAttr).to.be('true');
|
||||
},
|
||||
|
||||
async editDimensionLabel(label: string) {
|
||||
await testSubjects.setValue('indexPattern-label-edit', label, { clearWithKeyboard: true });
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue