mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Dashboard][Embeddable] Create Explicit Diffing System (#121241)
Co-authored-by: Anton Dosov <dosantappdev@gmail.com> Co-authored-by: nreese <reese.nathan@elastic.co>
This commit is contained in:
parent
27a9df79e7
commit
944ccf10e8
17 changed files with 632 additions and 179 deletions
|
@ -92,13 +92,11 @@ export class BookEmbeddable
|
|||
};
|
||||
|
||||
readonly getInputAsValueType = async (): Promise<BookByValueInput> => {
|
||||
const input = this.attributeService.getExplicitInputFromEmbeddable(this);
|
||||
return this.attributeService.getInputAsValueType(input);
|
||||
return this.attributeService.getInputAsValueType(this.getExplicitInput());
|
||||
};
|
||||
|
||||
readonly getInputAsRefType = async (): Promise<BookByReferenceInput> => {
|
||||
const input = this.attributeService.getExplicitInputFromEmbeddable(this);
|
||||
return this.attributeService.getInputAsRefType(input, {
|
||||
return this.attributeService.getInputAsRefType(this.getExplicitInput(), {
|
||||
showSaveModal: true,
|
||||
saveModalTitle: this.getTitle(),
|
||||
});
|
||||
|
|
|
@ -71,7 +71,7 @@ export const createEditBookAction = (getStartServices: () => Promise<StartServic
|
|||
const newInput = await attributeService.wrapAttributes(
|
||||
attributes,
|
||||
useRefType,
|
||||
attributeService.getExplicitInputFromEmbeddable(embeddable)
|
||||
embeddable.getExplicitInput()
|
||||
);
|
||||
if (!useRefType && (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId) {
|
||||
// Set the saved object ID to null so that update input will remove the existing savedObjectId...
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
import { History } from 'history';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import { debounceTime, switchMap } from 'rxjs/operators';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { BehaviorSubject, combineLatest, Subject } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
|
||||
|
||||
import { DashboardConstants } from '../..';
|
||||
import { ViewMode } from '../../services/embeddable';
|
||||
|
@ -261,37 +261,47 @@ export const useDashboardAppState = ({
|
|||
dashboardAppState.$onDashboardStateChange,
|
||||
dashboardBuildContext.$checkForUnsavedChanges,
|
||||
])
|
||||
.pipe(debounceTime(DashboardConstants.CHANGE_CHECK_DEBOUNCE))
|
||||
.subscribe((states) => {
|
||||
const [lastSaved, current] = states;
|
||||
const unsavedChanges = diffDashboardState(lastSaved, current);
|
||||
.pipe(
|
||||
debounceTime(DashboardConstants.CHANGE_CHECK_DEBOUNCE),
|
||||
switchMap((states) => {
|
||||
return new Observable((observer) => {
|
||||
const [lastSaved, current] = states;
|
||||
diffDashboardState({
|
||||
getEmbeddable: (id: string) => dashboardContainer.untilEmbeddableLoaded(id),
|
||||
originalState: lastSaved,
|
||||
newState: current,
|
||||
}).then((unsavedChanges) => {
|
||||
if (observer.closed) return;
|
||||
const savedTimeChanged =
|
||||
lastSaved.timeRestore &&
|
||||
(!areTimeRangesEqual(
|
||||
{
|
||||
from: savedDashboard?.timeFrom,
|
||||
to: savedDashboard?.timeTo,
|
||||
},
|
||||
timefilter.getTime()
|
||||
) ||
|
||||
!areRefreshIntervalsEqual(
|
||||
savedDashboard?.refreshInterval,
|
||||
timefilter.getRefreshInterval()
|
||||
));
|
||||
|
||||
const savedTimeChanged =
|
||||
lastSaved.timeRestore &&
|
||||
(!areTimeRangesEqual(
|
||||
{
|
||||
from: savedDashboard?.timeFrom,
|
||||
to: savedDashboard?.timeTo,
|
||||
},
|
||||
timefilter.getTime()
|
||||
) ||
|
||||
!areRefreshIntervalsEqual(
|
||||
savedDashboard?.refreshInterval,
|
||||
timefilter.getRefreshInterval()
|
||||
));
|
||||
/**
|
||||
* changes to the dashboard should only be considered 'unsaved changes' when
|
||||
* editing the dashboard
|
||||
*/
|
||||
const hasUnsavedChanges =
|
||||
current.viewMode === ViewMode.EDIT &&
|
||||
(Object.keys(unsavedChanges).length > 0 || savedTimeChanged);
|
||||
setDashboardAppState((s) => ({ ...s, hasUnsavedChanges }));
|
||||
|
||||
/**
|
||||
* changes to the dashboard should only be considered 'unsaved changes' when
|
||||
* editing the dashboard
|
||||
*/
|
||||
const hasUnsavedChanges =
|
||||
current.viewMode === ViewMode.EDIT &&
|
||||
(Object.keys(unsavedChanges).length > 0 || savedTimeChanged);
|
||||
setDashboardAppState((s) => ({ ...s, hasUnsavedChanges }));
|
||||
|
||||
unsavedChanges.viewMode = current.viewMode; // always push view mode into session store.
|
||||
dashboardSessionStorage.setState(savedDashboardId, unsavedChanges);
|
||||
});
|
||||
unsavedChanges.viewMode = current.viewMode; // always push view mode into session store.
|
||||
dashboardSessionStorage.setState(savedDashboardId, unsavedChanges);
|
||||
});
|
||||
});
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
/**
|
||||
* initialize the last saved state, and build a callback which can be used to update
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Filter } from '@kbn/es-query';
|
||||
|
||||
import { DashboardOptions, DashboardState } from '../../types';
|
||||
import { diffDashboardState } from './diff_dashboard_state';
|
||||
import { EmbeddableInput, IEmbeddable, ViewMode } from '../../services/embeddable';
|
||||
|
||||
const testFilter: Filter = {
|
||||
meta: {
|
||||
alias: null,
|
||||
disabled: false,
|
||||
negate: false,
|
||||
},
|
||||
query: { query: 'hi' },
|
||||
};
|
||||
|
||||
const getEmbeddable = (id: string) =>
|
||||
Promise.resolve({
|
||||
getExplicitInputIsEqual: (previousInput: EmbeddableInput) => true,
|
||||
} as unknown as IEmbeddable);
|
||||
|
||||
const getDashboardState = (state?: Partial<DashboardState>): DashboardState => {
|
||||
const defaultState: DashboardState = {
|
||||
description: 'This is a dashboard which is very neat',
|
||||
query: { query: '', language: 'kql' },
|
||||
title: 'A very neat dashboard',
|
||||
viewMode: ViewMode.VIEW,
|
||||
fullScreenMode: false,
|
||||
filters: [testFilter],
|
||||
timeRestore: false,
|
||||
tags: [],
|
||||
options: {
|
||||
hidePanelTitles: false,
|
||||
useMargins: true,
|
||||
syncColors: false,
|
||||
},
|
||||
panels: {
|
||||
panel_1: {
|
||||
type: 'panel_type',
|
||||
gridData: { w: 0, h: 0, x: 0, y: 0, i: 'panel_1' },
|
||||
panelRefName: 'panel_panel_1',
|
||||
explicitInput: {
|
||||
id: 'panel_1',
|
||||
},
|
||||
},
|
||||
panel_2: {
|
||||
type: 'panel_type',
|
||||
gridData: { w: 0, h: 0, x: 0, y: 0, i: 'panel_2' },
|
||||
panelRefName: 'panel_panel_2',
|
||||
explicitInput: {
|
||||
id: 'panel_1',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return { ...defaultState, ...state };
|
||||
};
|
||||
|
||||
const getKeysFromDiff = async (partialState?: Partial<DashboardState>): Promise<string[]> =>
|
||||
Object.keys(
|
||||
await diffDashboardState({
|
||||
originalState: getDashboardState(),
|
||||
newState: getDashboardState(partialState),
|
||||
getEmbeddable,
|
||||
})
|
||||
);
|
||||
|
||||
describe('Dashboard state diff function', () => {
|
||||
it('finds no difference in equal states', async () => {
|
||||
expect(await getKeysFromDiff()).toEqual([]);
|
||||
});
|
||||
|
||||
it('diffs simple state keys correctly', async () => {
|
||||
expect(
|
||||
(
|
||||
await getKeysFromDiff({
|
||||
timeRestore: true,
|
||||
title: 'what a cool new title',
|
||||
description: 'what a cool new description',
|
||||
query: { query: 'woah a query', language: 'kql' },
|
||||
})
|
||||
).sort()
|
||||
).toEqual(['description', 'query', 'timeRestore', 'title']);
|
||||
});
|
||||
|
||||
it('picks up differences in dashboard options', async () => {
|
||||
expect(
|
||||
await getKeysFromDiff({
|
||||
options: {
|
||||
hidePanelTitles: false,
|
||||
useMargins: false,
|
||||
syncColors: false,
|
||||
},
|
||||
})
|
||||
).toEqual(['options']);
|
||||
});
|
||||
|
||||
it('considers undefined and false to be equivalent in dashboard options', async () => {
|
||||
expect(
|
||||
await getKeysFromDiff({
|
||||
options: {
|
||||
useMargins: true,
|
||||
syncColors: undefined,
|
||||
} as unknown as DashboardOptions,
|
||||
})
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('calls getExplicitInputIsEqual on each panel', async () => {
|
||||
const mockedGetEmbeddable = jest.fn().mockImplementation((id) => getEmbeddable(id));
|
||||
await diffDashboardState({
|
||||
originalState: getDashboardState(),
|
||||
newState: getDashboardState(),
|
||||
getEmbeddable: mockedGetEmbeddable,
|
||||
});
|
||||
expect(mockedGetEmbeddable).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('short circuits panels comparison when one panel returns false', async () => {
|
||||
const mockedGetEmbeddable = jest.fn().mockImplementation((id) => {
|
||||
if (id === 'panel_1') {
|
||||
return Promise.resolve({
|
||||
getExplicitInputIsEqual: (previousInput: EmbeddableInput) => false,
|
||||
} as unknown as IEmbeddable);
|
||||
}
|
||||
getEmbeddable(id);
|
||||
});
|
||||
|
||||
await diffDashboardState({
|
||||
originalState: getDashboardState(),
|
||||
newState: getDashboardState(),
|
||||
getEmbeddable: mockedGetEmbeddable,
|
||||
});
|
||||
expect(mockedGetEmbeddable).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('skips individual panel comparisons if panel ids are different', async () => {
|
||||
const mockedGetEmbeddable = jest.fn().mockImplementation((id) => getEmbeddable(id));
|
||||
const stateDiff = await diffDashboardState({
|
||||
originalState: getDashboardState(),
|
||||
newState: getDashboardState({
|
||||
panels: {
|
||||
panel_1: {
|
||||
type: 'panel_type',
|
||||
gridData: { w: 0, h: 0, x: 0, y: 0, i: 'panel_1' },
|
||||
panelRefName: 'panel_panel_1',
|
||||
explicitInput: {
|
||||
id: 'panel_1',
|
||||
},
|
||||
},
|
||||
// panel 2 has been deleted
|
||||
},
|
||||
}),
|
||||
getEmbeddable: mockedGetEmbeddable,
|
||||
});
|
||||
expect(mockedGetEmbeddable).not.toHaveBeenCalled();
|
||||
expect(Object.keys(stateDiff)).toEqual(['panels']);
|
||||
});
|
||||
});
|
|
@ -6,158 +6,168 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { DashboardPanelState } from '..';
|
||||
import { esFilters, Filter } from '../../services/data';
|
||||
import { EmbeddableInput } from '../../services/embeddable';
|
||||
import {
|
||||
DashboardContainerInput,
|
||||
DashboardOptions,
|
||||
DashboardPanelMap,
|
||||
DashboardState,
|
||||
} from '../../types';
|
||||
import { xor, omit, isEmpty } from 'lodash';
|
||||
import fastIsEqual from 'fast-deep-equal';
|
||||
import { compareFilters, COMPARE_ALL_OPTIONS, Filter, isFilterPinned } from '@kbn/es-query';
|
||||
|
||||
import { DashboardContainerInput } from '../..';
|
||||
import { controlGroupInputIsEqual } from './dashboard_control_group';
|
||||
import { DashboardOptions, DashboardPanelMap, DashboardState } from '../../types';
|
||||
import { IEmbeddable } from '../../services/embeddable';
|
||||
|
||||
interface DashboardDiffCommon {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
const stateKeystoIgnore = [
|
||||
'expandedPanelId',
|
||||
'fullScreenMode',
|
||||
'savedQuery',
|
||||
'viewMode',
|
||||
'tags',
|
||||
] as const;
|
||||
type DashboardStateToCompare = Omit<DashboardState, typeof stateKeystoIgnore[number]>;
|
||||
|
||||
type DashboardDiffCommonFilters = DashboardDiffCommon & { filters: Filter[] };
|
||||
const inputKeystoIgnore = ['searchSessionId', 'lastReloadRequestTime', 'executionContext'] as const;
|
||||
type DashboardInputToCompare = Omit<DashboardContainerInput, typeof inputKeystoIgnore[number]>;
|
||||
|
||||
/**
|
||||
* The diff dashboard Container method is used to sync redux state and the dashboard container input.
|
||||
* It should eventually be replaced with a usage of the dashboardContainer.isInputEqual function
|
||||
**/
|
||||
export const diffDashboardContainerInput = (
|
||||
originalInput: DashboardContainerInput,
|
||||
newInput: DashboardContainerInput
|
||||
) => {
|
||||
return commonDiffFilters<DashboardContainerInput>(
|
||||
originalInput as unknown as DashboardDiffCommonFilters,
|
||||
newInput as unknown as DashboardDiffCommonFilters,
|
||||
['searchSessionId', 'lastReloadRequestTime', 'executionContext']
|
||||
);
|
||||
};
|
||||
): Partial<DashboardContainerInput> => {
|
||||
const { filters: originalFilters, ...commonOriginal } = omit(originalInput, inputKeystoIgnore);
|
||||
const { filters: newFilters, ...commonNew } = omit(newInput, inputKeystoIgnore);
|
||||
|
||||
export const diffDashboardState = (
|
||||
original: DashboardState,
|
||||
newState: DashboardState
|
||||
): Partial<DashboardState> => {
|
||||
const common = commonDiffFilters<DashboardState>(
|
||||
original as unknown as DashboardDiffCommonFilters,
|
||||
newState as unknown as DashboardDiffCommonFilters,
|
||||
[
|
||||
'viewMode',
|
||||
'panels',
|
||||
'options',
|
||||
'fullScreenMode',
|
||||
'savedQuery',
|
||||
'expandedPanelId',
|
||||
'controlGroupInput',
|
||||
],
|
||||
true
|
||||
);
|
||||
const commonInputDiff: Partial<DashboardInputToCompare> = commonDiff(commonOriginal, commonNew);
|
||||
const filtersAreEqual = getFiltersAreEqual(originalInput.filters, newInput.filters);
|
||||
|
||||
return {
|
||||
...common,
|
||||
...(panelsAreEqual(original.panels, newState.panels) ? {} : { panels: newState.panels }),
|
||||
...(optionsAreEqual(original.options, newState.options) ? {} : { options: newState.options }),
|
||||
...(controlGroupInputIsEqual(original.controlGroupInput, newState.controlGroupInput)
|
||||
? {}
|
||||
: { controlGroupInput: newState.controlGroupInput }),
|
||||
...commonInputDiff,
|
||||
...(filtersAreEqual ? {} : { filters: newInput.filters }),
|
||||
};
|
||||
};
|
||||
|
||||
const optionsAreEqual = (optionsA: DashboardOptions, optionsB: DashboardOptions): boolean => {
|
||||
/**
|
||||
* The diff dashboard state method compares dashboard state keys to determine which state keys
|
||||
* have changed, and therefore should be backed up.
|
||||
**/
|
||||
export const diffDashboardState = async ({
|
||||
originalState,
|
||||
newState,
|
||||
getEmbeddable,
|
||||
}: {
|
||||
originalState: DashboardState;
|
||||
newState: DashboardState;
|
||||
getEmbeddable: (id: string) => Promise<IEmbeddable>;
|
||||
}): Promise<Partial<DashboardState>> => {
|
||||
const {
|
||||
controlGroupInput: originalControlGroupInput,
|
||||
options: originalOptions,
|
||||
filters: originalFilters,
|
||||
panels: originalPanels,
|
||||
...commonCompareOriginal
|
||||
} = omit(originalState, stateKeystoIgnore);
|
||||
const {
|
||||
controlGroupInput: newControlGroupInput,
|
||||
options: newOptions,
|
||||
filters: newFilters,
|
||||
panels: newPanels,
|
||||
...commonCompareNew
|
||||
} = omit(newState, stateKeystoIgnore);
|
||||
|
||||
const commonStateDiff: Partial<DashboardStateToCompare> = commonDiff(
|
||||
commonCompareOriginal,
|
||||
commonCompareNew
|
||||
);
|
||||
|
||||
const panelsAreEqual = await getPanelsAreEqual(
|
||||
originalState.panels,
|
||||
newState.panels,
|
||||
getEmbeddable
|
||||
);
|
||||
const optionsAreEqual = getOptionsAreEqual(originalState.options, newState.options);
|
||||
const filtersAreEqual = getFiltersAreEqual(originalState.filters, newState.filters, true);
|
||||
const controlGroupIsEqual = controlGroupInputIsEqual(
|
||||
originalState.controlGroupInput,
|
||||
newState.controlGroupInput
|
||||
);
|
||||
|
||||
return {
|
||||
...commonStateDiff,
|
||||
...(panelsAreEqual ? {} : { panels: newState.panels }),
|
||||
...(filtersAreEqual ? {} : { filters: newState.filters }),
|
||||
...(optionsAreEqual ? {} : { options: newState.options }),
|
||||
...(controlGroupIsEqual ? {} : { controlGroupInput: newState.controlGroupInput }),
|
||||
};
|
||||
};
|
||||
|
||||
const getFiltersAreEqual = (
|
||||
filtersA: Filter[],
|
||||
filtersB: Filter[],
|
||||
ignorePinned?: boolean
|
||||
): boolean => {
|
||||
return compareFilters(
|
||||
filtersA,
|
||||
ignorePinned ? filtersB.filter((f) => !isFilterPinned(f)) : filtersB,
|
||||
COMPARE_ALL_OPTIONS
|
||||
);
|
||||
};
|
||||
|
||||
const getOptionsAreEqual = (optionsA: DashboardOptions, optionsB: DashboardOptions): boolean => {
|
||||
const optionKeys = [
|
||||
...(Object.keys(optionsA) as Array<keyof DashboardOptions>),
|
||||
...(Object.keys(optionsB) as Array<keyof DashboardOptions>),
|
||||
];
|
||||
for (const key of optionKeys) {
|
||||
if (Boolean(optionsA[key]) !== Boolean(optionsB[key])) {
|
||||
return false;
|
||||
}
|
||||
if (Boolean(optionsA[key]) !== Boolean(optionsB[key])) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const panelsAreEqual = (panelsA: DashboardPanelMap, panelsB: DashboardPanelMap): boolean => {
|
||||
const embeddableIdsA = Object.keys(panelsA);
|
||||
const embeddableIdsB = Object.keys(panelsB);
|
||||
if (
|
||||
embeddableIdsA.length !== embeddableIdsB.length ||
|
||||
_.xor(embeddableIdsA, embeddableIdsB).length > 0
|
||||
) {
|
||||
const getPanelsAreEqual = async (
|
||||
originalPanels: DashboardPanelMap,
|
||||
newPanels: DashboardPanelMap,
|
||||
getEmbeddable: (id: string) => Promise<IEmbeddable>
|
||||
): Promise<boolean> => {
|
||||
const originalEmbeddableIds = Object.keys(originalPanels);
|
||||
const newEmbeddableIds = Object.keys(newPanels);
|
||||
|
||||
const embeddableIdDiff = xor(originalEmbeddableIds, newEmbeddableIds);
|
||||
if (embeddableIdDiff.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// embeddable ids are equal so let's compare individual panels.
|
||||
for (const id of embeddableIdsA) {
|
||||
const panelCommonDiff = commonDiff<DashboardPanelState>(
|
||||
panelsA[id] as unknown as DashboardDiffCommon,
|
||||
panelsB[id] as unknown as DashboardDiffCommon,
|
||||
['panelRefName', 'explicitInput']
|
||||
);
|
||||
if (
|
||||
Object.keys(panelCommonDiff).length > 0 ||
|
||||
!explicitInputIsEqual(panelsA[id].explicitInput, panelsB[id].explicitInput)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
for (const embeddableId of newEmbeddableIds) {
|
||||
const {
|
||||
explicitInput: originalExplicitInput,
|
||||
panelRefName: panelRefA,
|
||||
...commonPanelDiffOriginal
|
||||
} = originalPanels[embeddableId];
|
||||
const {
|
||||
explicitInput: newExplicitInput,
|
||||
panelRefName: panelRefB,
|
||||
...commonPanelDiffNew
|
||||
} = newPanels[embeddableId];
|
||||
|
||||
if (!isEmpty(commonDiff(commonPanelDiffOriginal, commonPanelDiffNew))) return false;
|
||||
|
||||
// the position and type of this embeddable is equal. Now we compare the embeddable input
|
||||
const embeddable = await getEmbeddable(embeddableId);
|
||||
if (!(await embeddable.getExplicitInputIsEqual(originalExplicitInput))) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Need to compare properties of explicitInput *directly* in order to handle special comparisons for 'title'
|
||||
* and 'hidePanelTitles.' For example, if some object 'obj1' has 'obj1[title] = undefined' and some other
|
||||
* object `obj2' simply does not have the key `title,' we want obj1 to still equal obj2 - in normal comparisons
|
||||
* without this special case, `obj1 != obj2.'
|
||||
* @param originalInput
|
||||
* @param newInput
|
||||
*/
|
||||
const explicitInputIsEqual = (
|
||||
originalInput: EmbeddableInput,
|
||||
newInput: EmbeddableInput
|
||||
): boolean => {
|
||||
const diffs = commonDiff<DashboardPanelState>(originalInput, newInput, [
|
||||
'hidePanelTitles',
|
||||
'title',
|
||||
]);
|
||||
const hidePanelsAreEqual =
|
||||
Boolean(originalInput.hidePanelTitles) === Boolean(newInput.hidePanelTitles);
|
||||
const titlesAreEqual = originalInput.title === newInput.title;
|
||||
return Object.keys(diffs).length === 0 && hidePanelsAreEqual && titlesAreEqual;
|
||||
};
|
||||
|
||||
const commonDiffFilters = <T extends { filters: Filter[] }>(
|
||||
originalObj: DashboardDiffCommonFilters,
|
||||
newObj: DashboardDiffCommonFilters,
|
||||
omitKeys: string[],
|
||||
ignorePinned?: boolean
|
||||
): Partial<T> => {
|
||||
const filtersAreDifferent = () =>
|
||||
!esFilters.compareFilters(
|
||||
originalObj.filters,
|
||||
ignorePinned ? newObj.filters.filter((f) => !esFilters.isFilterPinned(f)) : newObj.filters,
|
||||
esFilters.COMPARE_ALL_OPTIONS
|
||||
);
|
||||
const otherDifferences = commonDiff<T>(originalObj, newObj, [...omitKeys, 'filters']);
|
||||
return _.cloneDeep({
|
||||
...otherDifferences,
|
||||
...(filtersAreDifferent() ? { filters: newObj.filters } : {}),
|
||||
});
|
||||
};
|
||||
|
||||
const commonDiff = <T>(
|
||||
originalObj: DashboardDiffCommon,
|
||||
newObj: DashboardDiffCommon,
|
||||
omitKeys: string[]
|
||||
) => {
|
||||
const commonDiff = <T>(originalObj: Partial<T>, newObj: Partial<T>) => {
|
||||
const differences: Partial<T> = {};
|
||||
const keys = [...Object.keys(originalObj), ...Object.keys(newObj)].filter(
|
||||
(key) => !omitKeys.includes(key)
|
||||
);
|
||||
keys.forEach((key) => {
|
||||
if (key === undefined) return;
|
||||
if (!_.isEqual(originalObj[key], newObj[key])) {
|
||||
(differences as { [key: string]: unknown })[key] = newObj[key];
|
||||
}
|
||||
});
|
||||
const keys = [
|
||||
...(Object.keys(originalObj) as Array<keyof T>),
|
||||
...(Object.keys(newObj) as Array<keyof T>),
|
||||
];
|
||||
for (const key of keys) {
|
||||
if (key === undefined) continue;
|
||||
if (!fastIsEqual(originalObj[key], newObj[key])) differences[key] = newObj[key];
|
||||
}
|
||||
return differences;
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ export { getDashboardIdFromUrl } from './url';
|
|||
export { saveDashboard } from './save_dashboard';
|
||||
export { migrateAppState } from './migrate_app_state';
|
||||
export { addHelpMenuToAppChrome } from './help_menu_util';
|
||||
export { diffDashboardState } from './diff_dashboard_state';
|
||||
export { getTagsFromSavedDashboard } from './dashboard_tagging';
|
||||
export { syncDashboardUrlState } from './sync_dashboard_url_state';
|
||||
export { DashboardSessionStorage } from './dashboard_session_storage';
|
||||
|
@ -19,7 +20,6 @@ export { attemptLoadDashboardByTitle } from './load_dashboard_by_title';
|
|||
export { syncDashboardFilterState } from './sync_dashboard_filter_state';
|
||||
export { syncDashboardIndexPatterns } from './sync_dashboard_index_patterns';
|
||||
export { syncDashboardContainerInput } from './sync_dashboard_container_input';
|
||||
export { diffDashboardContainerInput, diffDashboardState } from './diff_dashboard_state';
|
||||
export { loadDashboardHistoryLocationState } from './load_dashboard_history_location_state';
|
||||
export { buildDashboardContainer, tryDestroyDashboardContainer } from './build_dashboard_container';
|
||||
export {
|
||||
|
|
|
@ -65,6 +65,8 @@ export {
|
|||
VALUE_CLICK_TRIGGER,
|
||||
ViewMode,
|
||||
withEmbeddableSubscription,
|
||||
genericEmbeddableInputIsEqual,
|
||||
omitGenericEmbeddableInput,
|
||||
isSavedObjectEmbeddableInput,
|
||||
isRangeSelectTriggerContext,
|
||||
isValueClickTriggerContext,
|
||||
|
|
|
@ -15,8 +15,6 @@ import {
|
|||
EmbeddableInput,
|
||||
SavedObjectEmbeddableInput,
|
||||
isSavedObjectEmbeddableInput,
|
||||
IEmbeddable,
|
||||
Container,
|
||||
EmbeddableFactoryNotFoundError,
|
||||
EmbeddableFactory,
|
||||
} from '../index';
|
||||
|
@ -134,11 +132,6 @@ export class AttributeService<
|
|||
return isSavedObjectEmbeddableInput(input);
|
||||
};
|
||||
|
||||
public getExplicitInputFromEmbeddable(embeddable: IEmbeddable): ValType | RefType {
|
||||
return ((embeddable.getRoot() as Container).getInput()?.panels?.[embeddable.id]
|
||||
?.explicitInput ?? embeddable.getInput()) as ValType | RefType;
|
||||
}
|
||||
|
||||
getInputAsValueType = async (input: ValType | RefType): Promise<ValType> => {
|
||||
if (!this.inputIsRefType(input)) {
|
||||
return input as ValType;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import uuid from 'uuid';
|
||||
import { isEqual, xor } from 'lodash';
|
||||
import { merge, Subscription } from 'rxjs';
|
||||
import { startWith, pairwise } from 'rxjs/operators';
|
||||
import {
|
||||
|
@ -16,6 +17,7 @@ import {
|
|||
ErrorEmbeddable,
|
||||
EmbeddableFactory,
|
||||
IEmbeddable,
|
||||
isErrorEmbeddable,
|
||||
} from '../embeddables';
|
||||
import { IContainer, ContainerInput, ContainerOutput, PanelState } from './i_container';
|
||||
import { PanelNotFoundError, EmbeddableFactoryNotFoundError } from '../errors';
|
||||
|
@ -195,6 +197,32 @@ export abstract class Container<
|
|||
});
|
||||
}
|
||||
|
||||
public async getExplicitInputIsEqual(lastInput: TContainerInput) {
|
||||
const { panels: lastPanels, ...restOfLastInput } = lastInput;
|
||||
const { panels: currentPanels, ...restOfCurrentInput } = this.getInput();
|
||||
const otherInputIsEqual = isEqual(restOfLastInput, restOfCurrentInput);
|
||||
if (!otherInputIsEqual) return false;
|
||||
|
||||
const embeddableIdsA = Object.keys(lastPanels);
|
||||
const embeddableIdsB = Object.keys(currentPanels);
|
||||
if (
|
||||
embeddableIdsA.length !== embeddableIdsB.length ||
|
||||
xor(embeddableIdsA, embeddableIdsB).length > 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// embeddable ids are equal so let's compare individual panels.
|
||||
for (const id of embeddableIdsA) {
|
||||
const currentEmbeddable = await this.untilEmbeddableLoaded(id);
|
||||
const lastPanelInput = lastPanels[id].explicitInput;
|
||||
if (isErrorEmbeddable(currentEmbeddable)) continue;
|
||||
if (!(await currentEmbeddable.getExplicitInputIsEqual(lastPanelInput))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected createNewPanelState<
|
||||
TEmbeddableInput extends EmbeddableInput,
|
||||
TEmbeddable extends IEmbeddable<TEmbeddableInput, any>
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ViewMode } from '..';
|
||||
import { KibanaExecutionContext } from '../../../../../core/types';
|
||||
import { EmbeddableInput, omitGenericEmbeddableInput, genericEmbeddableInputIsEqual } from '.';
|
||||
|
||||
const getGenericEmbeddableState = (state?: Partial<EmbeddableInput>): EmbeddableInput => {
|
||||
const defaultState: EmbeddableInput = {
|
||||
lastReloadRequestTime: 1,
|
||||
executionContext: {} as KibanaExecutionContext,
|
||||
searchSessionId: 'what a session',
|
||||
hidePanelTitles: false,
|
||||
disabledActions: [],
|
||||
disableTriggers: false,
|
||||
enhancements: undefined,
|
||||
syncColors: false,
|
||||
viewMode: ViewMode.VIEW,
|
||||
title: 'So Very Generic',
|
||||
id: 'soVeryGeneric',
|
||||
};
|
||||
return { ...defaultState, ...state };
|
||||
};
|
||||
|
||||
test('Omitting generic embeddable input omits all generic input keys', () => {
|
||||
const superEmbeddableSpecificInput = {
|
||||
SuperInputKeyA: 'I am so specific',
|
||||
SuperInputKeyB: 'I am extremely specific',
|
||||
};
|
||||
const fullInput = { ...getGenericEmbeddableState(), ...superEmbeddableSpecificInput };
|
||||
const omittedState = omitGenericEmbeddableInput(fullInput);
|
||||
|
||||
const genericInputKeysToRemove: Array<keyof EmbeddableInput> = [
|
||||
'lastReloadRequestTime',
|
||||
'executionContext',
|
||||
'searchSessionId',
|
||||
'hidePanelTitles',
|
||||
'disabledActions',
|
||||
'disableTriggers',
|
||||
'enhancements',
|
||||
'syncColors',
|
||||
'viewMode',
|
||||
'title',
|
||||
'id',
|
||||
];
|
||||
for (const key of genericInputKeysToRemove) {
|
||||
expect((omittedState as unknown as EmbeddableInput)[key]).toBeUndefined();
|
||||
}
|
||||
|
||||
expect(omittedState.SuperInputKeyA).toBeDefined();
|
||||
expect(omittedState.SuperInputKeyB).toBeDefined();
|
||||
});
|
||||
|
||||
describe('Generic embeddable input diff function', () => {
|
||||
it('considers blank string title to be distinct from undefined title', () => {
|
||||
const genericInputWithUndefinedTitle = getGenericEmbeddableState();
|
||||
genericInputWithUndefinedTitle.title = undefined;
|
||||
expect(
|
||||
genericEmbeddableInputIsEqual(
|
||||
getGenericEmbeddableState({ title: '' }),
|
||||
genericInputWithUndefinedTitle
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('considers missing title key to be equal to input with undefined title', () => {
|
||||
const genericInputWithUndefinedTitle = getGenericEmbeddableState();
|
||||
genericInputWithUndefinedTitle.title = undefined;
|
||||
const genericInputWithDeletedTitle = getGenericEmbeddableState();
|
||||
delete genericInputWithDeletedTitle.title;
|
||||
expect(
|
||||
genericEmbeddableInputIsEqual(genericInputWithDeletedTitle, genericInputWithUndefinedTitle)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('considers hide panel titles false to be equal to hide panel titles undefined', () => {
|
||||
const genericInputWithUndefinedShowPanelTitles = getGenericEmbeddableState();
|
||||
genericInputWithUndefinedShowPanelTitles.hidePanelTitles = undefined;
|
||||
expect(
|
||||
genericEmbeddableInputIsEqual(
|
||||
getGenericEmbeddableState(),
|
||||
genericInputWithUndefinedShowPanelTitles
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('ignores differences in viewMode', () => {
|
||||
expect(
|
||||
genericEmbeddableInputIsEqual(
|
||||
getGenericEmbeddableState(),
|
||||
getGenericEmbeddableState({ viewMode: ViewMode.EDIT })
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('ignores differences in searchSessionId', () => {
|
||||
expect(
|
||||
genericEmbeddableInputIsEqual(
|
||||
getGenericEmbeddableState(),
|
||||
getGenericEmbeddableState({ searchSessionId: 'What a lovely session!' })
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import fastIsEqual from 'fast-deep-equal';
|
||||
import { pick, omit } from 'lodash';
|
||||
import { EmbeddableInput } from '.';
|
||||
|
||||
// list out the keys from the EmbeddableInput type to allow lodash to pick them later
|
||||
const allGenericInputKeys: Readonly<Array<keyof EmbeddableInput>> = [
|
||||
'lastReloadRequestTime',
|
||||
'executionContext',
|
||||
'searchSessionId',
|
||||
'hidePanelTitles',
|
||||
'disabledActions',
|
||||
'disableTriggers',
|
||||
'enhancements',
|
||||
'syncColors',
|
||||
'viewMode',
|
||||
'title',
|
||||
'id',
|
||||
] as const;
|
||||
|
||||
const genericInputKeysToCompare = [
|
||||
'hidePanelTitles',
|
||||
'disabledActions',
|
||||
'disableTriggers',
|
||||
'enhancements',
|
||||
'syncColors',
|
||||
'title',
|
||||
'id',
|
||||
] as const;
|
||||
|
||||
// type used to ensure that only keys present in EmbeddableInput are extracted
|
||||
type GenericEmbedableInputToCompare = Pick<
|
||||
EmbeddableInput,
|
||||
typeof genericInputKeysToCompare[number]
|
||||
>;
|
||||
|
||||
export const omitGenericEmbeddableInput = <
|
||||
I extends Partial<EmbeddableInput> = Partial<EmbeddableInput>
|
||||
>(
|
||||
input: I
|
||||
): Omit<I, keyof EmbeddableInput> => omit(input, allGenericInputKeys);
|
||||
|
||||
export const genericEmbeddableInputIsEqual = (
|
||||
currentInput: Partial<EmbeddableInput>,
|
||||
lastInput: Partial<EmbeddableInput>
|
||||
) => {
|
||||
const {
|
||||
title: currentTitle,
|
||||
hidePanelTitles: currentHidePanelTitles,
|
||||
...current
|
||||
} = pick(currentInput as GenericEmbedableInputToCompare, genericInputKeysToCompare);
|
||||
const {
|
||||
title: lastTitle,
|
||||
hidePanelTitles: lastHidePanelTitles,
|
||||
...last
|
||||
} = pick(lastInput as GenericEmbedableInputToCompare, genericInputKeysToCompare);
|
||||
|
||||
if (currentTitle !== lastTitle) return false;
|
||||
if (Boolean(currentHidePanelTitles) !== Boolean(lastHidePanelTitles)) return false;
|
||||
if (!fastIsEqual(current, last)) return false;
|
||||
return true;
|
||||
};
|
|
@ -6,7 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { cloneDeep, isEqual } from 'lodash';
|
||||
import fastIsEqual from 'fast-deep-equal';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import * as Rx from 'rxjs';
|
||||
import { merge } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, map, skip } from 'rxjs/operators';
|
||||
|
@ -15,11 +16,11 @@ import { Adapters } from '../types';
|
|||
import { IContainer } from '../containers';
|
||||
import { EmbeddableOutput, IEmbeddable } from './i_embeddable';
|
||||
import { EmbeddableInput, ViewMode } from '../../../common/types';
|
||||
import { genericEmbeddableInputIsEqual, omitGenericEmbeddableInput } from './diff_embeddable_input';
|
||||
|
||||
function getPanelTitle(input: EmbeddableInput, output: EmbeddableOutput) {
|
||||
return input.hidePanelTitles ? '' : input.title === undefined ? output.defaultTitle : input.title;
|
||||
}
|
||||
|
||||
export abstract class Embeddable<
|
||||
TEmbeddableInput extends EmbeddableInput = EmbeddableInput,
|
||||
TEmbeddableOutput extends EmbeddableOutput = EmbeddableOutput
|
||||
|
@ -131,6 +132,33 @@ export abstract class Embeddable<
|
|||
return this.output;
|
||||
}
|
||||
|
||||
public async getExplicitInputIsEqual(
|
||||
lastExplicitInput: Partial<TEmbeddableInput>
|
||||
): Promise<boolean> {
|
||||
const currentExplicitInput = this.getExplicitInput();
|
||||
return (
|
||||
genericEmbeddableInputIsEqual(lastExplicitInput, currentExplicitInput) &&
|
||||
fastIsEqual(
|
||||
omitGenericEmbeddableInput(lastExplicitInput),
|
||||
omitGenericEmbeddableInput(currentExplicitInput)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public getExplicitInput() {
|
||||
const root = this.getRoot();
|
||||
if (root.getIsContainer()) {
|
||||
return (
|
||||
(root.getInput().panels?.[this.id]?.explicitInput as TEmbeddableInput) ?? this.getInput()
|
||||
);
|
||||
}
|
||||
return this.getInput();
|
||||
}
|
||||
|
||||
public getPersistableInput() {
|
||||
return this.getExplicitInput();
|
||||
}
|
||||
|
||||
public getInput(): Readonly<TEmbeddableInput> {
|
||||
return this.input;
|
||||
}
|
||||
|
@ -213,7 +241,7 @@ export abstract class Embeddable<
|
|||
...this.output,
|
||||
...outputChanges,
|
||||
};
|
||||
if (!isEqual(this.output, newOutput)) {
|
||||
if (!fastIsEqual(this.output, newOutput)) {
|
||||
this.output = newOutput;
|
||||
this.output$.next(this.output);
|
||||
}
|
||||
|
@ -230,7 +258,7 @@ export abstract class Embeddable<
|
|||
}
|
||||
|
||||
private onResetInput(newInput: TEmbeddableInput) {
|
||||
if (!isEqual(this.input, newInput)) {
|
||||
if (!fastIsEqual(this.input, newInput)) {
|
||||
const oldLastReloadRequestTime = this.input.lastReloadRequestTime;
|
||||
this.input = newInput;
|
||||
this.input$.next(newInput);
|
||||
|
|
|
@ -103,6 +103,20 @@ export interface IEmbeddable<
|
|||
**/
|
||||
getInput(): Readonly<I>;
|
||||
|
||||
/**
|
||||
* Because embeddables can inherit input from their parents, they also need a way to separate their own
|
||||
* input from input which is inherited. If the embeddable does not have a parent, getExplicitInput
|
||||
* and getInput should return the same.
|
||||
**/
|
||||
getExplicitInput(): Readonly<Partial<I>>;
|
||||
|
||||
/**
|
||||
* Some embeddables contain input that should not be persisted anywhere beyond their own state. This method
|
||||
* is a way for containers to separate input to store from input which can be ephemeral. In most cases, this
|
||||
* will be the same as getExplicitInput
|
||||
**/
|
||||
getPersistableInput(): Readonly<Partial<I>>;
|
||||
|
||||
/**
|
||||
* Output state is:
|
||||
*
|
||||
|
@ -170,4 +184,9 @@ export interface IEmbeddable<
|
|||
* List of triggers that this embeddable will execute.
|
||||
*/
|
||||
supportedTriggers(): string[];
|
||||
|
||||
/**
|
||||
* Used to diff explicit embeddable input
|
||||
*/
|
||||
getExplicitInputIsEqual(lastInput: Partial<I>): Promise<boolean>;
|
||||
}
|
||||
|
|
|
@ -18,3 +18,4 @@ export { EmbeddableRoot } from './embeddable_root';
|
|||
export * from '../../../common/lib/saved_object_embeddable';
|
||||
export type { EmbeddableRendererProps } from './embeddable_renderer';
|
||||
export { EmbeddableRenderer, useEmbeddableFactory } from './embeddable_renderer';
|
||||
export { genericEmbeddableInputIsEqual, omitGenericEmbeddableInput } from './diff_embeddable_input';
|
||||
|
|
|
@ -610,16 +610,14 @@ export class Embeddable
|
|||
};
|
||||
|
||||
public getInputAsRefType = async (): Promise<LensByReferenceInput> => {
|
||||
const input = this.deps.attributeService.getExplicitInputFromEmbeddable(this);
|
||||
return this.deps.attributeService.getInputAsRefType(input, {
|
||||
return this.deps.attributeService.getInputAsRefType(this.getExplicitInput(), {
|
||||
showSaveModal: true,
|
||||
saveModalTitle: this.getTitle(),
|
||||
});
|
||||
};
|
||||
|
||||
public getInputAsValueType = async (): Promise<LensByValueInput> => {
|
||||
const input = this.deps.attributeService.getExplicitInputFromEmbeddable(this);
|
||||
return this.deps.attributeService.getInputAsValueType(input);
|
||||
return this.deps.attributeService.getInputAsValueType(this.getExplicitInput());
|
||||
};
|
||||
|
||||
// same API as Visualize
|
||||
|
|
|
@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import fastIsEqual from 'fast-deep-equal';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Unsubscribe } from 'redux';
|
||||
|
@ -18,7 +19,9 @@ import {
|
|||
Embeddable,
|
||||
IContainer,
|
||||
ReferenceOrValueEmbeddable,
|
||||
genericEmbeddableInputIsEqual,
|
||||
VALUE_CLICK_TRIGGER,
|
||||
omitGenericEmbeddableInput,
|
||||
} from '../../../../../src/plugins/embeddable/public';
|
||||
import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/public';
|
||||
import {
|
||||
|
@ -210,16 +213,26 @@ export class MapEmbeddable
|
|||
}
|
||||
|
||||
public async getInputAsRefType(): Promise<MapByReferenceInput> {
|
||||
const input = getMapAttributeService().getExplicitInputFromEmbeddable(this);
|
||||
return getMapAttributeService().getInputAsRefType(input, {
|
||||
return getMapAttributeService().getInputAsRefType(this.getExplicitInput(), {
|
||||
showSaveModal: true,
|
||||
saveModalTitle: this.getTitle(),
|
||||
});
|
||||
}
|
||||
|
||||
public async getExplicitInputIsEqual(
|
||||
lastExplicitInput: Partial<MapByValueInput | MapByReferenceInput>
|
||||
): Promise<boolean> {
|
||||
const currentExplicitInput = this.getExplicitInput();
|
||||
if (!genericEmbeddableInputIsEqual(lastExplicitInput, currentExplicitInput)) return false;
|
||||
|
||||
// generic embeddable input is equal, now we compare map specific input elements, ignoring 'mapBuffer'.
|
||||
const lastMapInput = omitGenericEmbeddableInput(_.omit(lastExplicitInput, 'mapBuffer'));
|
||||
const currentMapInput = omitGenericEmbeddableInput(_.omit(currentExplicitInput, 'mapBuffer'));
|
||||
return fastIsEqual(lastMapInput, currentMapInput);
|
||||
}
|
||||
|
||||
public async getInputAsValueType(): Promise<MapByValueInput> {
|
||||
const input = getMapAttributeService().getExplicitInputFromEmbeddable(this);
|
||||
return getMapAttributeService().getInputAsValueType(input);
|
||||
return getMapAttributeService().getInputAsValueType(this.getExplicitInput());
|
||||
}
|
||||
|
||||
public getDescription() {
|
||||
|
|
|
@ -98,6 +98,9 @@ export class MapsPlugin implements Plugin {
|
|||
embeddableType: MAP_SAVED_OBJECT_TYPE,
|
||||
embeddableConfig: {
|
||||
isLayerTOCOpen: false,
|
||||
hiddenLayers: [],
|
||||
mapCenter: { lat: 45.88578, lon: -15.07605, zoom: 2.11 },
|
||||
openTOCDetails: [],
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -124,6 +127,9 @@ export class MapsPlugin implements Plugin {
|
|||
embeddableType: MAP_SAVED_OBJECT_TYPE,
|
||||
embeddableConfig: {
|
||||
isLayerTOCOpen: true,
|
||||
hiddenLayers: [],
|
||||
mapCenter: { lat: 48.72307, lon: -115.18171, zoom: 4.28 },
|
||||
openTOCDetails: [],
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -148,6 +154,9 @@ export class MapsPlugin implements Plugin {
|
|||
embeddableType: MAP_SAVED_OBJECT_TYPE,
|
||||
embeddableConfig: {
|
||||
isLayerTOCOpen: false,
|
||||
hiddenLayers: [],
|
||||
mapCenter: { lat: 42.16337, lon: -88.92107, zoom: 3.64 },
|
||||
openTOCDetails: [],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue