mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Lens] Enables ad-hoc dataviews (#138732)
* [Lens] Enables ad-hoc dataviews * Fix bug in legacy field existence logic * lift ad hoc state to frame level and rename data view on incompatible change * fix some tests * migrate embedded ad hoc data views * fix tests and inject/extract ad hoc data view references * fix bugs and add functional tests * fix unit tests * do not show geo fields for ad hoc data views * Fix functional test * Refactor to use the new metric viz on the functional tests Co-authored-by: Joe Reuter <johannes.reuter@elastic.co>
This commit is contained in:
parent
d5cc164bd8
commit
255f24d595
51 changed files with 1080 additions and 353 deletions
|
@ -26,7 +26,7 @@ describe('filter manager persistable state tests', () => {
|
|||
const updatedFilters = inject(filters, [
|
||||
{ type: DATA_VIEW_SAVED_OBJECT_TYPE, name: 'test123', id: '123' },
|
||||
]);
|
||||
expect(updatedFilters[0]).toHaveProperty('meta.index', undefined);
|
||||
expect(updatedFilters[0]).toHaveProperty('meta.index', 'test');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -46,7 +46,8 @@ export const inject = (filters: Filter[], references: SavedObjectReference[]) =>
|
|||
...filter,
|
||||
meta: {
|
||||
...filter.meta,
|
||||
index: reference && reference.id,
|
||||
// if no reference has been found, keep the current "index" property (used for adhoc data views)
|
||||
index: reference ? reference.id : filter.meta.index,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -41,7 +41,7 @@ describe('query service persistable state tests', () => {
|
|||
const updatedQueryState = inject(queryState, [
|
||||
{ type: DATA_VIEW_SAVED_OBJECT_TYPE, name: 'test123', id: '123' },
|
||||
]);
|
||||
expect(updatedQueryState.filters[0]).toHaveProperty('meta.index', undefined);
|
||||
expect(updatedQueryState.filters[0]).toHaveProperty('meta.index', 'test');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -60,6 +60,7 @@ export const TextBasedLanguagesList = (props: TextBasedLanguagesListProps) => (
|
|||
export function ChangeDataView({
|
||||
isMissingCurrent,
|
||||
currentDataViewId,
|
||||
adHocDataViews,
|
||||
onChangeDataView,
|
||||
onAddField,
|
||||
onDataViewCreated,
|
||||
|
@ -93,10 +94,21 @@ export function ChangeDataView({
|
|||
useEffect(() => {
|
||||
const fetchDataViews = async () => {
|
||||
const dataViewsRefs = await data.dataViews.getIdsWithTitle();
|
||||
if (adHocDataViews?.length) {
|
||||
adHocDataViews.forEach((adHocDataView) => {
|
||||
if (adHocDataView.id) {
|
||||
dataViewsRefs.push({
|
||||
title: adHocDataView.title,
|
||||
name: adHocDataView.name,
|
||||
id: adHocDataView.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
setDataViewsList(dataViewsRefs);
|
||||
};
|
||||
fetchDataViews();
|
||||
}, [data, currentDataViewId]);
|
||||
}, [data, currentDataViewId, adHocDataViews]);
|
||||
|
||||
useEffect(() => {
|
||||
if (trigger.label) {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import type { EuiButtonProps, EuiSelectableProps } from '@elastic/eui';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { AggregateQuery, Query } from '@kbn/es-query';
|
||||
import { ChangeDataView } from './change_dataview';
|
||||
|
||||
|
@ -44,6 +45,10 @@ export interface DataViewPickerProps {
|
|||
* The id of the selected dataview.
|
||||
*/
|
||||
currentDataViewId?: string;
|
||||
/**
|
||||
* The adHocDataviews.
|
||||
*/
|
||||
adHocDataViews?: DataView[];
|
||||
/**
|
||||
* EuiSelectable properties.
|
||||
*/
|
||||
|
@ -84,6 +89,7 @@ export interface DataViewPickerPropsExtended extends DataViewPickerProps {
|
|||
export const DataViewPicker = ({
|
||||
isMissingCurrent,
|
||||
currentDataViewId,
|
||||
adHocDataViews,
|
||||
onChangeDataView,
|
||||
onAddField,
|
||||
onDataViewCreated,
|
||||
|
@ -102,6 +108,7 @@ export const DataViewPicker = ({
|
|||
onAddField={onAddField}
|
||||
onDataViewCreated={onDataViewCreated}
|
||||
trigger={trigger}
|
||||
adHocDataViews={adHocDataViews}
|
||||
selectableProps={selectableProps}
|
||||
textBasedLanguages={textBasedLanguages}
|
||||
onSaveTextLanguageQuery={onSaveTextLanguageQuery}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { FtrService } from '../ftr_provider_context';
|
|||
export class UnifiedSearchPageObject extends FtrService {
|
||||
private readonly retry = this.ctx.getService('retry');
|
||||
private readonly testSubjects = this.ctx.getService('testSubjects');
|
||||
private readonly find = this.ctx.getService('find');
|
||||
|
||||
public async switchDataView(switchButtonSelector: string, dataViewTitle: string) {
|
||||
await this.testSubjects.click(switchButtonSelector);
|
||||
|
@ -35,4 +36,28 @@ export class UnifiedSearchPageObject extends FtrService {
|
|||
|
||||
return visibleText;
|
||||
}
|
||||
|
||||
public async clickCreateNewDataView() {
|
||||
await this.retry.waitForWithTimeout('data create new to be visible', 15000, async () => {
|
||||
return await this.testSubjects.isDisplayed('dataview-create-new');
|
||||
});
|
||||
await this.testSubjects.click('dataview-create-new');
|
||||
await this.retry.waitForWithTimeout(
|
||||
'index pattern editor form to be visible',
|
||||
15000,
|
||||
async () => {
|
||||
return await (await this.find.byClassName('indexPatternEditor__form')).isDisplayed();
|
||||
}
|
||||
);
|
||||
await (await this.find.byClassName('indexPatternEditor__form')).click();
|
||||
}
|
||||
|
||||
public async createNewDataView(dataViewName: string, adHoc?: boolean) {
|
||||
await this.clickCreateNewDataView();
|
||||
await this.testSubjects.setValue('createIndexPatternTitleInput', dataViewName, {
|
||||
clearWithKeyboard: true,
|
||||
typeCharByChar: true,
|
||||
});
|
||||
await this.testSubjects.click(adHoc ? 'exploreIndexPatternButton' : 'saveIndexPatternButton');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1033,7 +1033,11 @@ describe('common utils', () => {
|
|||
].join('\n\n');
|
||||
|
||||
const extractedReferences = extractLensReferencesFromCommentString(
|
||||
makeLensEmbeddableFactory(() => ({}), {}),
|
||||
makeLensEmbeddableFactory(
|
||||
() => ({}),
|
||||
() => ({}),
|
||||
{}
|
||||
),
|
||||
commentString
|
||||
);
|
||||
|
||||
|
@ -1132,7 +1136,11 @@ describe('common utils', () => {
|
|||
].join('\n\n');
|
||||
|
||||
const updatedReferences = getOrUpdateLensReferences(
|
||||
makeLensEmbeddableFactory(() => ({}), {}),
|
||||
makeLensEmbeddableFactory(
|
||||
() => ({}),
|
||||
() => ({}),
|
||||
{}
|
||||
),
|
||||
newCommentString,
|
||||
{
|
||||
references: currentCommentReferences,
|
||||
|
|
|
@ -237,7 +237,11 @@ describe('comments migrations', () => {
|
|||
it('should remove time zone param from date histogram', () => {
|
||||
const migrations = createCommentsMigrations({
|
||||
persistableStateAttachmentTypeRegistry: new PersistableStateAttachmentTypeRegistry(),
|
||||
lensEmbeddableFactory: makeLensEmbeddableFactory(() => ({}), {}),
|
||||
lensEmbeddableFactory: makeLensEmbeddableFactory(
|
||||
() => ({}),
|
||||
() => ({}),
|
||||
{}
|
||||
),
|
||||
});
|
||||
|
||||
expect(migrations['7.14.0']).toBeDefined();
|
||||
|
@ -574,7 +578,11 @@ describe('comments migrations', () => {
|
|||
|
||||
const migrations = createCommentsMigrations({
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
lensEmbeddableFactory: makeLensEmbeddableFactory(() => ({}), {}),
|
||||
lensEmbeddableFactory: makeLensEmbeddableFactory(
|
||||
() => ({}),
|
||||
() => ({}),
|
||||
{}
|
||||
),
|
||||
});
|
||||
|
||||
it('migrates a persistable state attachment correctly', () => {
|
||||
|
|
|
@ -10,6 +10,7 @@ Array [
|
|||
"loadIndexPatternRefs": [Function],
|
||||
"loadIndexPatterns": [Function],
|
||||
"refreshExistingFields": [Function],
|
||||
"replaceDataViewId": [Function],
|
||||
"updateDataViewsState": [Function],
|
||||
},
|
||||
"lensInspector": Object {
|
||||
|
|
|
@ -146,7 +146,7 @@ describe('Lens App', () => {
|
|||
|
||||
it('updates global filters with store state', async () => {
|
||||
const services = makeDefaultServicesForApp();
|
||||
const indexPattern = { id: 'index1' } as unknown as DataView;
|
||||
const indexPattern = { id: 'index1', isPersisted: () => true } as unknown as DataView;
|
||||
const pinnedField = { name: 'pinnedField' } as unknown as FieldSpec;
|
||||
const pinnedFilter = buildExistsFilter(pinnedField, indexPattern);
|
||||
services.data.query.filterManager.getFilters = jest.fn().mockImplementation(() => {
|
||||
|
@ -348,7 +348,9 @@ describe('Lens App', () => {
|
|||
const customServices = makeDefaultServicesForApp();
|
||||
customServices.dataViews.get = jest
|
||||
.fn()
|
||||
.mockImplementation((id) => Promise.resolve({ id, isTimeBased: () => true } as DataView));
|
||||
.mockImplementation((id) =>
|
||||
Promise.resolve({ id, isTimeBased: () => true, isPersisted: () => true } as DataView)
|
||||
);
|
||||
const { services } = await mountWith({ services: customServices });
|
||||
expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ showDatePicker: true }),
|
||||
|
@ -359,7 +361,9 @@ describe('Lens App', () => {
|
|||
const customServices = makeDefaultServicesForApp();
|
||||
customServices.dataViews.get = jest
|
||||
.fn()
|
||||
.mockImplementation((id) => Promise.resolve({ id, isTimeBased: () => true } as DataView));
|
||||
.mockImplementation((id) =>
|
||||
Promise.resolve({ id, isTimeBased: () => true, isPersisted: () => true } as DataView)
|
||||
);
|
||||
const customProps = makeDefaultProps();
|
||||
customProps.datasourceMap.testDatasource.isTimeBased = () => true;
|
||||
const { services } = await mountWith({ props: customProps, services: customServices });
|
||||
|
@ -372,7 +376,9 @@ describe('Lens App', () => {
|
|||
const customServices = makeDefaultServicesForApp();
|
||||
customServices.dataViews.get = jest
|
||||
.fn()
|
||||
.mockImplementation((id) => Promise.resolve({ id, isTimeBased: () => true } as DataView));
|
||||
.mockImplementation((id) =>
|
||||
Promise.resolve({ id, isTimeBased: () => true, isPersisted: () => true } as DataView)
|
||||
);
|
||||
const customProps = makeDefaultProps();
|
||||
customProps.datasourceMap.testDatasource.isTimeBased = () => false;
|
||||
const { services } = await mountWith({ props: customProps, services: customServices });
|
||||
|
@ -477,7 +483,14 @@ describe('Lens App', () => {
|
|||
expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
query: 'fake query',
|
||||
indexPatterns: [{ id: 'mockip', isTimeBased: expect.any(Function), fields: [] }],
|
||||
indexPatterns: [
|
||||
{
|
||||
id: 'mockip',
|
||||
isTimeBased: expect.any(Function),
|
||||
fields: [],
|
||||
isPersisted: expect.any(Function),
|
||||
},
|
||||
],
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
@ -823,7 +836,7 @@ describe('Lens App', () => {
|
|||
});
|
||||
|
||||
it('saves app filters and does not save pinned filters', async () => {
|
||||
const indexPattern = { id: 'index1' } as unknown as DataView;
|
||||
const indexPattern = { id: 'index1', isPersisted: () => true } as unknown as DataView;
|
||||
const field = { name: 'myfield' } as unknown as FieldSpec;
|
||||
const pinnedField = { name: 'pinnedField' } as unknown as FieldSpec;
|
||||
const unpinned = buildExistsFilter(field, indexPattern);
|
||||
|
@ -1032,7 +1045,7 @@ describe('Lens App', () => {
|
|||
|
||||
it('updates the filters when the user changes them', async () => {
|
||||
const { instance, services, lensStore } = await mountWith({});
|
||||
const indexPattern = { id: 'index1' } as unknown as DataView;
|
||||
const indexPattern = { id: 'index1', isPersisted: () => true } as unknown as DataView;
|
||||
const field = { name: 'myfield' } as unknown as FieldSpec;
|
||||
expect(lensStore.getState()).toEqual({
|
||||
lens: expect.objectContaining({
|
||||
|
@ -1085,7 +1098,7 @@ describe('Lens App', () => {
|
|||
searchSessionId: `sessionId-3`,
|
||||
}),
|
||||
});
|
||||
const indexPattern = { id: 'index1' } as unknown as DataView;
|
||||
const indexPattern = { id: 'index1', isPersisted: () => true } as unknown as DataView;
|
||||
const field = { name: 'myfield' } as unknown as FieldSpec;
|
||||
act(() =>
|
||||
services.data.query.filterManager.setFilters([buildExistsFilter(field, indexPattern)])
|
||||
|
@ -1218,7 +1231,7 @@ describe('Lens App', () => {
|
|||
query: { query: 'new', language: 'lucene' },
|
||||
})
|
||||
);
|
||||
const indexPattern = { id: 'index1' } as unknown as DataView;
|
||||
const indexPattern = { id: 'index1', isPersisted: () => true } as unknown as DataView;
|
||||
const field = { name: 'myfield' } as unknown as FieldSpec;
|
||||
const pinnedField = { name: 'pinnedField' } as unknown as FieldSpec;
|
||||
const unpinned = buildExistsFilter(field, indexPattern);
|
||||
|
@ -1275,7 +1288,7 @@ describe('Lens App', () => {
|
|||
query: { query: 'new', language: 'lucene' },
|
||||
})
|
||||
);
|
||||
const indexPattern = { id: 'index1' } as unknown as DataView;
|
||||
const indexPattern = { id: 'index1', isPersisted: () => true } as unknown as DataView;
|
||||
const field = { name: 'myfield' } as unknown as FieldSpec;
|
||||
const pinnedField = { name: 'pinnedField' } as unknown as FieldSpec;
|
||||
const unpinned = buildExistsFilter(field, indexPattern);
|
||||
|
|
|
@ -32,6 +32,7 @@ import { LensInspector } from '../lens_inspector_service';
|
|||
import { getEditPath } from '../../common';
|
||||
import { isLensEqual } from './lens_document_equality';
|
||||
import { IndexPatternServiceAPI, createIndexPatternService } from '../indexpattern_service/service';
|
||||
import { replaceIndexpattern } from '../state_management/lens_slice';
|
||||
|
||||
export type SaveProps = Omit<OnSaveProps, 'onTitleDuplicate' | 'newDescription'> & {
|
||||
returnToOrigin: boolean;
|
||||
|
@ -368,6 +369,7 @@ export function App({
|
|||
createIndexPatternService({
|
||||
dataViews: lensAppServices.dataViews,
|
||||
uiSettings: lensAppServices.uiSettings,
|
||||
uiActions: lensAppServices.uiActions,
|
||||
core: { http, notifications },
|
||||
updateIndexPatterns: (newIndexPatternsState, options) => {
|
||||
dispatch(updateIndexPatterns(newIndexPatternsState));
|
||||
|
@ -375,6 +377,12 @@ export function App({
|
|||
dispatch(applyChanges());
|
||||
}
|
||||
},
|
||||
replaceIndexPattern: (newIndexPattern, oldId, options) => {
|
||||
dispatch(replaceIndexpattern({ newIndexPattern, oldId }));
|
||||
if (options?.applyImmediately) {
|
||||
dispatch(applyChanges());
|
||||
}
|
||||
},
|
||||
}),
|
||||
[dispatch, http, notifications, lensAppServices]
|
||||
);
|
||||
|
|
|
@ -54,9 +54,9 @@ export const isLensEqual = (
|
|||
.map((type) =>
|
||||
datasourceMap[type].isEqual(
|
||||
doc1.state.datasourceStates[type],
|
||||
doc1.references,
|
||||
[...doc1.references, ...(doc1.state.internalReferences || [])],
|
||||
doc2.state.datasourceStates[type],
|
||||
doc2.references
|
||||
[...doc2.references, ...(doc2.state.internalReferences || [])]
|
||||
)
|
||||
)
|
||||
.every((res) => res);
|
||||
|
|
|
@ -251,23 +251,20 @@ export const LensTopNavMenu = ({
|
|||
(state: Partial<LensAppState>) => dispatch(setState(state)),
|
||||
[dispatch]
|
||||
);
|
||||
const [indexPatterns, setIndexPatterns] = useState<DataView[]>([]);
|
||||
const [currentIndexPattern, setCurrentIndexPattern] = useState<DataView>();
|
||||
const [rejectedIndexPatterns, setRejectedIndexPatterns] = useState<string[]>([]);
|
||||
|
||||
const dispatchChangeIndexPattern = React.useCallback(
|
||||
async (indexPatternId) => {
|
||||
const [newIndexPatternRefs, newIndexPatterns] = await Promise.all([
|
||||
// Reload refs in case it's a new indexPattern created on the spot
|
||||
dataViews.indexPatternRefs[indexPatternId]
|
||||
? dataViews.indexPatternRefs
|
||||
: indexPatternService.loadIndexPatternRefs({
|
||||
isFullEditor: true,
|
||||
}),
|
||||
indexPatternService.ensureIndexPattern({
|
||||
id: indexPatternId,
|
||||
cache: dataViews.indexPatterns,
|
||||
}),
|
||||
]);
|
||||
async (dataViewOrId: DataView | string) => {
|
||||
const indexPatternId = typeof dataViewOrId === 'string' ? dataViewOrId : dataViewOrId.id!;
|
||||
const newIndexPatterns = await indexPatternService.ensureIndexPattern({
|
||||
id: indexPatternId,
|
||||
cache: dataViews.indexPatterns,
|
||||
});
|
||||
dispatch(
|
||||
changeIndexPattern({
|
||||
dataViews: { indexPatterns: newIndexPatterns, indexPatternRefs: newIndexPatternRefs },
|
||||
dataViews: { indexPatterns: newIndexPatterns },
|
||||
datasourceIds: Object.keys(datasourceStates),
|
||||
visualizationIds: visualization.activeId ? [visualization.activeId] : [],
|
||||
indexPatternId,
|
||||
|
@ -275,7 +272,6 @@ export const LensTopNavMenu = ({
|
|||
);
|
||||
},
|
||||
[
|
||||
dataViews.indexPatternRefs,
|
||||
dataViews.indexPatterns,
|
||||
datasourceStates,
|
||||
dispatch,
|
||||
|
@ -284,9 +280,6 @@ export const LensTopNavMenu = ({
|
|||
]
|
||||
);
|
||||
|
||||
const [indexPatterns, setIndexPatterns] = useState<DataView[]>([]);
|
||||
const [currentIndexPattern, setCurrentIndexPattern] = useState<DataView>();
|
||||
const [rejectedIndexPatterns, setRejectedIndexPatterns] = useState<string[]>([]);
|
||||
const canEditDataView = Boolean(dataViewEditor?.userPermissions.editDataView());
|
||||
const closeFieldEditor = useRef<() => void | undefined>();
|
||||
const closeDataViewEditor = useRef<() => void | undefined>();
|
||||
|
@ -301,19 +294,28 @@ export const LensTopNavMenu = ({
|
|||
if (!activeDatasource) {
|
||||
return;
|
||||
}
|
||||
const indexPatternIds = getIndexPatternsIds({
|
||||
activeDatasources: Object.keys(datasourceStates).reduce(
|
||||
(acc, datasourceId) => ({
|
||||
...acc,
|
||||
[datasourceId]: datasourceMap[datasourceId],
|
||||
}),
|
||||
{}
|
||||
),
|
||||
datasourceStates,
|
||||
});
|
||||
const indexPatternIds = new Set(
|
||||
getIndexPatternsIds({
|
||||
activeDatasources: Object.keys(datasourceStates).reduce(
|
||||
(acc, datasourceId) => ({
|
||||
...acc,
|
||||
[datasourceId]: datasourceMap[datasourceId],
|
||||
}),
|
||||
{}
|
||||
),
|
||||
datasourceStates,
|
||||
})
|
||||
);
|
||||
// Add ad-hoc data views from the Lens state even if they are not used
|
||||
Object.values(dataViews.indexPatterns)
|
||||
.filter((indexPattern) => indexPattern.spec)
|
||||
.forEach((indexPattern) => {
|
||||
indexPatternIds.add(indexPattern.id);
|
||||
});
|
||||
|
||||
const hasIndexPatternsChanged =
|
||||
indexPatterns.length + rejectedIndexPatterns.length !== indexPatternIds.length ||
|
||||
indexPatternIds.some(
|
||||
indexPatterns.length + rejectedIndexPatterns.length !== indexPatternIds.size ||
|
||||
[...indexPatternIds].some(
|
||||
(id) =>
|
||||
![...indexPatterns.map((ip) => ip.id), ...rejectedIndexPatterns].find(
|
||||
(loadedId) => loadedId === id
|
||||
|
@ -322,7 +324,7 @@ export const LensTopNavMenu = ({
|
|||
|
||||
// Update the cached index patterns if the user made a change to any of them
|
||||
if (hasIndexPatternsChanged) {
|
||||
getIndexPatternsObjects(indexPatternIds, dataViewsService).then(
|
||||
getIndexPatternsObjects([...indexPatternIds], dataViewsService).then(
|
||||
({ indexPatterns: indexPatternObjects, rejectedIds }) => {
|
||||
setIndexPatterns(indexPatternObjects);
|
||||
setRejectedIndexPatterns(rejectedIds);
|
||||
|
@ -336,6 +338,7 @@ export const LensTopNavMenu = ({
|
|||
datasourceMap,
|
||||
indexPatterns,
|
||||
dataViewsService,
|
||||
dataViews,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -675,8 +678,12 @@ export const LensTopNavMenu = ({
|
|||
dataView: indexPatternInstance,
|
||||
},
|
||||
fieldName,
|
||||
onSave: async () => {
|
||||
refreshFieldList();
|
||||
onSave: () => {
|
||||
if (indexPatternInstance.isPersisted()) {
|
||||
refreshFieldList();
|
||||
} else {
|
||||
indexPatternService.replaceDataViewId(indexPatternInstance);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -687,6 +694,7 @@ export const LensTopNavMenu = ({
|
|||
currentIndexPattern?.id,
|
||||
data.dataViews,
|
||||
dataViewFieldEditor,
|
||||
indexPatternService,
|
||||
refreshFieldList,
|
||||
]
|
||||
);
|
||||
|
@ -703,14 +711,15 @@ export const LensTopNavMenu = ({
|
|||
closeDataViewEditor.current = dataViewEditor.openEditor({
|
||||
onSave: async (dataView) => {
|
||||
if (dataView.id) {
|
||||
dispatchChangeIndexPattern(dataView.id);
|
||||
refreshFieldList();
|
||||
dispatchChangeIndexPattern(dataView);
|
||||
setCurrentIndexPattern(dataView);
|
||||
}
|
||||
},
|
||||
allowAdHocDataView: true,
|
||||
});
|
||||
}
|
||||
: undefined,
|
||||
[canEditDataView, dataViewEditor, dispatchChangeIndexPattern, refreshFieldList]
|
||||
[canEditDataView, dataViewEditor, dispatchChangeIndexPattern]
|
||||
);
|
||||
|
||||
const dataViewPickerProps = {
|
||||
|
@ -722,6 +731,7 @@ export const LensTopNavMenu = ({
|
|||
currentDataViewId: currentIndexPattern?.id,
|
||||
onAddField: addField,
|
||||
onDataViewCreated: createNewDataView,
|
||||
adHocDataViews: indexPatterns.filter((pattern) => !pattern.isPersisted()),
|
||||
onChangeDataView: (newIndexPatternId: string) => {
|
||||
const currentDataView = indexPatterns.find(
|
||||
(indexPattern) => indexPattern.id === newIndexPatternId
|
||||
|
|
|
@ -86,6 +86,7 @@ export async function getLensServices(
|
|||
attributeService,
|
||||
executionContext: coreStart.executionContext,
|
||||
http: coreStart.http,
|
||||
uiActions: startDependencies.uiActions,
|
||||
chrome: coreStart.chrome,
|
||||
overlays: coreStart.overlays,
|
||||
uiSettings: coreStart.uiSettings,
|
||||
|
|
|
@ -32,7 +32,11 @@ import type { DashboardFeatureFlagConfig } from '@kbn/dashboard-plugin/public';
|
|||
import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public';
|
||||
import type { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public';
|
||||
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
|
||||
import { VisualizeFieldContext, ACTION_VISUALIZE_LENS_FIELD } from '@kbn/ui-actions-plugin/public';
|
||||
import {
|
||||
VisualizeFieldContext,
|
||||
ACTION_VISUALIZE_LENS_FIELD,
|
||||
UiActionsStart,
|
||||
} from '@kbn/ui-actions-plugin/public';
|
||||
import { ACTION_CONVERT_TO_LENS } from '@kbn/visualizations-plugin/public';
|
||||
import type { EmbeddableEditorState, EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public';
|
||||
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
|
||||
|
@ -130,6 +134,7 @@ export interface LensAppServices {
|
|||
data: DataPublicPluginStart;
|
||||
inspector: LensInspector;
|
||||
uiSettings: IUiSettingsClient;
|
||||
uiActions: UiActionsStart;
|
||||
application: ApplicationStart;
|
||||
notifications: NotificationsStart;
|
||||
usageCollection?: UsageCollectionStart;
|
||||
|
|
|
@ -10,8 +10,9 @@ import { Ast } from '@kbn/interpreter';
|
|||
import memoizeOne from 'memoize-one';
|
||||
import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
|
||||
import { difference } from 'lodash';
|
||||
import type { DataViewsContract } from '@kbn/data-views-plugin/public';
|
||||
import type { DataViewsContract, DataViewSpec } from '@kbn/data-views-plugin/public';
|
||||
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
|
||||
import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common';
|
||||
import {
|
||||
Datasource,
|
||||
DatasourceLayers,
|
||||
|
@ -43,7 +44,8 @@ import { loadIndexPatternRefs, loadIndexPatterns } from '../../indexpattern_serv
|
|||
function getIndexPatterns(
|
||||
references?: SavedObjectReference[],
|
||||
initialContext?: VisualizeFieldContext | VisualizeEditorContext,
|
||||
initialId?: string
|
||||
initialId?: string,
|
||||
adHocDataviews?: string[]
|
||||
) {
|
||||
const indexPatternIds = [];
|
||||
if (initialContext) {
|
||||
|
@ -67,6 +69,9 @@ function getIndexPatterns(
|
|||
}
|
||||
}
|
||||
}
|
||||
if (adHocDataviews) {
|
||||
indexPatternIds.push(...adHocDataviews);
|
||||
}
|
||||
return [...new Set(indexPatternIds)];
|
||||
}
|
||||
|
||||
|
@ -87,6 +92,7 @@ export async function initializeDataViews(
|
|||
defaultIndexPatternId,
|
||||
references,
|
||||
initialContext,
|
||||
adHocDataViews: persistedAdHocDataViews,
|
||||
}: {
|
||||
dataViews: DataViewsContract;
|
||||
datasourceMap: DatasourceMap;
|
||||
|
@ -95,13 +101,20 @@ export async function initializeDataViews(
|
|||
storage: IStorageWrapper;
|
||||
references?: SavedObjectReference[];
|
||||
initialContext?: VisualizeFieldContext | VisualizeEditorContext;
|
||||
adHocDataViews?: Record<string, DataViewSpec>;
|
||||
},
|
||||
options?: InitializationOptions
|
||||
) {
|
||||
const adHocDataViews = Object.fromEntries(
|
||||
Object.entries(persistedAdHocDataViews || {}).map(([id, persistedSpec]) => {
|
||||
const spec = DataViewPersistableStateService.inject(persistedSpec, references || []);
|
||||
return [id, spec];
|
||||
})
|
||||
);
|
||||
const { isFullEditor } = options ?? {};
|
||||
// make it explicit or TS will infer never[] and break few lines down
|
||||
const indexPatternRefs: IndexPatternRef[] = await (isFullEditor
|
||||
? loadIndexPatternRefs(dataViews)
|
||||
? loadIndexPatternRefs(dataViews, adHocDataViews)
|
||||
: []);
|
||||
|
||||
// if no state is available, use the fallbackId
|
||||
|
@ -113,7 +126,14 @@ export async function initializeDataViews(
|
|||
? fallbackId
|
||||
: undefined;
|
||||
|
||||
const usedIndexPatterns = getIndexPatterns(references, initialContext, initialId);
|
||||
const adHocDataviewsIds: string[] = Object.keys(adHocDataViews || {});
|
||||
|
||||
const usedIndexPatterns = getIndexPatterns(
|
||||
references,
|
||||
initialContext,
|
||||
initialId,
|
||||
adHocDataviewsIds
|
||||
);
|
||||
|
||||
// load them
|
||||
const availableIndexPatterns = new Set(indexPatternRefs.map(({ id }: IndexPatternRef) => id));
|
||||
|
@ -125,6 +145,7 @@ export async function initializeDataViews(
|
|||
patterns: usedIndexPatterns,
|
||||
notUsedPatterns,
|
||||
cache: {},
|
||||
adHocDataViews,
|
||||
});
|
||||
|
||||
return { indexPatternRefs, indexPatterns };
|
||||
|
@ -142,6 +163,7 @@ export async function initializeSources(
|
|||
defaultIndexPatternId,
|
||||
references,
|
||||
initialContext,
|
||||
adHocDataViews,
|
||||
}: {
|
||||
dataViews: DataViewsContract;
|
||||
datasourceMap: DatasourceMap;
|
||||
|
@ -150,6 +172,7 @@ export async function initializeSources(
|
|||
storage: IStorageWrapper;
|
||||
references?: SavedObjectReference[];
|
||||
initialContext?: VisualizeFieldContext | VisualizeEditorContext;
|
||||
adHocDataViews?: Record<string, DataViewSpec>;
|
||||
},
|
||||
options?: InitializationOptions
|
||||
) {
|
||||
|
@ -162,6 +185,7 @@ export async function initializeSources(
|
|||
storage,
|
||||
defaultIndexPatternId,
|
||||
references,
|
||||
adHocDataViews,
|
||||
},
|
||||
options
|
||||
);
|
||||
|
@ -246,7 +270,12 @@ export async function persistedStateToExpression(
|
|||
}
|
||||
): Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }> {
|
||||
const {
|
||||
state: { visualization: visualizationState, datasourceStates: persistedDatasourceStates },
|
||||
state: {
|
||||
visualization: visualizationState,
|
||||
datasourceStates: persistedDatasourceStates,
|
||||
adHocDataViews,
|
||||
internalReferences,
|
||||
},
|
||||
visualizationType,
|
||||
references,
|
||||
title,
|
||||
|
@ -279,13 +308,14 @@ export async function persistedStateToExpression(
|
|||
dataViews: services.dataViews,
|
||||
storage: services.storage,
|
||||
defaultIndexPatternId: services.uiSettings.get('defaultIndex'),
|
||||
adHocDataViews,
|
||||
},
|
||||
{ isFullEditor: false }
|
||||
);
|
||||
const datasourceStates = initializeDatasources({
|
||||
datasourceMap,
|
||||
datasourceStates: datasourceStatesFromSO,
|
||||
references,
|
||||
references: [...references, ...(internalReferences || [])],
|
||||
indexPatterns,
|
||||
indexPatternRefs,
|
||||
});
|
||||
|
|
|
@ -55,6 +55,7 @@ import type {
|
|||
} from '@kbn/core/public';
|
||||
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
||||
import { BrushTriggerEvent, ClickTriggerEvent } from '@kbn/charts-plugin/public';
|
||||
import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common';
|
||||
import { getExecutionContextEvents, trackUiCounterEvents } from '../lens_ui_telemetry';
|
||||
import { Document } from '../persistence';
|
||||
import { ExpressionWrapper, ExpressionWrapperProps } from './expression_wrapper';
|
||||
|
@ -770,8 +771,20 @@ export class Embeddable
|
|||
|
||||
this.activeDataInfo.activeDatasource = this.deps.datasourceMap[activeDatasourceId];
|
||||
const docDatasourceState = this.savedVis?.state.datasourceStates[activeDatasourceId];
|
||||
const adHocDataviews = await Promise.all(
|
||||
Object.values(this.savedVis?.state.adHocDataViews || {})
|
||||
.map((persistedSpec) => {
|
||||
return DataViewPersistableStateService.inject(
|
||||
persistedSpec,
|
||||
this.savedVis?.references || []
|
||||
);
|
||||
})
|
||||
.map((spec) => this.deps.dataViews.create(spec))
|
||||
);
|
||||
|
||||
const indexPatternsCache = this.indexPatterns.reduce(
|
||||
const allIndexPatterns = [...this.indexPatterns, ...adHocDataviews];
|
||||
|
||||
const indexPatternsCache = allIndexPatterns.reduce(
|
||||
(acc, indexPattern) => ({
|
||||
[indexPattern.id!]: convertDataViewIntoLensIndexPattern(indexPattern),
|
||||
...acc,
|
||||
|
@ -782,7 +795,7 @@ export class Embeddable
|
|||
if (!this.activeDataInfo.activeDatasourceState) {
|
||||
this.activeDataInfo.activeDatasourceState = this.activeDataInfo.activeDatasource.initialize(
|
||||
docDatasourceState,
|
||||
this.savedVis?.references,
|
||||
[...(this.savedVis?.references || []), ...(this.savedVis?.state.internalReferences || [])],
|
||||
undefined,
|
||||
undefined,
|
||||
indexPatternsCache
|
||||
|
@ -831,6 +844,13 @@ export class Embeddable
|
|||
this.savedVis?.references.map(({ id }) => id) || [],
|
||||
this.deps.dataViews
|
||||
);
|
||||
(
|
||||
await Promise.all(
|
||||
Object.values(this.savedVis?.state.adHocDataViews || {}).map((spec) =>
|
||||
this.deps.dataViews.create(spec)
|
||||
)
|
||||
)
|
||||
).forEach((dataView) => indexPatterns.push(dataView));
|
||||
|
||||
this.indexPatterns = uniqBy(indexPatterns, 'id');
|
||||
|
||||
|
|
|
@ -1,93 +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 { i18n } from '@kbn/i18n';
|
||||
import React, { useState } from 'react';
|
||||
import { EuiPopover, EuiPopoverTitle, EuiSelectableProps } from '@elastic/eui';
|
||||
import { ToolbarButton, ToolbarButtonProps } from '@kbn/kibana-react-plugin/public';
|
||||
import { DataViewsList } from '@kbn/unified-search-plugin/public';
|
||||
import type { IndexPatternRef } from '../types';
|
||||
|
||||
export type ChangeIndexPatternTriggerProps = ToolbarButtonProps & {
|
||||
label: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export function ChangeIndexPattern({
|
||||
indexPatternRefs,
|
||||
isMissingCurrent,
|
||||
indexPatternId,
|
||||
onChangeIndexPattern,
|
||||
trigger,
|
||||
selectableProps,
|
||||
}: {
|
||||
trigger: ChangeIndexPatternTriggerProps;
|
||||
indexPatternRefs: IndexPatternRef[];
|
||||
isMissingCurrent?: boolean;
|
||||
onChangeIndexPattern: (newId: string) => void;
|
||||
indexPatternId?: string;
|
||||
selectableProps?: EuiSelectableProps;
|
||||
}) {
|
||||
const [isPopoverOpen, setPopoverIsOpen] = useState(false);
|
||||
|
||||
// be careful to only add color with a value, otherwise it will fallbacks to "primary"
|
||||
const colorProp = isMissingCurrent
|
||||
? {
|
||||
color: 'danger' as const,
|
||||
}
|
||||
: {};
|
||||
|
||||
const createTrigger = function () {
|
||||
const { label, title, ...rest } = trigger;
|
||||
return (
|
||||
<ToolbarButton
|
||||
title={title}
|
||||
onClick={() => setPopoverIsOpen(!isPopoverOpen)}
|
||||
fullWidth
|
||||
{...colorProp}
|
||||
{...rest}
|
||||
>
|
||||
{label}
|
||||
</ToolbarButton>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPopover
|
||||
panelClassName="lnsChangeIndexPatternPopover"
|
||||
button={createTrigger()}
|
||||
panelProps={{
|
||||
['data-test-subj']: 'lnsChangeIndexPatternPopover',
|
||||
}}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => setPopoverIsOpen(false)}
|
||||
display="block"
|
||||
panelPaddingSize="none"
|
||||
ownFocus
|
||||
>
|
||||
<div>
|
||||
<EuiPopoverTitle paddingSize="s">
|
||||
{i18n.translate('xpack.lens.indexPattern.changeDataViewTitle', {
|
||||
defaultMessage: 'Data view',
|
||||
})}
|
||||
</EuiPopoverTitle>
|
||||
|
||||
<DataViewsList
|
||||
dataViewsList={indexPatternRefs}
|
||||
onChangeDataView={(newId) => {
|
||||
onChangeIndexPattern(newId);
|
||||
setPopoverIsOpen(false);
|
||||
}}
|
||||
currentDataViewId={indexPatternId}
|
||||
selectableProps={selectableProps}
|
||||
/>
|
||||
</div>
|
||||
</EuiPopover>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -307,9 +307,15 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
const currentIndexPattern = indexPatterns[currentIndexPatternId];
|
||||
const existingFieldsForIndexPattern = existingFields[currentIndexPattern?.title];
|
||||
const visualizeGeoFieldTrigger = uiActions.getTrigger(VISUALIZE_GEO_FIELD_TRIGGER);
|
||||
const allFields = visualizeGeoFieldTrigger
|
||||
? currentIndexPattern.fields
|
||||
: currentIndexPattern.fields.filter(({ type }) => type !== 'geo_point' && type !== 'geo_shape');
|
||||
const allFields = useMemo(
|
||||
() =>
|
||||
visualizeGeoFieldTrigger && !currentIndexPattern.spec
|
||||
? currentIndexPattern.fields
|
||||
: currentIndexPattern.fields.filter(
|
||||
({ type }) => type !== 'geo_point' && type !== 'geo_shape'
|
||||
),
|
||||
[currentIndexPattern.fields, currentIndexPattern.spec, visualizeGeoFieldTrigger]
|
||||
);
|
||||
const clearLocalState = () => setLocalState((s) => ({ ...s, nameFilter: '', typeFilter: [] }));
|
||||
const availableFieldTypes = uniq(allFields.map(({ type }) => type)).filter(
|
||||
(type) => type in fieldTypeNames
|
||||
|
@ -523,11 +529,24 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
dataView: indexPatternInstance,
|
||||
},
|
||||
fieldName,
|
||||
onSave: () => refreshFieldList(),
|
||||
onSave: () => {
|
||||
if (indexPatternInstance.isPersisted()) {
|
||||
refreshFieldList();
|
||||
} else {
|
||||
indexPatternService.replaceDataViewId(indexPatternInstance);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
: undefined,
|
||||
[editPermission, dataViews, currentIndexPattern.id, indexPatternFieldEditor, refreshFieldList]
|
||||
[
|
||||
editPermission,
|
||||
dataViews,
|
||||
currentIndexPattern.id,
|
||||
indexPatternFieldEditor,
|
||||
refreshFieldList,
|
||||
indexPatternService,
|
||||
]
|
||||
);
|
||||
|
||||
const removeField = useMemo(
|
||||
|
@ -540,11 +559,24 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
dataView: indexPatternInstance,
|
||||
},
|
||||
fieldName,
|
||||
onDelete: () => refreshFieldList(),
|
||||
onDelete: () => {
|
||||
if (indexPatternInstance.isPersisted()) {
|
||||
refreshFieldList();
|
||||
} else {
|
||||
indexPatternService.replaceDataViewId(indexPatternInstance);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
: undefined,
|
||||
[currentIndexPattern.id, dataViews, editPermission, indexPatternFieldEditor, refreshFieldList]
|
||||
[
|
||||
currentIndexPattern.id,
|
||||
dataViews,
|
||||
editPermission,
|
||||
indexPatternFieldEditor,
|
||||
indexPatternService,
|
||||
refreshFieldList,
|
||||
]
|
||||
);
|
||||
|
||||
const fieldProps = useMemo(
|
||||
|
|
|
@ -41,6 +41,7 @@ import {
|
|||
injectReferences,
|
||||
loadInitialState,
|
||||
onRefreshIndexPattern,
|
||||
renameIndexPattern,
|
||||
triggerActionOnIndexPatternChange,
|
||||
} from './loader';
|
||||
import { toExpression } from './to_expression';
|
||||
|
@ -464,6 +465,9 @@ export function getIndexPatternDatasource({
|
|||
}
|
||||
return changeIndexPattern({ indexPatternId, state, storage, indexPatterns });
|
||||
},
|
||||
onIndexPatternRename: (state, oldIndexPatternId, newIndexPatternId) => {
|
||||
return renameIndexPattern({ state, oldIndexPatternId, newIndexPatternId });
|
||||
},
|
||||
getRenderEventCounters(state: IndexPatternPrivateState): string[] {
|
||||
const additionalEvents = {
|
||||
time_shift: false,
|
||||
|
@ -550,15 +554,18 @@ export function getIndexPatternDatasource({
|
|||
}
|
||||
return null;
|
||||
},
|
||||
getSourceId: () => layer.indexPatternId,
|
||||
getFilters: (activeData: FramePublicAPI['activeData'], timeRange?: TimeRange) =>
|
||||
getFiltersInLayer(
|
||||
getSourceId: () => {
|
||||
return layer.indexPatternId;
|
||||
},
|
||||
getFilters: (activeData: FramePublicAPI['activeData'], timeRange?: TimeRange) => {
|
||||
return getFiltersInLayer(
|
||||
layer,
|
||||
visibleColumnIds,
|
||||
activeData?.[layerId],
|
||||
indexPatterns[layer.indexPatternId],
|
||||
timeRange
|
||||
),
|
||||
);
|
||||
},
|
||||
getVisualDefaults: () => getVisualDefaultsForLayer(layer),
|
||||
getMaxPossibleNumValues: (columnId) => {
|
||||
if (layer && layer.columns[columnId]) {
|
||||
|
|
|
@ -49,16 +49,18 @@ function getLayerReferenceName(layerId: string) {
|
|||
|
||||
export function extractReferences({ layers }: IndexPatternPrivateState) {
|
||||
const savedObjectReferences: SavedObjectReference[] = [];
|
||||
const persistableLayers: Record<string, Omit<IndexPatternLayer, 'indexPatternId'>> = {};
|
||||
const persistableState: IndexPatternPersistedState = {
|
||||
layers: {},
|
||||
};
|
||||
Object.entries(layers).forEach(([layerId, { indexPatternId, ...persistableLayer }]) => {
|
||||
persistableState.layers[layerId] = persistableLayer;
|
||||
savedObjectReferences.push({
|
||||
type: 'index-pattern',
|
||||
id: indexPatternId,
|
||||
name: getLayerReferenceName(layerId),
|
||||
});
|
||||
persistableLayers[layerId] = persistableLayer;
|
||||
});
|
||||
return { savedObjectReferences, state: { layers: persistableLayers } };
|
||||
return { savedObjectReferences, state: persistableState };
|
||||
}
|
||||
|
||||
export function injectReferences(
|
||||
|
@ -200,6 +202,27 @@ export function changeIndexPattern({
|
|||
};
|
||||
}
|
||||
|
||||
export function renameIndexPattern({
|
||||
oldIndexPatternId,
|
||||
newIndexPatternId,
|
||||
state,
|
||||
}: {
|
||||
oldIndexPatternId: string;
|
||||
newIndexPatternId: string;
|
||||
state: IndexPatternPrivateState;
|
||||
}) {
|
||||
return {
|
||||
...state,
|
||||
layers: mapValues(state.layers, (layer) =>
|
||||
layer.indexPatternId === oldIndexPatternId
|
||||
? { ...layer, indexPatternId: newIndexPatternId }
|
||||
: layer
|
||||
),
|
||||
currentIndexPatternId:
|
||||
state.currentIndexPatternId === oldIndexPatternId ? newIndexPatternId : oldIndexPatternId,
|
||||
};
|
||||
}
|
||||
|
||||
export function triggerActionOnIndexPatternChange({
|
||||
state,
|
||||
layerId,
|
||||
|
@ -211,7 +234,8 @@ export function triggerActionOnIndexPatternChange({
|
|||
state: IndexPatternPrivateState;
|
||||
uiActions: UiActionsStart;
|
||||
}) {
|
||||
const fromDataView = state.layers[layerId].indexPatternId;
|
||||
const fromDataView = state.layers[layerId]?.indexPatternId;
|
||||
if (!fromDataView) return;
|
||||
const toDataView = indexPatternId;
|
||||
|
||||
const trigger = uiActions.getTrigger(UPDATE_FILTER_REFERENCES_TRIGGER);
|
||||
|
|
|
@ -101,6 +101,7 @@ export const createMockedIndexPattern = (): IndexPattern => {
|
|||
hasRestrictions: false,
|
||||
fields,
|
||||
getFieldByName: getFieldByNameFactory(fields),
|
||||
spec: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -140,6 +141,7 @@ export const createMockedRestrictedIndexPattern = () => {
|
|||
fieldFormatMap: { bytes: { id: 'bytes', params: { pattern: '0.0' } } },
|
||||
fields,
|
||||
getFieldByName: getFieldByNameFactory(fields),
|
||||
spec: undefined,
|
||||
typeMeta: {
|
||||
params: {
|
||||
rollup_index: 'my-fake-index-pattern',
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { DragDropIdentifier } from '../drag_drop/providers';
|
||||
import type { IncompleteColumn, GenericIndexPatternColumn } from './operations';
|
||||
import type { DragDropOperation } from '../types';
|
||||
|
|
|
@ -465,6 +465,14 @@ export function getFiltersInLayer(
|
|||
indexPattern: IndexPattern,
|
||||
timeRange: TimeRange | undefined
|
||||
) {
|
||||
if (indexPattern.spec) {
|
||||
return {
|
||||
error: i18n.translate('xpack.lens.indexPattern.adHocDataViewError', {
|
||||
defaultMessage:
|
||||
'"Explore data in Discover" does not support unsaved data views. Save the data view to switch to Discover.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
const filtersGroupedByState = collectFiltersFromMetrics(layer, columnIds);
|
||||
const [enabledFiltersFromMetricsByLanguage, disabledFitleredFromMetricsByLanguage] = (
|
||||
['enabled', 'disabled'] as const
|
||||
|
|
|
@ -64,6 +64,7 @@ describe('loader', () => {
|
|||
id: 'foo',
|
||||
title: 'Foo index',
|
||||
metaFields: [],
|
||||
isPersisted: () => true,
|
||||
typeMeta: {
|
||||
aggs: {
|
||||
date_histogram: {
|
||||
|
@ -100,7 +101,8 @@ describe('loader', () => {
|
|||
id: 'foo',
|
||||
title: 'Foo index',
|
||||
})),
|
||||
} as unknown as Pick<DataViewsContract, 'get' | 'getIdsWithTitle'>,
|
||||
create: jest.fn(),
|
||||
} as unknown as Pick<DataViewsContract, 'get' | 'getIdsWithTitle' | 'create'>,
|
||||
});
|
||||
|
||||
expect(cache.foo.getFieldByName('bytes')!.aggregationRestrictions).toEqual({
|
||||
|
@ -120,6 +122,7 @@ describe('loader', () => {
|
|||
id: 'foo',
|
||||
title: 'Foo index',
|
||||
metaFields: ['timestamp'],
|
||||
isPersisted: () => true,
|
||||
typeMeta: {
|
||||
aggs: {
|
||||
date_histogram: {
|
||||
|
@ -156,7 +159,8 @@ describe('loader', () => {
|
|||
id: 'foo',
|
||||
title: 'Foo index',
|
||||
})),
|
||||
} as unknown as Pick<DataViewsContract, 'get' | 'getIdsWithTitle'>,
|
||||
create: jest.fn(),
|
||||
} as unknown as Pick<DataViewsContract, 'get' | 'getIdsWithTitle' | 'create'>,
|
||||
});
|
||||
|
||||
expect(cache.foo.getFieldByName('timestamp')!.meta).toEqual(true);
|
||||
|
@ -198,12 +202,13 @@ describe('loader', () => {
|
|||
timeFieldName: 'timestamp',
|
||||
hasRestrictions: false,
|
||||
fields: [],
|
||||
isPersisted: () => true,
|
||||
};
|
||||
}
|
||||
return Promise.reject();
|
||||
}),
|
||||
getIdsWithTitle: jest.fn(),
|
||||
} as unknown as Pick<DataViewsContract, 'get' | 'getIdsWithTitle'>;
|
||||
} as unknown as Pick<DataViewsContract, 'get' | 'getIdsWithTitle' | 'create'>;
|
||||
const cache = await loadIndexPatterns({
|
||||
cache: {},
|
||||
patterns: ['1', '2'],
|
||||
|
@ -234,7 +239,7 @@ describe('loader', () => {
|
|||
throw err;
|
||||
}),
|
||||
getIdsWithTitle: jest.fn(),
|
||||
} as unknown as Pick<DataViewsContract, 'get' | 'getIdsWithTitle'>,
|
||||
} as unknown as Pick<DataViewsContract, 'get' | 'getIdsWithTitle' | 'create'>,
|
||||
onError,
|
||||
});
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { isNestedField } from '@kbn/data-views-plugin/common';
|
||||
import type { DataViewsContract, DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { DataViewsContract, DataView, DataViewSpec } from '@kbn/data-views-plugin/public';
|
||||
import { keyBy } from 'lodash';
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { IndexPattern, IndexPatternField, IndexPatternMap, IndexPatternRef } from '../types';
|
||||
|
@ -15,7 +15,7 @@ import { BASE_API_URL, DateRange, ExistingFields } from '../../common';
|
|||
import { DataViewsState } from '../state_management';
|
||||
|
||||
type ErrorHandler = (err: Error) => void;
|
||||
type MinimalDataViewsContract = Pick<DataViewsContract, 'get' | 'getIdsWithTitle'>;
|
||||
type MinimalDataViewsContract = Pick<DataViewsContract, 'get' | 'getIdsWithTitle' | 'create'>;
|
||||
|
||||
/**
|
||||
* All these functions will be used by the Embeddable instance too,
|
||||
|
@ -92,17 +92,27 @@ export function convertDataViewIntoLensIndexPattern(
|
|||
fields: newFields,
|
||||
getFieldByName: getFieldByNameFactory(newFields),
|
||||
hasRestrictions: !!typeMeta?.aggs,
|
||||
spec: dataView.isPersisted() ? undefined : dataView.toSpec(false),
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadIndexPatternRefs(
|
||||
dataViews: MinimalDataViewsContract
|
||||
dataViews: MinimalDataViewsContract,
|
||||
adHocDataViews?: Record<string, DataViewSpec>
|
||||
): Promise<IndexPatternRef[]> {
|
||||
const indexPatterns = await dataViews.getIdsWithTitle();
|
||||
|
||||
return indexPatterns.sort((a, b) => {
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
return indexPatterns
|
||||
.concat(
|
||||
Object.values(adHocDataViews || {}).map((dataViewSpec) => ({
|
||||
id: dataViewSpec.id!,
|
||||
name: dataViewSpec.name,
|
||||
title: dataViewSpec.title!,
|
||||
}))
|
||||
)
|
||||
.sort((a, b) => {
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -122,17 +132,20 @@ export async function loadIndexPatterns({
|
|||
patterns,
|
||||
notUsedPatterns,
|
||||
cache,
|
||||
adHocDataViews,
|
||||
onIndexPatternRefresh,
|
||||
}: {
|
||||
dataViews: MinimalDataViewsContract;
|
||||
patterns: string[];
|
||||
notUsedPatterns?: string[];
|
||||
cache: Record<string, IndexPattern>;
|
||||
adHocDataViews?: Record<string, DataViewSpec>;
|
||||
onIndexPatternRefresh?: () => void;
|
||||
}) {
|
||||
const missingIds = patterns.filter((id) => !cache[id]);
|
||||
const missingIds = patterns.filter((id) => !cache[id] && !adHocDataViews?.[id]);
|
||||
const hasAdHocDataViews = Object.values(adHocDataViews || {}).length > 0;
|
||||
|
||||
if (missingIds.length === 0) {
|
||||
if (missingIds.length === 0 && !hasAdHocDataViews) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
|
@ -147,7 +160,7 @@ export async function loadIndexPatterns({
|
|||
.map((response) => response.value);
|
||||
|
||||
// if all of the used index patterns failed to load, try loading one of not used ones till one succeeds
|
||||
if (!indexPatterns.length && notUsedPatterns) {
|
||||
if (!indexPatterns.length && !hasAdHocDataViews && notUsedPatterns) {
|
||||
for (const notUsedPattern of notUsedPatterns) {
|
||||
const resp = await dataViews.get(notUsedPattern).catch((e) => {
|
||||
// do nothing
|
||||
|
@ -157,6 +170,11 @@ export async function loadIndexPatterns({
|
|||
}
|
||||
}
|
||||
}
|
||||
indexPatterns.push(
|
||||
...(await Promise.all(
|
||||
Object.values(adHocDataViews || {}).map((spec) => dataViews.create(spec))
|
||||
))
|
||||
);
|
||||
|
||||
const indexPatternsObject = indexPatterns.reduce(
|
||||
(acc, indexPattern) => ({
|
||||
|
@ -228,6 +246,10 @@ async function refreshExistingFields({
|
|||
body.timeFieldName = pattern.timeFieldName;
|
||||
}
|
||||
|
||||
if (pattern.spec) {
|
||||
body.spec = pattern.spec;
|
||||
}
|
||||
|
||||
return fetchJson(`${BASE_API_URL}/existing_fields/${pattern.id}`, {
|
||||
body: JSON.stringify(body),
|
||||
}) as Promise<ExistingFields>;
|
||||
|
|
|
@ -42,6 +42,7 @@ const indexPattern1 = {
|
|||
title: 'my-fake-index-pattern',
|
||||
timeFieldName: 'timestamp',
|
||||
hasRestrictions: false,
|
||||
isPersisted: () => true,
|
||||
fields: [
|
||||
{
|
||||
name: 'timestamp',
|
||||
|
@ -127,6 +128,7 @@ const indexPattern2 = {
|
|||
title: 'my-fake-restricted-pattern',
|
||||
timeFieldName: 'timestamp',
|
||||
hasRestrictions: true,
|
||||
isPersisted: () => true,
|
||||
fieldFormatMap: { bytes: { id: 'bytes', params: { pattern: '0.0' } } },
|
||||
fields: [
|
||||
{
|
||||
|
@ -198,7 +200,11 @@ export const sampleIndexPatterns = {
|
|||
export function mockDataViewsService() {
|
||||
return {
|
||||
get: jest.fn(async (id: '1' | '2') => {
|
||||
const result = { ...sampleIndexPatternsFromService[id], metaFields: [] };
|
||||
const result = {
|
||||
...sampleIndexPatternsFromService[id],
|
||||
metaFields: [],
|
||||
isPersisted: () => true,
|
||||
};
|
||||
if (!result.fields) {
|
||||
result.fields = [];
|
||||
}
|
||||
|
@ -216,5 +222,6 @@ export function mockDataViewsService() {
|
|||
},
|
||||
];
|
||||
}),
|
||||
} as unknown as Pick<DataViewsContract, 'get' | 'getIdsWithTitle'>;
|
||||
create: jest.fn(),
|
||||
} as unknown as Pick<DataViewsContract, 'get' | 'getIdsWithTitle' | 'create'>;
|
||||
}
|
||||
|
|
|
@ -5,9 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { DataViewsContract } from '@kbn/data-views-plugin/public';
|
||||
import type { DataViewsContract, DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { CoreStart, IUiSettingsClient } from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ActionExecutionContext, UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import {
|
||||
UPDATE_FILTER_REFERENCES_ACTION,
|
||||
UPDATE_FILTER_REFERENCES_TRIGGER,
|
||||
} from '@kbn/unified-search-plugin/public';
|
||||
import type { DateRange } from '../../common';
|
||||
import type { IndexPattern, IndexPatternMap, IndexPatternRef } from '../types';
|
||||
import {
|
||||
|
@ -17,15 +22,22 @@ import {
|
|||
syncExistingFields,
|
||||
} from './loader';
|
||||
import type { DataViewsState } from '../state_management';
|
||||
import { generateId } from '../id_generator';
|
||||
|
||||
export interface IndexPatternServiceProps {
|
||||
core: Pick<CoreStart, 'http' | 'notifications'>;
|
||||
dataViews: DataViewsContract;
|
||||
uiSettings: IUiSettingsClient;
|
||||
uiActions: UiActionsStart;
|
||||
updateIndexPatterns: (
|
||||
newState: Partial<DataViewsState>,
|
||||
options?: { applyImmediately: boolean }
|
||||
) => void;
|
||||
replaceIndexPattern: (
|
||||
newIndexPattern: IndexPattern,
|
||||
oldId: string,
|
||||
options?: { applyImmediately: boolean }
|
||||
) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -69,6 +81,8 @@ export interface IndexPatternServiceAPI {
|
|||
indexPatternList: IndexPattern[];
|
||||
isFirstExistenceFetch: boolean;
|
||||
}) => Promise<void>;
|
||||
|
||||
replaceDataViewId: (newDataView: DataView) => Promise<void>;
|
||||
/**
|
||||
* Retrieves the default indexPattern from the uiSettings
|
||||
*/
|
||||
|
@ -88,6 +102,8 @@ export function createIndexPatternService({
|
|||
dataViews,
|
||||
uiSettings,
|
||||
updateIndexPatterns,
|
||||
replaceIndexPattern,
|
||||
uiActions,
|
||||
}: IndexPatternServiceProps): IndexPatternServiceAPI {
|
||||
const onChangeError = (err: Error) =>
|
||||
core.notifications.toasts.addError(err, {
|
||||
|
@ -103,6 +119,27 @@ export function createIndexPatternService({
|
|||
...args,
|
||||
});
|
||||
},
|
||||
replaceDataViewId: async (dataView: DataView) => {
|
||||
const newDataView = await dataViews.create({ ...dataView.toSpec(), id: generateId() });
|
||||
dataViews.clearInstanceCache(dataView.id);
|
||||
const loadedPatterns = await loadIndexPatterns({
|
||||
dataViews,
|
||||
patterns: [newDataView.id!],
|
||||
cache: {},
|
||||
});
|
||||
replaceIndexPattern(loadedPatterns[newDataView.id!], dataView.id!, {
|
||||
applyImmediately: true,
|
||||
});
|
||||
const trigger = uiActions.getTrigger(UPDATE_FILTER_REFERENCES_TRIGGER);
|
||||
const action = uiActions.getAction(UPDATE_FILTER_REFERENCES_ACTION);
|
||||
|
||||
action?.execute({
|
||||
trigger,
|
||||
fromDataView: dataView.id,
|
||||
toDataView: newDataView.id,
|
||||
usedDataViews: [],
|
||||
} as ActionExecutionContext);
|
||||
},
|
||||
ensureIndexPattern: (args) =>
|
||||
ensureIndexPattern({ onError: onChangeError, dataViews, ...args }),
|
||||
refreshExistingFields: (args) =>
|
||||
|
|
|
@ -102,7 +102,7 @@ export function mockDataPlugin(
|
|||
extract: (filtersIn: Filter[]) => {
|
||||
const state = filtersIn.map((filter) => ({
|
||||
...filter,
|
||||
meta: { ...filter.meta, index: 'extracted!' },
|
||||
meta: { ...filter.meta },
|
||||
}));
|
||||
return { state, references: [] };
|
||||
},
|
||||
|
@ -127,6 +127,13 @@ export function mockDataPlugin(
|
|||
indexPatterns: {
|
||||
get: jest.fn().mockImplementation((id) => Promise.resolve({ id, isTimeBased: () => true })),
|
||||
},
|
||||
dataViews: {
|
||||
get: jest
|
||||
.fn()
|
||||
.mockImplementation((id) =>
|
||||
Promise.resolve({ id, isTimeBased: () => true, isPersisted: () => true })
|
||||
),
|
||||
},
|
||||
search: createMockSearchService(),
|
||||
nowProvider: {
|
||||
get: jest.fn(),
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
||||
import {
|
||||
createIndexPatternService,
|
||||
IndexPatternServiceProps,
|
||||
|
@ -18,7 +19,16 @@ export function createIndexPatternServiceMock({
|
|||
core = coreMock.createStart(),
|
||||
uiSettings = uiSettingsServiceMock.createStartContract(),
|
||||
dataViews = dataViewPluginMocks.createStartContract(),
|
||||
uiActions = uiActionsPluginMock.createStartContract(),
|
||||
updateIndexPatterns = jest.fn(),
|
||||
replaceIndexPattern = jest.fn(),
|
||||
}: Partial<IndexPatternServiceProps> = {}): IndexPatternServiceAPI {
|
||||
return createIndexPatternService({ core, uiSettings, updateIndexPatterns, dataViews });
|
||||
return createIndexPatternService({
|
||||
core,
|
||||
uiSettings,
|
||||
updateIndexPatterns,
|
||||
replaceIndexPattern,
|
||||
dataViews,
|
||||
uiActions,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'
|
|||
import type { EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import { presentationUtilPluginMock } from '@kbn/presentation-util-plugin/public/mocks';
|
||||
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
||||
import type { LensAttributeService } from '../lens_attribute_service';
|
||||
import type {
|
||||
LensByValueInput,
|
||||
|
@ -87,7 +88,7 @@ export function makeDefaultServices(
|
|||
const dataViewsMock = dataViewPluginMocks.createStartContract();
|
||||
dataViewsMock.get.mockImplementation(
|
||||
jest.fn((id) =>
|
||||
Promise.resolve({ id, isTimeBased: () => true, fields: [] })
|
||||
Promise.resolve({ id, isTimeBased: () => true, fields: [], isPersisted: () => true })
|
||||
) as unknown as DataViewsPublicPluginStart['get']
|
||||
);
|
||||
dataViewsMock.getIdsWithTitle.mockImplementation(jest.fn(async () => []));
|
||||
|
@ -158,6 +159,7 @@ export function makeDefaultServices(
|
|||
remove: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
},
|
||||
uiActions: uiActionsPluginMock.createStartContract(),
|
||||
spaces: spacesPluginMock.createStartContract(),
|
||||
charts: chartPluginMock.createSetupContract(),
|
||||
dataViewFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(),
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
SavedObjectReference,
|
||||
ResolvedSimpleSavedObject,
|
||||
} from '@kbn/core/public';
|
||||
import { DataViewSpec } from '@kbn/data-views-plugin/public';
|
||||
import { DOC_TYPE } from '../../common';
|
||||
import { LensSavedObjectAttributes } from '../async_services';
|
||||
|
||||
|
@ -30,6 +31,8 @@ export interface Document {
|
|||
state?: unknown;
|
||||
};
|
||||
filters: Filter[];
|
||||
adHocDataViews?: Record<string, DataViewSpec>;
|
||||
internalReferences?: SavedObjectReference[];
|
||||
};
|
||||
references: SavedObjectReference[];
|
||||
}
|
||||
|
|
|
@ -119,6 +119,7 @@ export function loadInitial(
|
|||
datasourceMap,
|
||||
datasourceStates: lens.datasourceStates,
|
||||
initialContext,
|
||||
adHocDataViews: lens.persistedDoc?.state.adHocDataViews,
|
||||
...loaderSharedArgs,
|
||||
},
|
||||
{
|
||||
|
@ -190,10 +191,11 @@ export function loadInitial(
|
|||
{
|
||||
datasourceMap,
|
||||
datasourceStates: docDatasourceStates,
|
||||
references: doc.references,
|
||||
references: [...doc.references, ...(doc.state.internalReferences || [])],
|
||||
initialContext,
|
||||
dataViews: lensServices.dataViews,
|
||||
storage: lensServices.storage,
|
||||
adHocDataViews: doc.state.adHocDataViews,
|
||||
defaultIndexPatternId: lensServices.uiSettings.get('defaultIndex'),
|
||||
},
|
||||
{ isFullEditor: true }
|
||||
|
|
|
@ -12,7 +12,7 @@ import { Query } from '@kbn/es-query';
|
|||
import { History } from 'history';
|
||||
import { LensEmbeddableInput } from '..';
|
||||
import { TableInspectorAdapter } from '../editor_frame_service/types';
|
||||
import type { VisualizeEditorContext, Suggestion } from '../types';
|
||||
import type { VisualizeEditorContext, Suggestion, IndexPattern } from '../types';
|
||||
import { getInitialDatasourceId, getResolvedDateRange, getRemoveOperation } from '../utils';
|
||||
import type { DataViewsState, LensAppState, LensStoreDeps, VisualizationState } from './types';
|
||||
import type { Datasource, Visualization } from '../types';
|
||||
|
@ -172,6 +172,9 @@ export const setLayerDefaultDimension = createAction<{
|
|||
export const updateIndexPatterns = createAction<Partial<DataViewsState>>(
|
||||
'lens/updateIndexPatterns'
|
||||
);
|
||||
export const replaceIndexpattern = createAction<{ newIndexPattern: IndexPattern; oldId: string }>(
|
||||
'lens/replaceIndexPattern'
|
||||
);
|
||||
export const changeIndexPattern = createAction<{
|
||||
visualizationIds?: string[];
|
||||
datasourceIds?: string[];
|
||||
|
@ -206,6 +209,7 @@ export const lensActions = {
|
|||
addLayer,
|
||||
setLayerDefaultDimension,
|
||||
updateIndexPatterns,
|
||||
replaceIndexpattern,
|
||||
changeIndexPattern,
|
||||
};
|
||||
|
||||
|
@ -316,7 +320,25 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
}
|
||||
) => {
|
||||
const { visualizationIds, datasourceIds, layerId, indexPatternId, dataViews } = payload;
|
||||
const newState: Partial<LensAppState> = { dataViews: { ...state.dataViews, ...dataViews } };
|
||||
const newIndexPatternRefs = [...state.dataViews.indexPatternRefs];
|
||||
const availableRefs = new Set(newIndexPatternRefs.map((ref) => ref.id));
|
||||
// check for missing refs
|
||||
Object.values(dataViews.indexPatterns || {}).forEach((indexPattern) => {
|
||||
if (!availableRefs.has(indexPattern.id)) {
|
||||
newIndexPatternRefs.push({
|
||||
id: indexPattern.id!,
|
||||
name: indexPattern.name,
|
||||
title: indexPattern.title,
|
||||
});
|
||||
}
|
||||
});
|
||||
const newState: Partial<LensAppState> = {
|
||||
dataViews: {
|
||||
...state.dataViews,
|
||||
indexPatterns: dataViews.indexPatterns,
|
||||
indexPatternRefs: newIndexPatternRefs,
|
||||
},
|
||||
};
|
||||
if (visualizationIds?.length) {
|
||||
for (const visualizationId of visualizationIds) {
|
||||
const activeVisualization =
|
||||
|
@ -402,6 +424,38 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
dataViews: { ...state.dataViews, ...payload },
|
||||
};
|
||||
},
|
||||
[replaceIndexpattern.type]: (
|
||||
state,
|
||||
{ payload }: { payload: { newIndexPattern: IndexPattern; oldId: string } }
|
||||
) => {
|
||||
state.dataViews.indexPatterns[payload.newIndexPattern.id] = payload.newIndexPattern;
|
||||
delete state.dataViews.indexPatterns[payload.oldId];
|
||||
state.dataViews.indexPatternRefs = state.dataViews.indexPatternRefs.filter(
|
||||
(r) => r.id !== payload.oldId
|
||||
);
|
||||
state.dataViews.indexPatternRefs.push({
|
||||
id: payload.newIndexPattern.id,
|
||||
title: payload.newIndexPattern.title,
|
||||
name: payload.newIndexPattern.name,
|
||||
});
|
||||
const visualization = visualizationMap[state.visualization.activeId!];
|
||||
state.visualization.state =
|
||||
visualization.onIndexPatternRename?.(
|
||||
state.visualization.state,
|
||||
payload.oldId,
|
||||
payload.newIndexPattern.id
|
||||
) ?? state.visualization.state;
|
||||
|
||||
Object.entries(state.datasourceStates).forEach(([datasourceId, datasourceState]) => {
|
||||
const datasource = datasourceMap[datasourceId];
|
||||
state.datasourceStates[datasourceId].state =
|
||||
datasource?.onIndexPatternRename?.(
|
||||
datasourceState.state,
|
||||
payload.oldId,
|
||||
payload.newIndexPattern.id!
|
||||
) ?? datasourceState.state;
|
||||
});
|
||||
},
|
||||
[updateDatasourceState.type]: (
|
||||
state,
|
||||
{
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { FilterManager } from '@kbn/data-plugin/public';
|
||||
import { SavedObjectReference } from '@kbn/core/public';
|
||||
import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common';
|
||||
import { LensState } from './types';
|
||||
import { Datasource, DatasourceMap, VisualizationMap } from '../types';
|
||||
import { getDatasourceLayers } from '../editor_frame_service/editor_frame';
|
||||
|
@ -17,6 +18,12 @@ export const selectQuery = (state: LensState) => state.lens.query;
|
|||
export const selectSearchSessionId = (state: LensState) => state.lens.searchSessionId;
|
||||
export const selectFilters = (state: LensState) => state.lens.filters;
|
||||
export const selectResolvedDateRange = (state: LensState) => state.lens.resolvedDateRange;
|
||||
export const selectAdHocDataViews = (state: LensState) =>
|
||||
Object.fromEntries(
|
||||
Object.values(state.lens.dataViews.indexPatterns)
|
||||
.filter((indexPattern) => indexPattern.spec)
|
||||
.map((indexPattern) => [indexPattern.id, indexPattern.spec!])
|
||||
);
|
||||
export const selectVisualization = (state: LensState) => state.lens.visualization;
|
||||
export const selectStagedPreview = (state: LensState) => state.lens.stagedPreview;
|
||||
export const selectStagedActiveData = (state: LensState) =>
|
||||
|
@ -69,6 +76,7 @@ export const selectSavedObjectFormat = createSelector(
|
|||
selectQuery,
|
||||
selectFilters,
|
||||
selectActiveDatasourceId,
|
||||
selectAdHocDataViews,
|
||||
selectInjectedDependencies as SelectInjectedDependenciesFunction<{
|
||||
datasourceMap: DatasourceMap;
|
||||
visualizationMap: VisualizationMap;
|
||||
|
@ -82,6 +90,7 @@ export const selectSavedObjectFormat = createSelector(
|
|||
query,
|
||||
filters,
|
||||
activeDatasourceId,
|
||||
adHocDataViews,
|
||||
{ datasourceMap, visualizationMap, extractFilterReferences }
|
||||
) => {
|
||||
const activeVisualization =
|
||||
|
@ -105,16 +114,40 @@ export const selectSavedObjectFormat = createSelector(
|
|||
|
||||
const persistibleDatasourceStates: Record<string, unknown> = {};
|
||||
const references: SavedObjectReference[] = [];
|
||||
const internalReferences: SavedObjectReference[] = [];
|
||||
Object.entries(activeDatasources).forEach(([id, datasource]) => {
|
||||
const { state: persistableState, savedObjectReferences } = datasource.getPersistableState(
|
||||
datasourceStates[id].state
|
||||
);
|
||||
persistibleDatasourceStates[id] = persistableState;
|
||||
references.push(...savedObjectReferences);
|
||||
savedObjectReferences.forEach((r) => {
|
||||
if (r.type === 'index-pattern' && adHocDataViews[r.id]) {
|
||||
internalReferences.push(r);
|
||||
} else {
|
||||
references.push(r);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const persistableAdHocDataViews = Object.fromEntries(
|
||||
Object.entries(adHocDataViews).map(([id, dataView]) => {
|
||||
const { references: dataViewReferences, state } =
|
||||
DataViewPersistableStateService.extract(dataView);
|
||||
references.push(...dataViewReferences);
|
||||
return [id, state];
|
||||
})
|
||||
);
|
||||
|
||||
const adHocFilters = filters
|
||||
.filter((f) => !references.some((r) => r.type === 'index-pattern' && r.id === f.meta.index))
|
||||
.map((f) => ({ ...f, meta: { ...f.meta, value: undefined } }));
|
||||
|
||||
const referencedFilters = filters.filter((f) =>
|
||||
references.some((r) => r.type === 'index-pattern' && r.id === f.meta.index)
|
||||
);
|
||||
|
||||
const { state: persistableFilters, references: filterReferences } =
|
||||
extractFilterReferences(filters);
|
||||
extractFilterReferences(referencedFilters);
|
||||
|
||||
references.push(...filterReferences);
|
||||
|
||||
|
@ -128,8 +161,10 @@ export const selectSavedObjectFormat = createSelector(
|
|||
state: {
|
||||
visualization: visualization.state,
|
||||
query,
|
||||
filters: persistableFilters,
|
||||
filters: [...persistableFilters, ...adHocFilters],
|
||||
datasourceStates: persistibleDatasourceStates,
|
||||
internalReferences,
|
||||
adHocDataViews: persistableAdHocDataViews,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ import type {
|
|||
} from '@kbn/ui-actions-plugin/public';
|
||||
import type { ClickTriggerEvent, BrushTriggerEvent } from '@kbn/charts-plugin/public';
|
||||
import type { IndexPatternAggRestrictions } from '@kbn/data-plugin/public';
|
||||
import type { FieldSpec } from '@kbn/data-views-plugin/common';
|
||||
import type { FieldSpec, DataViewSpec } from '@kbn/data-views-plugin/common';
|
||||
import type { FieldFormatParams } from '@kbn/field-formats-plugin/common';
|
||||
import type { DraggingIdentifier, DragDropIdentifier, DragContextState } from './drag_drop';
|
||||
import type { DateRange, LayerType, SortingHint } from '../common';
|
||||
|
@ -69,6 +69,7 @@ export interface IndexPattern {
|
|||
}
|
||||
>;
|
||||
hasRestrictions: boolean;
|
||||
spec?: DataViewSpec;
|
||||
}
|
||||
|
||||
export type IndexPatternField = FieldSpec & {
|
||||
|
@ -340,6 +341,12 @@ export interface Datasource<T = unknown, P = unknown> {
|
|||
indexPatternId: string,
|
||||
layerId?: string
|
||||
) => T;
|
||||
onIndexPatternRename?: (state: T, oldIndexPatternId: string, newIndexPatternId: string) => T;
|
||||
triggerOnIndexPatternChange?: (
|
||||
state: T,
|
||||
oldIndexPatternId: string,
|
||||
newIndexPatternId: string
|
||||
) => void;
|
||||
|
||||
onRefreshIndexPattern: () => void;
|
||||
|
||||
|
@ -906,7 +913,7 @@ export interface Visualization<T = unknown> {
|
|||
/** Optional, if the visualization supports multiple layers */
|
||||
removeLayer?: (state: T, layerId: string) => T;
|
||||
/** Track added layers in internal state */
|
||||
appendLayer?: (state: T, layerId: string, type: LayerType) => T;
|
||||
appendLayer?: (state: T, layerId: string, type: LayerType, indexPatternId?: string) => T;
|
||||
|
||||
/** Retrieve a list of supported layer types with initialization data */
|
||||
getSupportedLayers: (
|
||||
|
@ -1083,6 +1090,7 @@ export interface Visualization<T = unknown> {
|
|||
* This method makes it aware of the change and produces a new updated state
|
||||
*/
|
||||
onIndexPatternChange?: (state: T, indexPatternId: string, layerId?: string) => T;
|
||||
onIndexPatternRename?: (state: T, oldIndexPatternId: string, newIndexPatternId: string) => T;
|
||||
/**
|
||||
* Gets custom display options for showing the visualization.
|
||||
*/
|
||||
|
|
|
@ -93,14 +93,11 @@ export async function refreshIndexPatternsList({
|
|||
.map((datasource) => datasource?.onRefreshIndexPattern)
|
||||
.filter(Boolean);
|
||||
|
||||
const [newlyMappedIndexPattern, indexPatternRefs] = await Promise.all([
|
||||
indexPatternService.loadIndexPatterns({
|
||||
cache: {},
|
||||
patterns: [indexPatternId],
|
||||
onIndexPatternRefresh: () => onRefreshCallbacks.forEach((fn) => fn()),
|
||||
}),
|
||||
indexPatternService.loadIndexPatternRefs({ isFullEditor: true }),
|
||||
]);
|
||||
const newlyMappedIndexPattern = await indexPatternService.loadIndexPatterns({
|
||||
cache: {},
|
||||
patterns: [indexPatternId],
|
||||
onIndexPatternRefresh: () => onRefreshCallbacks.forEach((fn) => fn()),
|
||||
});
|
||||
const indexPattern = newlyMappedIndexPattern[indexPatternId];
|
||||
// But what about existingFields here?
|
||||
// When the indexPatterns cache object gets updated, the data panel will
|
||||
|
@ -110,7 +107,6 @@ export async function refreshIndexPatternsList({
|
|||
...indexPatternsCache,
|
||||
[indexPatternId]: indexPattern,
|
||||
},
|
||||
indexPatternRefs,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -235,6 +235,7 @@ export const getLegacyMetricVisualization = ({
|
|||
groups: [
|
||||
{
|
||||
groupId: 'metric',
|
||||
dataTestSubj: 'lnsLegacyMetric_metricDimensionPanel',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: i18n.translate('xpack.lens.metric.headingLabel', {
|
||||
defaultMessage: 'Value',
|
||||
|
|
|
@ -10,14 +10,21 @@ import { makeLensEmbeddableFactory } from './make_lens_embeddable_factory';
|
|||
import { getAllMigrations } from '../migrations/saved_object_migrations';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { GetMigrationFunctionObjectFn } from '@kbn/kibana-utils-plugin/common';
|
||||
import { DataViewSpec } from '@kbn/data-views-plugin/common';
|
||||
|
||||
describe('embeddable migrations', () => {
|
||||
test('should have all saved object migrations versions (>7.13.0)', () => {
|
||||
const savedObjectMigrationVersions = Object.keys(getAllMigrations({}, {})).filter((version) => {
|
||||
return semverGte(version, '7.13.1');
|
||||
});
|
||||
const savedObjectMigrationVersions = Object.keys(getAllMigrations({}, {}, {})).filter(
|
||||
(version) => {
|
||||
return semverGte(version, '7.13.1');
|
||||
}
|
||||
);
|
||||
const embeddableMigrationVersions = (
|
||||
makeLensEmbeddableFactory(() => ({}), {})()?.migrations as GetMigrationFunctionObjectFn
|
||||
makeLensEmbeddableFactory(
|
||||
() => ({}),
|
||||
() => ({}),
|
||||
{}
|
||||
)()?.migrations as GetMigrationFunctionObjectFn
|
||||
)();
|
||||
if (embeddableMigrationVersions) {
|
||||
expect(savedObjectMigrationVersions.sort()).toEqual(
|
||||
|
@ -56,6 +63,7 @@ describe('embeddable migrations', () => {
|
|||
}));
|
||||
},
|
||||
}),
|
||||
() => ({}),
|
||||
{}
|
||||
)()?.migrations as GetMigrationFunctionObjectFn
|
||||
)();
|
||||
|
@ -80,6 +88,60 @@ describe('embeddable migrations', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('should properly apply a data view migration within a lens visualization', () => {
|
||||
const migrationVersion = 'some-version';
|
||||
|
||||
const lensVisualizationDoc = {
|
||||
attributes: {
|
||||
state: {
|
||||
adHocDataViews: {
|
||||
abc: {
|
||||
id: 'abc',
|
||||
},
|
||||
def: {
|
||||
id: 'def',
|
||||
name: 'A name',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const migrations = (
|
||||
makeLensEmbeddableFactory(
|
||||
() => ({}),
|
||||
() => ({
|
||||
[migrationVersion]: (dataView: DataViewSpec) => {
|
||||
return {
|
||||
...dataView,
|
||||
name: dataView.id,
|
||||
};
|
||||
},
|
||||
}),
|
||||
{}
|
||||
)()?.migrations as GetMigrationFunctionObjectFn
|
||||
)();
|
||||
|
||||
const migratedLensDoc = migrations[migrationVersion](lensVisualizationDoc);
|
||||
|
||||
expect(migratedLensDoc).toEqual({
|
||||
attributes: {
|
||||
state: {
|
||||
adHocDataViews: {
|
||||
abc: {
|
||||
id: 'abc',
|
||||
name: 'abc',
|
||||
},
|
||||
def: {
|
||||
id: 'def',
|
||||
name: 'def',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should properly apply a custom visualization migration', () => {
|
||||
const migrationVersion = 'some-version';
|
||||
|
||||
|
@ -97,11 +159,15 @@ describe('embeddable migrations', () => {
|
|||
}));
|
||||
|
||||
const embeddableMigrationVersions = (
|
||||
makeLensEmbeddableFactory(() => ({}), {
|
||||
abc: () => ({
|
||||
[migrationVersion]: migrationFn,
|
||||
}),
|
||||
})()?.migrations as GetMigrationFunctionObjectFn
|
||||
makeLensEmbeddableFactory(
|
||||
() => ({}),
|
||||
() => ({}),
|
||||
{
|
||||
abc: () => ({
|
||||
[migrationVersion]: migrationFn,
|
||||
}),
|
||||
}
|
||||
)()?.migrations as GetMigrationFunctionObjectFn
|
||||
)();
|
||||
|
||||
const migratedLensDoc = embeddableMigrationVersions?.[migrationVersion](lensVisualizationDoc);
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
commonUpdateVisLayerType,
|
||||
getLensCustomVisualizationMigrations,
|
||||
getLensFilterMigrations,
|
||||
getLensDataViewMigrations,
|
||||
commonMigrateMetricIds,
|
||||
} from '../migrations/common_migrations';
|
||||
import {
|
||||
|
@ -47,6 +48,7 @@ import { extract, inject } from '../../common/embeddable_factory';
|
|||
export const makeLensEmbeddableFactory =
|
||||
(
|
||||
getFilterMigrations: () => MigrateFunctionsObject,
|
||||
getDataViewMigrations: () => MigrateFunctionsObject,
|
||||
customVisualizationMigrations: CustomVisualizationMigrations
|
||||
) =>
|
||||
(): EmbeddableRegistryDefinition => {
|
||||
|
@ -54,89 +56,94 @@ export const makeLensEmbeddableFactory =
|
|||
id: DOC_TYPE,
|
||||
migrations: () =>
|
||||
mergeMigrationFunctionMaps(
|
||||
mergeMigrationFunctionMaps(getLensFilterMigrations(getFilterMigrations()), {
|
||||
// This migration is run in 7.13.1 for `by value` panels because the 7.13 release window was missed.
|
||||
'7.13.1': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShapePre712 };
|
||||
const migratedLensState = commonRenameOperationsForFormula(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'7.14.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape713 };
|
||||
const migratedLensState = commonRemoveTimezoneDateHistogramParam(
|
||||
lensState.attributes
|
||||
);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'7.15.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape715<VisStatePre715> };
|
||||
const migratedLensState = commonUpdateVisLayerType(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'7.16.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape715<VisState716> };
|
||||
const migratedLensState = commonMakeReversePaletteAsCustom(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'8.1.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape715 };
|
||||
const migratedLensState = commonRenameRecordsField(
|
||||
commonRenameFilterReferences(lensState.attributes)
|
||||
);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'8.2.0': (state) => {
|
||||
const lensState = state as unknown as {
|
||||
attributes: LensDocShape810<VisState810>;
|
||||
};
|
||||
let migratedLensState = commonSetLastValueShowArrayValues(lensState.attributes);
|
||||
migratedLensState = commonEnhanceTableRowHeight(
|
||||
migratedLensState as LensDocShape810<VisState810>
|
||||
);
|
||||
migratedLensState = commonSetIncludeEmptyRowsDateHistogram(migratedLensState);
|
||||
mergeMigrationFunctionMaps(
|
||||
mergeMigrationFunctionMaps(getLensFilterMigrations(getFilterMigrations()), {
|
||||
// This migration is run in 7.13.1 for `by value` panels because the 7.13 release window was missed.
|
||||
'7.13.1': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShapePre712 };
|
||||
const migratedLensState = commonRenameOperationsForFormula(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'7.14.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape713 };
|
||||
const migratedLensState = commonRemoveTimezoneDateHistogramParam(
|
||||
lensState.attributes
|
||||
);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'7.15.0': (state) => {
|
||||
const lensState = state as unknown as {
|
||||
attributes: LensDocShape715<VisStatePre715>;
|
||||
};
|
||||
const migratedLensState = commonUpdateVisLayerType(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'7.16.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape715<VisState716> };
|
||||
const migratedLensState = commonMakeReversePaletteAsCustom(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'8.1.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape715 };
|
||||
const migratedLensState = commonRenameRecordsField(
|
||||
commonRenameFilterReferences(lensState.attributes)
|
||||
);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'8.2.0': (state) => {
|
||||
const lensState = state as unknown as {
|
||||
attributes: LensDocShape810<VisState810>;
|
||||
};
|
||||
let migratedLensState = commonSetLastValueShowArrayValues(lensState.attributes);
|
||||
migratedLensState = commonEnhanceTableRowHeight(
|
||||
migratedLensState as LensDocShape810<VisState810>
|
||||
);
|
||||
migratedLensState = commonSetIncludeEmptyRowsDateHistogram(migratedLensState);
|
||||
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'8.3.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape810<VisState810> };
|
||||
let migratedLensState = commonLockOldMetricVisSettings(lensState.attributes);
|
||||
migratedLensState = commonPreserveOldLegendSizeDefault(migratedLensState);
|
||||
migratedLensState = commonFixValueLabelsInXY(
|
||||
migratedLensState as LensDocShape810<VisStatePre830>
|
||||
);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'8.5.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape840<VisState840> };
|
||||
const migratedLensState = commonMigrateMetricIds(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
}),
|
||||
getLensCustomVisualizationMigrations(customVisualizationMigrations)
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'8.3.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape810<VisState810> };
|
||||
let migratedLensState = commonLockOldMetricVisSettings(lensState.attributes);
|
||||
migratedLensState = commonPreserveOldLegendSizeDefault(migratedLensState);
|
||||
migratedLensState = commonFixValueLabelsInXY(
|
||||
migratedLensState as LensDocShape810<VisStatePre830>
|
||||
);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'8.5.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape840<VisState840> };
|
||||
const migratedLensState = commonMigrateMetricIds(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
}),
|
||||
getLensCustomVisualizationMigrations(customVisualizationMigrations)
|
||||
),
|
||||
getLensDataViewMigrations(getDataViewMigrations())
|
||||
),
|
||||
extract,
|
||||
inject,
|
||||
|
|
|
@ -5,8 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataViewSpec } from '@kbn/data-views-plugin/common';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { getLensFilterMigrations } from './common_migrations';
|
||||
import { getLensDataViewMigrations, getLensFilterMigrations } from './common_migrations';
|
||||
|
||||
describe('Lens migrations', () => {
|
||||
describe('applying filter migrations', () => {
|
||||
|
@ -41,4 +42,68 @@ describe('Lens migrations', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applying data view migrations', () => {
|
||||
it('creates a data view migrations map that works on a lens visualization', () => {
|
||||
const dataViewMigrations = {
|
||||
'1.1': (dataView: DataViewSpec) => ({ ...dataView, name: '1.1' }),
|
||||
'2.2': (dataView: DataViewSpec) => ({ ...dataView, name: '2.2' }),
|
||||
'3.3': (dataView: DataViewSpec) => ({ ...dataView, name: '3.3' }),
|
||||
};
|
||||
|
||||
const lensVisualizationSavedObject = {
|
||||
attributes: {
|
||||
state: {
|
||||
adHocDataViews: {
|
||||
abc: {
|
||||
id: 'abc',
|
||||
},
|
||||
def: {
|
||||
id: 'def',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const migrationMap = getLensDataViewMigrations(dataViewMigrations);
|
||||
|
||||
expect(
|
||||
migrationMap['1.1'](lensVisualizationSavedObject).attributes.state.adHocDataViews
|
||||
).toEqual({
|
||||
abc: {
|
||||
id: 'abc',
|
||||
name: '1.1',
|
||||
},
|
||||
def: {
|
||||
id: 'def',
|
||||
name: '1.1',
|
||||
},
|
||||
});
|
||||
expect(
|
||||
migrationMap['2.2'](lensVisualizationSavedObject).attributes.state.adHocDataViews
|
||||
).toEqual({
|
||||
abc: {
|
||||
id: 'abc',
|
||||
name: '2.2',
|
||||
},
|
||||
def: {
|
||||
id: 'def',
|
||||
name: '2.2',
|
||||
},
|
||||
});
|
||||
expect(
|
||||
migrationMap['3.3'](lensVisualizationSavedObject).attributes.state.adHocDataViews
|
||||
).toEqual({
|
||||
abc: {
|
||||
id: 'abc',
|
||||
name: '3.3',
|
||||
},
|
||||
def: {
|
||||
id: 'def',
|
||||
name: '3.3',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -347,6 +347,27 @@ export const getLensFilterMigrations = (
|
|||
},
|
||||
}));
|
||||
|
||||
export const getLensDataViewMigrations = (
|
||||
dataViewMigrations: MigrateFunctionsObject
|
||||
): MigrateFunctionsObject =>
|
||||
mapValues(dataViewMigrations, (migrate) => (lensDoc: { attributes: LensDocShape }) => ({
|
||||
...lensDoc,
|
||||
attributes: {
|
||||
...lensDoc.attributes,
|
||||
state: {
|
||||
...lensDoc.attributes.state,
|
||||
adHocDataViews: !lensDoc.attributes.state.adHocDataViews
|
||||
? undefined
|
||||
: Object.fromEntries(
|
||||
Object.entries(lensDoc.attributes.state.adHocDataViews).map(([id, spec]) => [
|
||||
id,
|
||||
migrate(spec),
|
||||
])
|
||||
),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export const fixLensTopValuesCustomFormatting = (attributes: LensDocShape810): LensDocShape810 => {
|
||||
const newAttributes = cloneDeep(attributes);
|
||||
const datasourceLayers = newAttributes.state.datasourceStates.indexpattern.layers || {};
|
||||
|
|
|
@ -25,9 +25,10 @@ import {
|
|||
} from './types';
|
||||
import { layerTypes, LegacyMetricState } from '../../common';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { DataViewSpec } from '@kbn/data-views-plugin/common';
|
||||
|
||||
describe('Lens migrations', () => {
|
||||
const migrations = getAllMigrations({}, {});
|
||||
const migrations = getAllMigrations({}, {}, {});
|
||||
describe('7.7.0 missing dimensions in XY', () => {
|
||||
const context = {} as SavedObjectMigrationContext;
|
||||
|
||||
|
@ -1623,6 +1624,7 @@ describe('Lens migrations', () => {
|
|||
}));
|
||||
},
|
||||
},
|
||||
{},
|
||||
{}
|
||||
);
|
||||
|
||||
|
@ -1649,6 +1651,61 @@ describe('Lens migrations', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('should properly apply a data view migration within a lens visualization', () => {
|
||||
const migrationVersion = 'some-version';
|
||||
|
||||
const lensVisualizationDoc = {
|
||||
attributes: {
|
||||
state: {
|
||||
adHocDataViews: {
|
||||
abc: {
|
||||
id: 'abc',
|
||||
},
|
||||
def: {
|
||||
id: 'def',
|
||||
name: 'A name',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const migrationFunctionsObject = getAllMigrations(
|
||||
{},
|
||||
{
|
||||
[migrationVersion]: (dataView: DataViewSpec) => {
|
||||
return {
|
||||
...dataView,
|
||||
name: dataView.id,
|
||||
};
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const migratedLensDoc = migrationFunctionsObject[migrationVersion](
|
||||
lensVisualizationDoc as SavedObjectUnsanitizedDoc,
|
||||
{} as SavedObjectMigrationContext
|
||||
);
|
||||
|
||||
expect(migratedLensDoc).toEqual({
|
||||
attributes: {
|
||||
state: {
|
||||
adHocDataViews: {
|
||||
abc: {
|
||||
id: 'abc',
|
||||
name: 'abc',
|
||||
},
|
||||
def: {
|
||||
id: 'def',
|
||||
name: 'def',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should properly apply a custom visualization migration', () => {
|
||||
const migrationVersion = 'some-version';
|
||||
|
||||
|
@ -1666,6 +1723,7 @@ describe('Lens migrations', () => {
|
|||
}));
|
||||
|
||||
const migrationFunctionsObject = getAllMigrations(
|
||||
{},
|
||||
{},
|
||||
{
|
||||
abc: () => ({
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
import type { Query, Filter } from '@kbn/es-query';
|
||||
import { mergeSavedObjectMigrationMaps } from '@kbn/core/server';
|
||||
import { MigrateFunctionsObject } from '@kbn/kibana-utils-plugin/common';
|
||||
import { DataViewSpec } from '@kbn/data-views-plugin/common';
|
||||
import { PersistableFilter } from '../../common';
|
||||
import {
|
||||
LensDocShapePost712,
|
||||
|
@ -51,6 +52,7 @@ import {
|
|||
commonFixValueLabelsInXY,
|
||||
commonLockOldMetricVisSettings,
|
||||
commonPreserveOldLegendSizeDefault,
|
||||
getLensDataViewMigrations,
|
||||
commonMigrateMetricIds,
|
||||
} from './common_migrations';
|
||||
|
||||
|
@ -103,6 +105,7 @@ export interface LensDocShape<VisualizationState = unknown> {
|
|||
visualization: VisualizationState;
|
||||
query: Query;
|
||||
filters: PersistableFilter[];
|
||||
adHocDataViews?: Record<string, DataViewSpec>;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -542,12 +545,16 @@ const lensMigrations: SavedObjectMigrationMap = {
|
|||
|
||||
export const getAllMigrations = (
|
||||
filterMigrations: MigrateFunctionsObject,
|
||||
dataViewMigrations: MigrateFunctionsObject,
|
||||
customVisualizationMigrations: CustomVisualizationMigrations
|
||||
): SavedObjectMigrationMap =>
|
||||
mergeSavedObjectMigrationMaps(
|
||||
mergeSavedObjectMigrationMaps(
|
||||
lensMigrations,
|
||||
getLensFilterMigrations(filterMigrations) as unknown as SavedObjectMigrationMap
|
||||
mergeSavedObjectMigrationMaps(
|
||||
lensMigrations,
|
||||
getLensFilterMigrations(filterMigrations) as unknown as SavedObjectMigrationMap
|
||||
),
|
||||
getLensCustomVisualizationMigrations(customVisualizationMigrations)
|
||||
),
|
||||
getLensCustomVisualizationMigrations(customVisualizationMigrations)
|
||||
getLensDataViewMigrations(dataViewMigrations) as unknown as SavedObjectMigrationMap
|
||||
);
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
TaskManagerStartContract,
|
||||
} from '@kbn/task-manager-plugin/server';
|
||||
import { EmbeddableSetup } from '@kbn/embeddable-plugin/server';
|
||||
import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common';
|
||||
import { setupRoutes } from './routes';
|
||||
import { getUiSettings } from './ui_settings';
|
||||
import { setupSavedObjects } from './saved_objects';
|
||||
|
@ -71,6 +72,7 @@ export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {
|
|||
|
||||
const lensEmbeddableFactory = makeLensEmbeddableFactory(
|
||||
getFilterMigrations,
|
||||
DataViewPersistableStateService.getAllMigrations.bind(DataViewPersistableStateService),
|
||||
this.customVisualizationMigrations
|
||||
);
|
||||
plugins.embeddable.registerEmbeddableFactory(lensEmbeddableFactory());
|
||||
|
|
|
@ -12,7 +12,7 @@ import { schema } from '@kbn/config-schema';
|
|||
import { RequestHandlerContext, ElasticsearchClient } from '@kbn/core/server';
|
||||
import { CoreSetup, Logger } from '@kbn/core/server';
|
||||
import { RuntimeField } from '@kbn/data-views-plugin/common';
|
||||
import { DataViewsService, DataView, FieldSpec } from '@kbn/data-views-plugin/common';
|
||||
import { DataViewsService, DataView, FieldSpec, DataViewSpec } from '@kbn/data-views-plugin/common';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/server';
|
||||
import { BASE_API_URL } from '../../common';
|
||||
import { FIELD_EXISTENCE_SETTING } from '../ui_settings';
|
||||
|
@ -51,6 +51,7 @@ export async function existingFieldsRoute(setup: CoreSetup<PluginStartContract>,
|
|||
fromDate: schema.maybe(schema.string()),
|
||||
toDate: schema.maybe(schema.string()),
|
||||
timeFieldName: schema.maybe(schema.string()),
|
||||
spec: schema.object({}, { unknowns: 'allow' }),
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
@ -110,6 +111,7 @@ async function fetchFieldExistence({
|
|||
fromDate,
|
||||
toDate,
|
||||
timeFieldName,
|
||||
spec,
|
||||
includeFrozen,
|
||||
useSampling,
|
||||
}: {
|
||||
|
@ -120,6 +122,7 @@ async function fetchFieldExistence({
|
|||
fromDate?: string;
|
||||
toDate?: string;
|
||||
timeFieldName?: string;
|
||||
spec?: DataViewSpec;
|
||||
includeFrozen: boolean;
|
||||
useSampling: boolean;
|
||||
}) {
|
||||
|
@ -132,13 +135,17 @@ async function fetchFieldExistence({
|
|||
fromDate,
|
||||
toDate,
|
||||
timeFieldName,
|
||||
spec,
|
||||
includeFrozen,
|
||||
});
|
||||
}
|
||||
|
||||
const uiSettingsClient = (await context.core).uiSettings.client;
|
||||
const metaFields: string[] = await uiSettingsClient.get(UI_SETTINGS.META_FIELDS);
|
||||
const dataView = await dataViewsService.get(indexPatternId);
|
||||
const dataView =
|
||||
spec && Object.keys(spec).length !== 0
|
||||
? await dataViewsService.create(spec)
|
||||
: await dataViewsService.get(indexPatternId);
|
||||
const allFields = buildFieldList(dataView, metaFields);
|
||||
const existingFieldList = await dataViewsService.getFieldsForIndexPattern(dataView, {
|
||||
// filled in by data views service
|
||||
|
@ -159,6 +166,7 @@ async function legacyFetchFieldExistenceSampling({
|
|||
fromDate,
|
||||
toDate,
|
||||
timeFieldName,
|
||||
spec,
|
||||
includeFrozen,
|
||||
}: {
|
||||
indexPatternId: string;
|
||||
|
@ -168,11 +176,15 @@ async function legacyFetchFieldExistenceSampling({
|
|||
fromDate?: string;
|
||||
toDate?: string;
|
||||
timeFieldName?: string;
|
||||
spec?: DataViewSpec;
|
||||
includeFrozen: boolean;
|
||||
}) {
|
||||
const coreContext = await context.core;
|
||||
const metaFields: string[] = await coreContext.uiSettings.client.get(UI_SETTINGS.META_FIELDS);
|
||||
const indexPattern = await dataViewsService.get(indexPatternId);
|
||||
const indexPattern =
|
||||
spec && Object.keys(spec).length !== 0
|
||||
? await dataViewsService.create(spec)
|
||||
: await dataViewsService.get(indexPatternId);
|
||||
|
||||
const fields = buildFieldList(indexPattern, metaFields);
|
||||
const runtimeMappings = indexPattern.getRuntimeMappings();
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { CoreSetup } from '@kbn/core/server';
|
||||
import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common';
|
||||
import { MigrateFunctionsObject } from '@kbn/kibana-utils-plugin/common';
|
||||
import { getEditPath } from '../common';
|
||||
import { getAllMigrations } from './migrations/saved_object_migrations';
|
||||
|
@ -31,7 +32,12 @@ export function setupSavedObjects(
|
|||
uiCapabilitiesPath: 'visualize.show',
|
||||
}),
|
||||
},
|
||||
migrations: () => getAllMigrations(getFilterMigrations(), customVisualizationMigrations),
|
||||
migrations: () =>
|
||||
getAllMigrations(
|
||||
getFilterMigrations(),
|
||||
DataViewPersistableStateService.getAllMigrations(),
|
||||
customVisualizationMigrations
|
||||
),
|
||||
mappings: {
|
||||
properties: {
|
||||
title: {
|
||||
|
|
149
x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts
Normal file
149
x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts
Normal file
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* 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 { DebugState } from '@elastic/charts';
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const PageObjects = getPageObjects([
|
||||
'visualize',
|
||||
'lens',
|
||||
'header',
|
||||
'unifiedSearch',
|
||||
'dashboard',
|
||||
'timeToVisualize',
|
||||
]);
|
||||
const elasticChart = getService('elasticChart');
|
||||
const fieldEditor = getService('fieldEditor');
|
||||
const retry = getService('retry');
|
||||
|
||||
const expectedData = [
|
||||
{ x: '97.220.3.248', y: 19755 },
|
||||
{ x: '169.228.188.120', y: 18994 },
|
||||
{ x: '78.83.247.30', y: 17246 },
|
||||
{ x: '226.82.228.233', y: 15687 },
|
||||
{ x: '93.28.27.24', y: 15614.33 },
|
||||
{ x: 'Other', y: 5722.77 },
|
||||
];
|
||||
function assertMatchesExpectedData(state: DebugState) {
|
||||
expect(
|
||||
state.bars![0].bars.map((bar) => ({
|
||||
x: bar.x,
|
||||
y: Math.floor(bar.y * 100) / 100,
|
||||
}))
|
||||
).to.eql(expectedData);
|
||||
}
|
||||
|
||||
async function setupAdHocDataView() {
|
||||
await PageObjects.visualize.navigateToNewVisualization();
|
||||
await PageObjects.visualize.clickVisType('lens');
|
||||
await elasticChart.setNewChartUiDebugFlag(true);
|
||||
await PageObjects.lens.createAdHocDataView('*stash*');
|
||||
retry.try(async () => {
|
||||
const selectedPattern = await PageObjects.lens.getDataPanelIndexPattern();
|
||||
expect(selectedPattern).to.eql('*stash*');
|
||||
});
|
||||
}
|
||||
|
||||
describe('lens ad hoc data view tests', () => {
|
||||
it('should allow building a chart based on ad hoc data view', async () => {
|
||||
await setupAdHocDataView();
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
|
||||
operation: 'terms',
|
||||
field: 'ip',
|
||||
});
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
|
||||
operation: 'average',
|
||||
field: 'bytes',
|
||||
});
|
||||
const data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart');
|
||||
assertMatchesExpectedData(data!);
|
||||
await PageObjects.lens.removeLayer();
|
||||
});
|
||||
|
||||
it('should allow adding and using a field', async () => {
|
||||
await PageObjects.lens.switchToVisualization('lnsDatatable');
|
||||
await retry.try(async () => {
|
||||
await PageObjects.lens.clickAddField();
|
||||
await fieldEditor.setName('runtimefield');
|
||||
await fieldEditor.enableValue();
|
||||
await fieldEditor.typeScript("emit('abc')");
|
||||
await fieldEditor.save();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.lens.searchField('runtime');
|
||||
await PageObjects.lens.waitForField('runtimefield');
|
||||
await PageObjects.lens.dragFieldToWorkspace('runtimefield');
|
||||
});
|
||||
await PageObjects.lens.waitForVisualization();
|
||||
expect(await PageObjects.lens.getDatatableHeaderText(0)).to.equal(
|
||||
'Top 5 values of runtimefield'
|
||||
);
|
||||
expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('abc');
|
||||
await PageObjects.lens.removeLayer();
|
||||
});
|
||||
|
||||
it('should allow switching to another data view and back', async () => {
|
||||
await PageObjects.lens.switchDataPanelIndexPattern('logstash-*');
|
||||
await PageObjects.lens.waitForFieldMissing('runtimefield');
|
||||
await PageObjects.lens.switchDataPanelIndexPattern('*stash*');
|
||||
await PageObjects.lens.waitForField('runtimefield');
|
||||
});
|
||||
|
||||
it('should allow removing a field', async () => {
|
||||
await PageObjects.lens.clickField('runtimefield');
|
||||
await PageObjects.lens.removeField();
|
||||
await fieldEditor.confirmDelete();
|
||||
await PageObjects.lens.waitForFieldMissing('runtimefield');
|
||||
});
|
||||
|
||||
it('should allow adding an ad-hoc chart to a dashboard', async () => {
|
||||
await PageObjects.lens.switchToVisualization('lnsMetric');
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsMetric_primaryMetricDimensionPanel > lns-empty-dimension',
|
||||
operation: 'average',
|
||||
field: 'bytes',
|
||||
});
|
||||
|
||||
await PageObjects.lens.waitForVisualization('mtrVis');
|
||||
const metricData = await PageObjects.lens.getMetricVisualizationData();
|
||||
expect(metricData[0].value).to.eql('5.73K');
|
||||
expect(metricData[0].title).to.eql('Average of bytes');
|
||||
await PageObjects.lens.save('New Lens from Modal', false, false, false, 'new');
|
||||
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
expect(metricData[0].value).to.eql('5.73K');
|
||||
|
||||
const panelCount = await PageObjects.dashboard.getPanelCount();
|
||||
expect(panelCount).to.eql(1);
|
||||
|
||||
await PageObjects.timeToVisualize.resetNewDashboard();
|
||||
});
|
||||
|
||||
it('should allow saving the ad-hoc chart into a saved object', async () => {
|
||||
await setupAdHocDataView();
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
|
||||
operation: 'average',
|
||||
field: 'bytes',
|
||||
});
|
||||
|
||||
await PageObjects.lens.switchToVisualization('lnsMetric');
|
||||
|
||||
await PageObjects.lens.waitForVisualization('mtrVis');
|
||||
await PageObjects.lens.save('Lens with adhoc data view');
|
||||
await PageObjects.lens.waitForVisualization('mtrVis');
|
||||
const metricData = await PageObjects.lens.getMetricVisualizationData();
|
||||
expect(metricData[0].value).to.eql('5.73K');
|
||||
expect(metricData[0].title).to.eql('Average of bytes');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -73,6 +73,7 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext
|
|||
loadTestFile(require.resolve('./smokescreen'));
|
||||
} else {
|
||||
loadTestFile(require.resolve('./smokescreen'));
|
||||
loadTestFile(require.resolve('./ad_hoc_data_view'));
|
||||
loadTestFile(require.resolve('./persistent_context'));
|
||||
loadTestFile(require.resolve('./table_dashboard'));
|
||||
loadTestFile(require.resolve('./table'));
|
||||
|
|
|
@ -6,52 +6,18 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { WebElementWrapper } from '../../../../../../test/functional/services/lib/web_element_wrapper';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']);
|
||||
const findService = getService('find');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const filterBar = getService('filterBar');
|
||||
const retry = getService('retry');
|
||||
|
||||
const getMetricTiles = () =>
|
||||
findService.allByCssSelector('[data-test-subj="mtrVis"] .echChart li');
|
||||
|
||||
const getIfExists = async (selector: string, container: WebElementWrapper) =>
|
||||
(await findService.descendantExistsByCssSelector(selector, container))
|
||||
? await container.findByCssSelector(selector)
|
||||
: undefined;
|
||||
|
||||
const getMetricDatum = async (tile: WebElementWrapper) => {
|
||||
return {
|
||||
title: await (await getIfExists('h2', tile))?.getVisibleText(),
|
||||
subtitle: await (await getIfExists('.echMetricText__subtitle', tile))?.getVisibleText(),
|
||||
extraText: await (await getIfExists('.echMetricText__extra', tile))?.getVisibleText(),
|
||||
value: await (await getIfExists('.echMetricText__value', tile))?.getVisibleText(),
|
||||
color: await (await getIfExists('.echMetric', tile))?.getComputedStyle('background-color'),
|
||||
};
|
||||
};
|
||||
|
||||
const getMetricData = async () => {
|
||||
const tiles = await getMetricTiles();
|
||||
const showingBar = Boolean(await findService.existsByCssSelector('.echSingleMetricProgress'));
|
||||
|
||||
const metricData = [];
|
||||
for (const tile of tiles) {
|
||||
metricData.push({
|
||||
...(await getMetricDatum(tile)),
|
||||
showingBar,
|
||||
});
|
||||
}
|
||||
return metricData;
|
||||
};
|
||||
|
||||
const clickMetric = async (title: string) => {
|
||||
const tiles = await getMetricTiles();
|
||||
const tiles = await PageObjects.lens.getMetricTiles();
|
||||
for (const tile of tiles) {
|
||||
const datum = await getMetricDatum(tile);
|
||||
const datum = await PageObjects.lens.getMetricDatum(tile);
|
||||
if (datum.title === title) {
|
||||
await tile.click();
|
||||
return;
|
||||
|
@ -79,7 +45,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
field: 'bytes',
|
||||
});
|
||||
|
||||
expect((await getMetricData()).length).to.be.equal(1);
|
||||
expect((await PageObjects.lens.getMetricVisualizationData()).length).to.be.equal(1);
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsMetric_breakdownByDimensionPanel > lns-empty-dimension',
|
||||
|
@ -89,7 +55,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
await PageObjects.lens.waitForVisualization('mtrVis');
|
||||
|
||||
expect(await getMetricData()).to.eql([
|
||||
expect(await PageObjects.lens.getMetricVisualizationData()).to.eql([
|
||||
{
|
||||
title: '97.220.3.248',
|
||||
subtitle: 'Average of bytes',
|
||||
|
@ -146,7 +112,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
await PageObjects.lens.waitForVisualization('mtrVis');
|
||||
|
||||
expect((await getMetricData())[0].showingBar).to.be(true);
|
||||
expect((await PageObjects.lens.getMetricVisualizationData())[0].showingBar).to.be(true);
|
||||
|
||||
await PageObjects.lens.closeDimensionEditor();
|
||||
await PageObjects.lens.removeDimension('lnsMetric_maxDimensionPanel');
|
||||
|
@ -179,7 +145,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
await PageObjects.lens.waitForVisualization('mtrVis');
|
||||
|
||||
const data = await getMetricData();
|
||||
const data = await PageObjects.lens.getMetricVisualizationData();
|
||||
|
||||
expect(data.map(({ color }) => color)).to.be.eql(new Array(6).fill('rgba(0, 0, 0, 1)'));
|
||||
});
|
||||
|
@ -198,7 +164,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
await PageObjects.lens.waitForVisualization('mtrVis');
|
||||
|
||||
const data = await getMetricData();
|
||||
const data = await PageObjects.lens.getMetricVisualizationData();
|
||||
expect(data.map(({ color }) => color)).to.eql(expectedDynamicColors);
|
||||
});
|
||||
|
||||
|
@ -213,7 +179,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
await PageObjects.lens.waitForVisualization('mtrVis');
|
||||
|
||||
expect((await getMetricData()).map(({ color }) => color)).to.eql(expectedDynamicColors); // colors shouldn't change
|
||||
expect(
|
||||
(await PageObjects.lens.getMetricVisualizationData()).map(({ color }) => color)
|
||||
).to.eql(expectedDynamicColors); // colors shouldn't change
|
||||
|
||||
await PageObjects.lens.closePaletteEditor();
|
||||
await PageObjects.lens.closeDimensionEditor();
|
||||
|
@ -235,7 +203,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
await PageObjects.lens.closeDimensionEditor();
|
||||
|
||||
const tiles = await getMetricTiles();
|
||||
const tiles = await await PageObjects.lens.getMetricTiles();
|
||||
const lastTile = tiles[tiles.length - 1];
|
||||
|
||||
const initialPosition = await lastTile.getPosition();
|
||||
|
|
|
@ -7,11 +7,13 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import { setTimeout as setTimeoutAsync } from 'timers/promises';
|
||||
import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper';
|
||||
import { FtrProviderContext } from '../ftr_provider_context';
|
||||
import { logWrapper } from './log_wrapper';
|
||||
|
||||
export function LensPageProvider({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const log = getService('log');
|
||||
const findService = getService('find');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const retry = getService('retry');
|
||||
const elasticChart = getService('elasticChart');
|
||||
|
@ -1111,6 +1113,47 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
// TODO: target dimensionTrigger color element after merging https://github.com/elastic/kibana/pull/76871
|
||||
await testSubjects.getAttribute('~indexPattern-dimension-colorPicker', color);
|
||||
},
|
||||
async getMetricTiles() {
|
||||
return findService.allByCssSelector('[data-test-subj="mtrVis"] .echChart li');
|
||||
},
|
||||
|
||||
async getMetricElementIfExists(selector: string, container: WebElementWrapper) {
|
||||
return (await findService.descendantExistsByCssSelector(selector, container))
|
||||
? await container.findByCssSelector(selector)
|
||||
: undefined;
|
||||
},
|
||||
|
||||
async getMetricDatum(tile: WebElementWrapper) {
|
||||
return {
|
||||
title: await (await this.getMetricElementIfExists('h2', tile))?.getVisibleText(),
|
||||
subtitle: await (
|
||||
await this.getMetricElementIfExists('.echMetricText__subtitle', tile)
|
||||
)?.getVisibleText(),
|
||||
extraText: await (
|
||||
await this.getMetricElementIfExists('.echMetricText__extra', tile)
|
||||
)?.getVisibleText(),
|
||||
value: await (
|
||||
await this.getMetricElementIfExists('.echMetricText__value', tile)
|
||||
)?.getVisibleText(),
|
||||
color: await (
|
||||
await this.getMetricElementIfExists('.echMetric', tile)
|
||||
)?.getComputedStyle('background-color'),
|
||||
};
|
||||
},
|
||||
|
||||
async getMetricVisualizationData() {
|
||||
const tiles = await this.getMetricTiles();
|
||||
const showingBar = Boolean(await findService.existsByCssSelector('.echSingleMetricProgress'));
|
||||
|
||||
const metricData = [];
|
||||
for (const tile of tiles) {
|
||||
metricData.push({
|
||||
...(await this.getMetricDatum(tile)),
|
||||
showingBar,
|
||||
});
|
||||
}
|
||||
return metricData;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates and saves a lens visualization from a dashboard
|
||||
|
@ -1208,6 +1251,11 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
await testSubjects.click('indexPattern-add-field');
|
||||
},
|
||||
|
||||
async createAdHocDataView(name: string) {
|
||||
await testSubjects.click('lns-dataView-switch-link');
|
||||
await PageObjects.unifiedSearch.createNewDataView(name, true);
|
||||
},
|
||||
|
||||
/** resets visualization/layer or removes a layer */
|
||||
async removeLayer() {
|
||||
await retry.try(async () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue