[Dashboard] Move all dashboard extract/inject into persistable state (#96095)

* Move all dashboard inject/extract to be part of embeddable persistable state

* Fixes typescript errors

* Remove comments

* Fixes test

* API Doc changes

* Fix integration tests

* Fix functional testS

* Fix unit tests

* Update Dashboard plugin API to get dashboard embeddable renderer

* Fix Types

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Corey Robertson 2021-04-12 12:25:03 -04:00 committed by GitHub
parent f544d8d458
commit b645fec8b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 966 additions and 347 deletions

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-embeddable-server](./kibana-plugin-plugins-embeddable-server.md) &gt; [EmbeddableStart](./kibana-plugin-plugins-embeddable-server.embeddablestart.md)
## EmbeddableStart type
<b>Signature:</b>
```typescript
export declare type EmbeddableStart = PersistableStateService<EmbeddableStateWithType>;
```

View file

@ -18,3 +18,9 @@
| --- | --- |
| [plugin](./kibana-plugin-plugins-embeddable-server.plugin.md) | |
## Type Aliases
| Type Alias | Description |
| --- | --- |
| [EmbeddableStart](./kibana-plugin-plugins-embeddable-server.embeddablestart.md) | |

View file

@ -55,7 +55,9 @@ const Nav = withRouter(({ history, pages }: NavProps) => {
interface Props {
basename: string;
DashboardContainerByValueRenderer: DashboardStart['DashboardContainerByValueRenderer'];
DashboardContainerByValueRenderer: ReturnType<
DashboardStart['getDashboardContainerByValueRenderer']
>;
}
const DashboardEmbeddableExplorerApp = ({ basename, DashboardContainerByValueRenderer }: Props) => {

View file

@ -96,7 +96,9 @@ const initialInput: DashboardContainerInput = {
export const DashboardEmbeddableByValue = ({
DashboardContainerByValueRenderer,
}: {
DashboardContainerByValueRenderer: DashboardStart['DashboardContainerByValueRenderer'];
DashboardContainerByValueRenderer: ReturnType<
DashboardStart['getDashboardContainerByValueRenderer']
>;
}) => {
const [input, setInput] = useState(initialInput);

View file

@ -33,8 +33,7 @@ export class DashboardEmbeddableExamples implements Plugin<void, void, {}, Start
return renderApp(
{
basename: params.appBasePath,
DashboardContainerByValueRenderer:
depsStart.dashboard.DashboardContainerByValueRenderer,
DashboardContainerByValueRenderer: depsStart.dashboard.getDashboardContainerByValueRenderer(),
},
params.element
);

View file

@ -78,6 +78,7 @@ export type RawSavedDashboardPanel730ToLatest = Pick<
readonly name?: string;
panelIndex: string;
panelRefName?: string;
};
// NOTE!!

View file

@ -0,0 +1,158 @@
/*
* 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 { createExtract, createInject } from './dashboard_container_persistable_state';
import { createEmbeddablePersistableStateServiceMock } from '../../../embeddable/common/mocks';
import { DashboardContainerStateWithType } from '../types';
const persistableStateService = createEmbeddablePersistableStateServiceMock();
const dashboardWithExtractedPanel: DashboardContainerStateWithType = {
id: 'id',
type: 'dashboard',
panels: {
panel_1: {
type: 'panel_type',
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
panelRefName: 'panel_panel_1',
explicitInput: {
id: 'panel_1',
},
},
},
};
const extractedSavedObjectPanelRef = {
name: 'panel_1:panel_panel_1',
type: 'panel_type',
id: 'object-id',
};
const unextractedDashboardState: DashboardContainerStateWithType = {
id: 'id',
type: 'dashboard',
panels: {
panel_1: {
type: 'panel_type',
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
explicitInput: {
id: 'panel_1',
savedObjectId: 'object-id',
},
},
},
};
describe('inject/extract by reference panel', () => {
it('should inject the extracted saved object panel', () => {
const inject = createInject(persistableStateService);
const references = [extractedSavedObjectPanelRef];
const injected = inject(
dashboardWithExtractedPanel,
references
) as DashboardContainerStateWithType;
expect(injected).toEqual(unextractedDashboardState);
});
it('should extract the saved object panel', () => {
const extract = createExtract(persistableStateService);
const { state: extractedState, references: extractedReferences } = extract(
unextractedDashboardState
);
expect(extractedState).toEqual(dashboardWithExtractedPanel);
expect(extractedReferences[0]).toEqual(extractedSavedObjectPanelRef);
});
});
const dashboardWithExtractedByValuePanel: DashboardContainerStateWithType = {
id: 'id',
type: 'dashboard',
panels: {
panel_1: {
type: 'panel_type',
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
explicitInput: {
id: 'panel_1',
extracted_reference: 'ref',
},
},
},
};
const extractedByValueRef = {
id: 'id',
name: 'panel_1:ref',
type: 'panel_type',
};
const unextractedDashboardByValueState: DashboardContainerStateWithType = {
id: 'id',
type: 'dashboard',
panels: {
panel_1: {
type: 'panel_type',
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
explicitInput: {
id: 'panel_1',
value: 'id',
},
},
},
};
describe('inject/extract by value panels', () => {
it('should inject the extracted references', () => {
const inject = createInject(persistableStateService);
persistableStateService.inject.mockImplementationOnce((state, references) => {
const ref = references.find((r) => r.name === 'ref');
if (!ref) {
return state;
}
if (('extracted_reference' in state) as any) {
(state as any).value = ref.id;
delete (state as any).extracted_reference;
}
return state;
});
const injectedState = inject(dashboardWithExtractedByValuePanel, [extractedByValueRef]);
expect(injectedState).toEqual(unextractedDashboardByValueState);
});
it('should extract references using persistable state', () => {
const extract = createExtract(persistableStateService);
persistableStateService.extract.mockImplementationOnce((state) => {
if ((state as any).value === 'id') {
delete (state as any).value;
(state as any).extracted_reference = 'ref';
return {
state,
references: [{ id: extractedByValueRef.id, name: 'ref', type: extractedByValueRef.type }],
};
}
return { state, references: [] };
});
const { state: extractedState, references: extractedReferences } = extract(
unextractedDashboardByValueState
);
expect(extractedState).toEqual(dashboardWithExtractedByValuePanel);
expect(extractedReferences).toEqual([extractedByValueRef]);
});
});

View file

@ -0,0 +1,125 @@
/*
* 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 {
EmbeddableInput,
EmbeddablePersistableStateService,
EmbeddableStateWithType,
} from '../../../embeddable/common';
import { SavedObjectReference } from '../../../../core/types';
import { DashboardContainerStateWithType, DashboardPanelState } from '../types';
const getPanelStatePrefix = (state: DashboardPanelState) => `${state.explicitInput.id}:`;
export const createInject = (
persistableStateService: EmbeddablePersistableStateService
): EmbeddablePersistableStateService['inject'] => {
return (state: EmbeddableStateWithType, references: SavedObjectReference[]) => {
const workingState = { ...state } as EmbeddableStateWithType | DashboardContainerStateWithType;
if ('panels' in workingState) {
workingState.panels = { ...workingState.panels };
for (const [key, panel] of Object.entries(workingState.panels)) {
workingState.panels[key] = { ...panel };
// Find the references for this panel
const prefix = getPanelStatePrefix(panel);
const filteredReferences = references
.filter((reference) => reference.name.indexOf(prefix) === 0)
.map((reference) => ({ ...reference, name: reference.name.replace(prefix, '') }));
const panelReferences = filteredReferences.length === 0 ? references : filteredReferences;
// Inject dashboard references back in
if (panel.panelRefName !== undefined) {
const matchingReference = panelReferences.find(
(reference) => reference.name === panel.panelRefName
);
if (!matchingReference) {
throw new Error(`Could not find reference "${panel.panelRefName}"`);
}
if (matchingReference !== undefined) {
workingState.panels[key] = {
...panel,
type: matchingReference.type,
explicitInput: {
...workingState.panels[key].explicitInput,
savedObjectId: matchingReference.id,
},
};
delete workingState.panels[key].panelRefName;
}
}
const { type, ...injectedState } = persistableStateService.inject(
{ ...workingState.panels[key].explicitInput, type: workingState.panels[key].type },
panelReferences
);
workingState.panels[key].explicitInput = injectedState as EmbeddableInput;
}
}
return workingState as EmbeddableStateWithType;
};
};
export const createExtract = (
persistableStateService: EmbeddablePersistableStateService
): EmbeddablePersistableStateService['extract'] => {
return (state: EmbeddableStateWithType) => {
const workingState = { ...state } as EmbeddableStateWithType | DashboardContainerStateWithType;
const references: SavedObjectReference[] = [];
if ('panels' in workingState) {
workingState.panels = { ...workingState.panels };
// Run every panel through the state service to get the nested references
for (const [key, panel] of Object.entries(workingState.panels)) {
const prefix = getPanelStatePrefix(panel);
// If the panel is a saved object, then we will make the reference for that saved object and change the explicit input
if (panel.explicitInput.savedObjectId) {
panel.panelRefName = `panel_${key}`;
references.push({
name: `${prefix}panel_${key}`,
type: panel.type,
id: panel.explicitInput.savedObjectId as string,
});
delete panel.explicitInput.savedObjectId;
delete panel.explicitInput.type;
}
const { state: panelState, references: panelReferences } = persistableStateService.extract({
...panel.explicitInput,
type: panel.type,
});
// We're going to prefix the names of the references so that we don't end up with dupes (from visualizations for instance)
const prefixedReferences = panelReferences.map((reference) => ({
...reference,
name: `${prefix}${reference.name}`,
}));
references.push(...prefixedReferences);
const { type, ...restOfState } = panelState;
workingState.panels[key].explicitInput = restOfState as EmbeddableInput;
}
}
return { state: workingState as EmbeddableStateWithType, references };
};
};

View file

@ -16,6 +16,7 @@ export function convertSavedDashboardPanelToPanelState(
return {
type: savedDashboardPanel.type,
gridData: savedDashboardPanel.gridData,
panelRefName: savedDashboardPanel.panelRefName,
explicitInput: {
id: savedDashboardPanel.panelIndex,
...(savedDashboardPanel.id !== undefined && { savedObjectId: savedDashboardPanel.id }),
@ -38,5 +39,6 @@ export function convertPanelStateToSavedDashboardPanel(
embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId', 'title']),
...(panelState.explicitInput.title !== undefined && { title: panelState.explicitInput.title }),
...(savedObjectId !== undefined && { id: savedObjectId }),
...(panelState.panelRefName !== undefined && { panelRefName: panelState.panelRefName }),
};
}

View file

@ -14,6 +14,7 @@ export {
DashboardDocPre700,
} from './bwc/types';
export {
DashboardContainerStateWithType,
SavedDashboardPanelTo60,
SavedDashboardPanel610,
SavedDashboardPanel620,

View file

@ -12,14 +12,34 @@ import {
InjectDeps,
ExtractDeps,
} from './saved_dashboard_references';
import { createExtract, createInject } from './embeddable/dashboard_container_persistable_state';
import { createEmbeddablePersistableStateServiceMock } from '../../embeddable/common/mocks';
const embeddablePersistableStateServiceMock = createEmbeddablePersistableStateServiceMock();
const dashboardInject = createInject(embeddablePersistableStateServiceMock);
const dashboardExtract = createExtract(embeddablePersistableStateServiceMock);
embeddablePersistableStateServiceMock.extract.mockImplementation((state) => {
if (state.type === 'dashboard') {
return dashboardExtract(state);
}
return { state, references: [] };
});
embeddablePersistableStateServiceMock.inject.mockImplementation((state, references) => {
if (state.type === 'dashboard') {
return dashboardInject(state, references);
}
return state;
});
const deps: InjectDeps & ExtractDeps = {
embeddablePersistableStateService: embeddablePersistableStateServiceMock,
};
describe('extractReferences', () => {
describe('legacy extract references', () => {
test('extracts references from panelsJSON', () => {
const doc = {
id: '1',
@ -30,13 +50,13 @@ describe('extractReferences', () => {
type: 'visualization',
id: '1',
title: 'Title 1',
version: '7.9.1',
version: '7.0.0',
},
{
type: 'visualization',
id: '2',
title: 'Title 2',
version: '7.9.1',
version: '7.0.0',
},
]),
},
@ -48,7 +68,7 @@ describe('extractReferences', () => {
Object {
"attributes": Object {
"foo": true,
"panelsJSON": "[{\\"version\\":\\"7.9.1\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_0\\"},{\\"version\\":\\"7.9.1\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_1\\"}]",
"panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"version\\":\\"7.0.0\\",\\"panelRefName\\":\\"panel_0\\"},{\\"title\\":\\"Title 2\\",\\"version\\":\\"7.0.0\\",\\"panelRefName\\":\\"panel_1\\"}]",
},
"references": Array [
Object {
@ -75,7 +95,7 @@ describe('extractReferences', () => {
{
id: '1',
title: 'Title 1',
version: '7.9.1',
version: '7.0.0',
},
]),
},
@ -186,6 +206,102 @@ describe('extractReferences', () => {
});
});
describe('extractReferences', () => {
test('extracts references from panelsJSON', () => {
const doc = {
id: '1',
attributes: {
foo: true,
panelsJSON: JSON.stringify([
{
panelIndex: 'panel-1',
type: 'visualization',
id: '1',
title: 'Title 1',
version: '7.9.1',
},
{
panelIndex: 'panel-2',
type: 'visualization',
id: '2',
title: 'Title 2',
version: '7.9.1',
},
]),
},
references: [],
};
const updatedDoc = extractReferences(doc, deps);
expect(updatedDoc).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"foo": true,
"panelsJSON": "[{\\"version\\":\\"7.9.1\\",\\"type\\":\\"visualization\\",\\"panelIndex\\":\\"panel-1\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_panel-1\\"},{\\"version\\":\\"7.9.1\\",\\"type\\":\\"visualization\\",\\"panelIndex\\":\\"panel-2\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_panel-2\\"}]",
},
"references": Array [
Object {
"id": "1",
"name": "panel-1:panel_panel-1",
"type": "visualization",
},
Object {
"id": "2",
"name": "panel-2:panel_panel-2",
"type": "visualization",
},
],
}
`);
});
test('fails when "type" attribute is missing from a panel', () => {
const doc = {
id: '1',
attributes: {
foo: true,
panelsJSON: JSON.stringify([
{
id: '1',
title: 'Title 1',
version: '7.9.1',
},
]),
},
references: [],
};
expect(() => extractReferences(doc, deps)).toThrowErrorMatchingInlineSnapshot(
`"\\"type\\" attribute is missing from panel \\"0\\""`
);
});
test('passes when "id" attribute is missing from a panel', () => {
const doc = {
id: '1',
attributes: {
foo: true,
panelsJSON: JSON.stringify([
{
type: 'visualization',
title: 'Title 1',
version: '7.9.1',
},
]),
},
references: [],
};
expect(extractReferences(doc, deps)).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"foo": true,
"panelsJSON": "[{\\"version\\":\\"7.9.1\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\"}]",
},
"references": Array [],
}
`);
});
});
describe('injectReferences', () => {
test('returns injected attributes', () => {
const attributes = {
@ -195,10 +311,12 @@ describe('injectReferences', () => {
{
panelRefName: 'panel_0',
title: 'Title 1',
version: '7.9.0',
},
{
panelRefName: 'panel_1',
title: 'Title 2',
version: '7.9.0',
},
]),
};
@ -219,7 +337,7 @@ describe('injectReferences', () => {
expect(newAttributes).toMatchInlineSnapshot(`
Object {
"id": "1",
"panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\"}]",
"panelsJSON": "[{\\"version\\":\\"7.9.0\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"version\\":\\"7.9.0\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\"}]",
"title": "test",
}
`);
@ -280,7 +398,7 @@ describe('injectReferences', () => {
expect(newAttributes).toMatchInlineSnapshot(`
Object {
"id": "1",
"panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\"}]",
"panelsJSON": "[{\\"version\\":\\"\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"version\\":\\"\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\"}]",
"title": "test",
}
`);

View file

@ -8,22 +8,71 @@
import semverSatisfies from 'semver/functions/satisfies';
import { SavedObjectAttributes, SavedObjectReference } from '../../../core/types';
import {
extractPanelsReferences,
injectPanelsReferences,
} from './embeddable/embeddable_references';
import { SavedDashboardPanel730ToLatest } from './types';
import { DashboardContainerStateWithType, DashboardPanelState } from './types';
import { EmbeddablePersistableStateService } from '../../embeddable/common/types';
import {
convertPanelStateToSavedDashboardPanel,
convertSavedDashboardPanelToPanelState,
} from './embeddable/embeddable_saved_object_converters';
import { SavedDashboardPanel } from './types';
export interface ExtractDeps {
embeddablePersistableStateService: EmbeddablePersistableStateService;
}
export interface SavedObjectAttributesAndReferences {
attributes: SavedObjectAttributes;
references: SavedObjectReference[];
}
const isPre730Panel = (panel: Record<string, string>): boolean => {
return 'version' in panel ? semverSatisfies(panel.version, '<7.3') : true;
};
function dashboardAttributesToState(
attributes: SavedObjectAttributes
): {
state: DashboardContainerStateWithType;
panels: SavedDashboardPanel[];
} {
let inputPanels = [] as SavedDashboardPanel[];
if (typeof attributes.panelsJSON === 'string') {
inputPanels = JSON.parse(attributes.panelsJSON) as SavedDashboardPanel[];
}
return {
panels: inputPanels,
state: {
id: attributes.id as string,
type: 'dashboard',
panels: inputPanels.reduce<Record<string, DashboardPanelState>>((current, panel, index) => {
const panelIndex = panel.panelIndex || `${index}`;
current[panelIndex] = convertSavedDashboardPanelToPanelState(panel);
return current;
}, {}),
},
};
}
function panelStatesToPanels(
panelStates: DashboardContainerStateWithType['panels'],
originalPanels: SavedDashboardPanel[]
): SavedDashboardPanel[] {
return Object.entries(panelStates).map(([id, panelState]) => {
// Find matching original panel to get the version
let originalPanel = originalPanels.find((p) => p.panelIndex === id);
if (!originalPanel) {
// Maybe original panel doesn't have a panel index and it's just straight up based on it's index
const numericId = parseInt(id, 10);
originalPanel = isNaN(numericId) ? originalPanel : originalPanels[numericId];
}
return convertPanelStateToSavedDashboardPanel(
panelState,
originalPanel?.version ? originalPanel.version : ''
);
});
}
export function extractReferences(
{ attributes, references = [] }: SavedObjectAttributesAndReferences,
deps: ExtractDeps
@ -31,64 +80,33 @@ export function extractReferences(
if (typeof attributes.panelsJSON !== 'string') {
return { attributes, references };
}
const panelReferences: SavedObjectReference[] = [];
let panels: Array<Record<string, string>> = JSON.parse(String(attributes.panelsJSON));
const isPre730Panel = (panel: Record<string, string>): boolean => {
return 'version' in panel ? semverSatisfies(panel.version, '<7.3') : true;
};
const { panels, state } = dashboardAttributesToState(attributes);
const hasPre730Panel = panels.some(isPre730Panel);
/**
* `extractPanelsReferences` only knows how to reliably handle "latest" panels
* It is possible that `extractReferences` is run on older dashboard SO with older panels,
* for example, when importing a saved object using saved object UI `extractReferences` is called BEFORE any server side migrations are run.
*
* In this case we skip running `extractPanelsReferences` on such object.
* We also know that there is nothing to extract
* (First possible entity to be extracted by this mechanism is a dashboard drilldown since 7.11)
*/
if (!hasPre730Panel) {
const extractedReferencesResult = extractPanelsReferences(
// it is ~safe~ to cast to `SavedDashboardPanel730ToLatest` because above we've checked that there are only >=7.3 panels
(panels as unknown) as SavedDashboardPanel730ToLatest[],
deps
);
panels = (extractedReferencesResult.map((res) => res.panel) as unknown) as Array<
Record<string, string>
>;
extractedReferencesResult.forEach((res) => {
panelReferences.push(...res.references);
});
if (((panels as unknown) as Array<Record<string, string>>).some(isPre730Panel)) {
return pre730ExtractReferences({ attributes, references }, deps);
}
// TODO: This extraction should be done by EmbeddablePersistableStateService
// https://github.com/elastic/kibana/issues/82830
panels.forEach((panel, i) => {
if (!panel.type) {
throw new Error(`"type" attribute is missing from panel "${i}"`);
}
if (!panel.id) {
// Embeddables are not required to be backed off a saved object.
return;
}
panel.panelRefName = `panel_${i}`;
panelReferences.push({
name: `panel_${i}`,
type: panel.type,
id: panel.id,
});
delete panel.type;
delete panel.id;
});
const missingTypeIndex = panels.findIndex((panel) => panel.type === undefined);
if (missingTypeIndex >= 0) {
throw new Error(`"type" attribute is missing from panel "${missingTypeIndex}"`);
}
const {
state: extractedState,
references: extractedReferences,
} = deps.embeddablePersistableStateService.extract(state);
const extractedPanels = panelStatesToPanels(
(extractedState as DashboardContainerStateWithType).panels,
panels
);
return {
references: [...references, ...panelReferences],
references: [...references, ...extractedReferences],
attributes: {
...attributes,
panelsJSON: JSON.stringify(panels),
panelsJSON: JSON.stringify(extractedPanels),
},
};
}
@ -107,33 +125,60 @@ export function injectReferences(
if (typeof attributes.panelsJSON !== 'string') {
return attributes;
}
let panels = JSON.parse(attributes.panelsJSON);
const parsedPanels = JSON.parse(attributes.panelsJSON);
// Same here, prevent failing saved object import if ever panels aren't an array.
if (!Array.isArray(panels)) {
if (!Array.isArray(parsedPanels)) {
return attributes;
}
// TODO: This injection should be done by EmbeddablePersistableStateService
// https://github.com/elastic/kibana/issues/82830
panels.forEach((panel) => {
if (!panel.panelRefName) {
return;
}
const reference = references.find((ref) => ref.name === panel.panelRefName);
if (!reference) {
// Throw an error since "panelRefName" means the reference exists within
// "references" and in this scenario we have bad data.
throw new Error(`Could not find reference "${panel.panelRefName}"`);
}
panel.id = reference.id;
panel.type = reference.type;
delete panel.panelRefName;
});
const { panels, state } = dashboardAttributesToState(attributes);
panels = injectPanelsReferences(panels, references, deps);
const injectedState = deps.embeddablePersistableStateService.inject(state, references);
const injectedPanels = panelStatesToPanels(
(injectedState as DashboardContainerStateWithType).panels,
panels
);
return {
...attributes,
panelsJSON: JSON.stringify(panels),
panelsJSON: JSON.stringify(injectedPanels),
};
}
function pre730ExtractReferences(
{ attributes, references = [] }: SavedObjectAttributesAndReferences,
deps: ExtractDeps
): SavedObjectAttributesAndReferences {
if (typeof attributes.panelsJSON !== 'string') {
return { attributes, references };
}
const panelReferences: SavedObjectReference[] = [];
const panels: Array<Record<string, string>> = JSON.parse(String(attributes.panelsJSON));
panels.forEach((panel, i) => {
if (!panel.type) {
throw new Error(`"type" attribute is missing from panel "${i}"`);
}
if (!panel.id) {
// Embeddables are not required to be backed off a saved object.
return;
}
panel.panelRefName = `panel_${i}`;
panelReferences.push({
name: `panel_${i}`,
type: panel.type,
id: panel.id,
});
delete panel.type;
delete panel.id;
});
return {
references: [...references, ...panelReferences],
attributes: {
...attributes,
panelsJSON: JSON.stringify(panels),
},
};
}

View file

@ -6,7 +6,11 @@
* Side Public License, v 1.
*/
import { EmbeddableInput, PanelState } from '../../../../src/plugins/embeddable/common/types';
import {
EmbeddableInput,
EmbeddableStateWithType,
PanelState,
} from '../../../../src/plugins/embeddable/common/types';
import { SavedObjectEmbeddableInput } from '../../../../src/plugins/embeddable/common/lib/saved_object_embeddable';
import {
RawSavedDashboardPanelTo60,
@ -25,6 +29,7 @@ export interface DashboardPanelState<
TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput
> extends PanelState<TEmbeddableInput> {
readonly gridData: GridData;
panelRefName?: string;
}
/**
@ -80,3 +85,11 @@ export type SavedDashboardPanel730ToLatest = Pick<
readonly id?: string;
readonly type: string;
};
// Making this interface because so much of the Container type from embeddable is tied up in public
// Once that is all available from common, we should be able to move the dashboard_container type to our common as well
export interface DashboardContainerStateWithType extends EmbeddableStateWithType {
panels: {
[panelId: string]: DashboardPanelState<EmbeddableInput & { [k: string]: unknown }>;
};
}

View file

@ -7,6 +7,7 @@
*/
import { i18n } from '@kbn/i18n';
import { EmbeddablePersistableStateService } from 'src/plugins/embeddable/common';
import {
Container,
ErrorEmbeddable,
@ -20,6 +21,10 @@ import {
DashboardContainerServices,
} from './dashboard_container';
import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants';
import {
createExtract,
createInject,
} from '../../../common/embeddable/dashboard_container_persistable_state';
export type DashboardContainerFactory = EmbeddableFactory<
DashboardContainerInput,
@ -32,7 +37,10 @@ export class DashboardContainerFactoryDefinition
public readonly isContainerType = true;
public readonly type = DASHBOARD_CONTAINER_TYPE;
constructor(private readonly getStartServices: () => Promise<DashboardContainerServices>) {}
constructor(
private readonly getStartServices: () => Promise<DashboardContainerServices>,
private readonly persistableStateService: EmbeddablePersistableStateService
) {}
public isEditable = async () => {
// Currently unused for dashboards
@ -62,4 +70,8 @@ export class DashboardContainerFactoryDefinition
const services = await this.getStartServices();
return new DashboardContainer(initialInput, services, parent);
};
public inject = createInject(this.persistableStateService);
public extract = createExtract(this.persistableStateService);
}

View file

@ -121,9 +121,11 @@ export type DashboardSetup = void;
export interface DashboardStart {
getSavedDashboardLoader: () => SavedObjectLoader;
getDashboardContainerByValueRenderer: () => ReturnType<
typeof createDashboardContainerByValueRenderer
>;
dashboardUrlGenerator?: DashboardUrlGenerator;
dashboardFeatureFlagConfig: DashboardFeatureFlagConfig;
DashboardContainerByValueRenderer: ReturnType<typeof createDashboardContainerByValueRenderer>;
}
export class DashboardPlugin
@ -260,8 +262,16 @@ export class DashboardPlugin
},
});
const dashboardContainerFactory = new DashboardContainerFactoryDefinition(getStartServices);
embeddable.registerEmbeddableFactory(dashboardContainerFactory.type, dashboardContainerFactory);
getStartServices().then((coreStart) => {
const dashboardContainerFactory = new DashboardContainerFactoryDefinition(
getStartServices,
coreStart.embeddable
);
embeddable.registerEmbeddableFactory(
dashboardContainerFactory.type,
dashboardContainerFactory
);
});
const placeholderFactory = new PlaceholderEmbeddableFactory();
embeddable.registerEmbeddableFactory(placeholderFactory.type, placeholderFactory);
@ -403,17 +413,24 @@ export class DashboardPlugin
savedObjects: plugins.savedObjects,
embeddableStart: plugins.embeddable,
});
const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory(
DASHBOARD_CONTAINER_TYPE
)! as DashboardContainerFactory;
return {
getSavedDashboardLoader: () => savedDashboardLoader,
getDashboardContainerByValueRenderer: () => {
const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory(
DASHBOARD_CONTAINER_TYPE
);
if (!dashboardContainerFactory) {
throw new Error(`${DASHBOARD_CONTAINER_TYPE} Embeddable Factory not found`);
}
return createDashboardContainerByValueRenderer({
factory: dashboardContainerFactory as DashboardContainerFactory,
});
},
dashboardUrlGenerator: this.dashboardUrlGenerator,
dashboardFeatureFlagConfig: this.dashboardFeatureFlagConfig!,
DashboardContainerByValueRenderer: createDashboardContainerByValueRenderer({
factory: dashboardContainerFactory,
}),
};
}

View file

@ -0,0 +1,24 @@
/*
* 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 { EmbeddablePersistableStateService } from 'src/plugins/embeddable/common';
import { EmbeddableRegistryDefinition } from '../../../embeddable/server';
import {
createExtract,
createInject,
} from '../../common/embeddable/dashboard_container_persistable_state';
export const dashboardPersistableStateServiceFactory = (
persistableStateService: EmbeddablePersistableStateService
): EmbeddableRegistryDefinition => {
return {
id: 'dashboard',
extract: createExtract(persistableStateService),
inject: createInject(persistableStateService),
};
};

View file

@ -18,24 +18,29 @@ import { createDashboardSavedObjectType } from './saved_objects';
import { capabilitiesProvider } from './capabilities_provider';
import { DashboardPluginSetup, DashboardPluginStart } from './types';
import { EmbeddableSetup } from '../../embeddable/server';
import { EmbeddableSetup, EmbeddableStart } from '../../embeddable/server';
import { UsageCollectionSetup } from '../../usage_collection/server';
import { registerDashboardUsageCollector } from './usage/register_collector';
import { dashboardPersistableStateServiceFactory } from './embeddable/dashboard_container_embeddable_factory';
interface SetupDeps {
embeddable: EmbeddableSetup;
usageCollection: UsageCollectionSetup;
}
interface StartDeps {
embeddable: EmbeddableStart;
}
export class DashboardPlugin
implements Plugin<DashboardPluginSetup, DashboardPluginStart, SetupDeps> {
implements Plugin<DashboardPluginSetup, DashboardPluginStart, SetupDeps, StartDeps> {
private readonly logger: Logger;
constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
}
public setup(core: CoreSetup, plugins: SetupDeps) {
public setup(core: CoreSetup<StartDeps>, plugins: SetupDeps) {
this.logger.debug('dashboard: Setup');
core.savedObjects.registerType(
@ -48,6 +53,15 @@ export class DashboardPlugin
core.capabilities.registerProvider(capabilitiesProvider);
registerDashboardUsageCollector(plugins.usageCollection, plugins.embeddable);
(async () => {
const [, startPlugins] = await core.getStartServices();
plugins.embeddable.registerEmbeddableFactory(
dashboardPersistableStateServiceFactory(startPlugins.embeddable)
);
})();
return {};
}

View file

@ -6,13 +6,39 @@
* Side Public License, v 1.
*/
import { SavedObjectUnsanitizedDoc } from 'kibana/server';
import { SavedObjectReference, SavedObjectUnsanitizedDoc } from 'kibana/server';
import { savedObjectsServiceMock } from '../../../../core/server/mocks';
import { createEmbeddableSetupMock } from '../../../embeddable/server/mocks';
import { createDashboardSavedObjectTypeMigrations } from './dashboard_migrations';
import { DashboardDoc730ToLatest } from '../../common';
import {
createExtract,
createInject,
} from '../../common/embeddable/dashboard_container_persistable_state';
import { EmbeddableStateWithType } from 'src/plugins/embeddable/common';
const embeddableSetupMock = createEmbeddableSetupMock();
const extract = createExtract(embeddableSetupMock);
const inject = createInject(embeddableSetupMock);
const extractImplementation = (state: EmbeddableStateWithType) => {
if (state.type === 'dashboard') {
return extract(state);
}
return { state, references: [] };
};
const injectImplementation = (
state: EmbeddableStateWithType,
references: SavedObjectReference[]
) => {
if (state.type === 'dashboard') {
return inject(state, references);
}
return state;
};
embeddableSetupMock.extract.mockImplementation(extractImplementation);
embeddableSetupMock.inject.mockImplementation(injectImplementation);
const migrations = createDashboardSavedObjectTypeMigrations({
embeddable: embeddableSetupMock,
});
@ -25,10 +51,10 @@ describe('dashboard', () => {
test('skips error on empty object', () => {
expect(migration({} as SavedObjectUnsanitizedDoc, contextMock)).toMatchInlineSnapshot(`
Object {
"references": Array [],
}
`);
Object {
"references": Array [],
}
`);
});
test('skips errors when searchSourceJSON is null', () => {
@ -45,29 +71,29 @@ Object {
};
const migratedDoc = migration(doc, contextMock);
expect(migratedDoc).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": null,
},
"panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
},
"id": "1",
"references": Array [
Object {
"id": "1",
"name": "panel_0",
"type": "visualization",
},
Object {
"id": "2",
"name": "panel_1",
"type": "visualization",
},
],
"type": "dashboard",
}
`);
Object {
"attributes": Object {
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": null,
},
"panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
},
"id": "1",
"references": Array [
Object {
"id": "1",
"name": "panel_0",
"type": "visualization",
},
Object {
"id": "2",
"name": "panel_1",
"type": "visualization",
},
],
"type": "dashboard",
}
`);
});
test('skips errors when searchSourceJSON is undefined', () => {
@ -84,29 +110,29 @@ Object {
};
const migratedDoc = migration(doc, contextMock);
expect(migratedDoc).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": undefined,
},
"panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
},
"id": "1",
"references": Array [
Object {
"id": "1",
"name": "panel_0",
"type": "visualization",
},
Object {
"id": "2",
"name": "panel_1",
"type": "visualization",
},
],
"type": "dashboard",
}
`);
Object {
"attributes": Object {
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": undefined,
},
"panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
},
"id": "1",
"references": Array [
Object {
"id": "1",
"name": "panel_0",
"type": "visualization",
},
Object {
"id": "2",
"name": "panel_1",
"type": "visualization",
},
],
"type": "dashboard",
}
`);
});
test('skips error when searchSourceJSON is not a string', () => {
@ -122,29 +148,29 @@ Object {
},
};
expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": 123,
},
"panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
},
"id": "1",
"references": Array [
Object {
"id": "1",
"name": "panel_0",
"type": "visualization",
},
Object {
"id": "2",
"name": "panel_1",
"type": "visualization",
},
],
"type": "dashboard",
}
`);
Object {
"attributes": Object {
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": 123,
},
"panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
},
"id": "1",
"references": Array [
Object {
"id": "1",
"name": "panel_0",
"type": "visualization",
},
Object {
"id": "2",
"name": "panel_1",
"type": "visualization",
},
],
"type": "dashboard",
}
`);
});
test('skips error when searchSourceJSON is invalid json', () => {
@ -160,29 +186,29 @@ Object {
},
};
expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "{abc123}",
},
"panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
},
"id": "1",
"references": Array [
Object {
"id": "1",
"name": "panel_0",
"type": "visualization",
},
Object {
"id": "2",
"name": "panel_1",
"type": "visualization",
},
],
"type": "dashboard",
}
`);
Object {
"attributes": Object {
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "{abc123}",
},
"panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
},
"id": "1",
"references": Array [
Object {
"id": "1",
"name": "panel_0",
"type": "visualization",
},
Object {
"id": "2",
"name": "panel_1",
"type": "visualization",
},
],
"type": "dashboard",
}
`);
});
test('skips error when "index" and "filter" is missing from searchSourceJSON', () => {
@ -199,29 +225,29 @@ Object {
};
const migratedDoc = migration(doc, contextMock);
expect(migratedDoc).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "{\\"bar\\":true}",
},
"panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
},
"id": "1",
"references": Array [
Object {
"id": "1",
"name": "panel_0",
"type": "visualization",
},
Object {
"id": "2",
"name": "panel_1",
"type": "visualization",
},
],
"type": "dashboard",
}
`);
Object {
"attributes": Object {
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "{\\"bar\\":true}",
},
"panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
},
"id": "1",
"references": Array [
Object {
"id": "1",
"name": "panel_0",
"type": "visualization",
},
Object {
"id": "2",
"name": "panel_1",
"type": "visualization",
},
],
"type": "dashboard",
}
`);
});
test('extracts "index" attribute from doc', () => {
@ -238,34 +264,34 @@ Object {
};
const migratedDoc = migration(doc, contextMock);
expect(migratedDoc).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}",
},
"panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
},
"id": "1",
"references": Array [
Object {
"id": "pattern*",
"name": "kibanaSavedObjectMeta.searchSourceJSON.index",
"type": "index-pattern",
},
Object {
"id": "1",
"name": "panel_0",
"type": "visualization",
},
Object {
"id": "2",
"name": "panel_1",
"type": "visualization",
},
],
"type": "dashboard",
}
`);
Object {
"attributes": Object {
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}",
},
"panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
},
"id": "1",
"references": Array [
Object {
"id": "pattern*",
"name": "kibanaSavedObjectMeta.searchSourceJSON.index",
"type": "index-pattern",
},
Object {
"id": "1",
"name": "panel_0",
"type": "visualization",
},
Object {
"id": "2",
"name": "panel_1",
"type": "visualization",
},
],
"type": "dashboard",
}
`);
});
test('extracts index patterns from filter', () => {
@ -293,34 +319,34 @@ Object {
const migratedDoc = migration(doc, contextMock);
expect(migratedDoc).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}",
},
"panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
},
"id": "1",
"references": Array [
Object {
"id": "my-index",
"name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index",
"type": "index-pattern",
},
Object {
"id": "1",
"name": "panel_0",
"type": "visualization",
},
Object {
"id": "2",
"name": "panel_1",
"type": "visualization",
},
],
"type": "dashboard",
}
`);
Object {
"attributes": Object {
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}",
},
"panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
},
"id": "1",
"references": Array [
Object {
"id": "my-index",
"name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index",
"type": "index-pattern",
},
Object {
"id": "1",
"name": "panel_0",
"type": "visualization",
},
Object {
"id": "2",
"name": "panel_1",
"type": "visualization",
},
],
"type": "dashboard",
}
`);
});
test('skips error when panelsJSON is not a string', () => {
@ -331,14 +357,14 @@ Object {
},
} as SavedObjectUnsanitizedDoc;
expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"panelsJSON": 123,
},
"id": "1",
"references": Array [],
}
`);
Object {
"attributes": Object {
"panelsJSON": 123,
},
"id": "1",
"references": Array [],
}
`);
});
test('skips error when panelsJSON is not valid JSON', () => {
@ -349,14 +375,14 @@ Object {
},
} as SavedObjectUnsanitizedDoc;
expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"panelsJSON": "{123abc}",
},
"id": "1",
"references": Array [],
}
`);
Object {
"attributes": Object {
"panelsJSON": "{123abc}",
},
"id": "1",
"references": Array [],
}
`);
});
test('skips panelsJSON when its not an array', () => {
@ -367,14 +393,14 @@ Object {
},
} as SavedObjectUnsanitizedDoc;
expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"panelsJSON": "{}",
},
"id": "1",
"references": Array [],
}
`);
Object {
"attributes": Object {
"panelsJSON": "{}",
},
"id": "1",
"references": Array [],
}
`);
});
test('skips error when a panel is missing "type" attribute', () => {
@ -385,14 +411,14 @@ Object {
},
} as SavedObjectUnsanitizedDoc;
expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"panelsJSON": "[{\\"id\\":\\"123\\"}]",
},
"id": "1",
"references": Array [],
}
`);
Object {
"attributes": Object {
"panelsJSON": "[{\\"id\\":\\"123\\"}]",
},
"id": "1",
"references": Array [],
}
`);
});
test('skips error when a panel is missing "id" attribute', () => {
@ -403,14 +429,14 @@ Object {
},
} as SavedObjectUnsanitizedDoc;
expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"panelsJSON": "[{\\"type\\":\\"visualization\\"}]",
},
"id": "1",
"references": Array [],
}
`);
Object {
"attributes": Object {
"panelsJSON": "[{\\"type\\":\\"visualization\\"}]",
},
"id": "1",
"references": Array [],
}
`);
});
test('extract panel references from doc', () => {
@ -423,25 +449,25 @@ Object {
} as SavedObjectUnsanitizedDoc;
const migratedDoc = migration(doc, contextMock);
expect(migratedDoc).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
},
"id": "1",
"references": Array [
Object {
"id": "1",
"name": "panel_0",
"type": "visualization",
},
Object {
"id": "2",
"name": "panel_1",
"type": "visualization",
},
],
}
`);
Object {
"attributes": Object {
"panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
},
"id": "1",
"references": Array [
Object {
"id": "1",
"name": "panel_0",
"type": "visualization",
},
Object {
"id": "2",
"name": "panel_1",
"type": "visualization",
},
],
}
`);
});
});
@ -475,19 +501,57 @@ Object {
test('should migrate 7.3.0 doc without embeddable state to extract', () => {
const newDoc = migration(doc, contextMock);
expect(newDoc).toEqual(doc);
expect(newDoc).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"description": "",
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "{\\"query\\":{\\"language\\":\\"kuery\\",\\"query\\":\\"\\"},\\"filter\\":[{\\"query\\":{\\"match_phrase\\":{\\"machine.os.keyword\\":\\"osx\\"}},\\"$state\\":{\\"store\\":\\"appState\\"},\\"meta\\":{\\"type\\":\\"phrase\\",\\"key\\":\\"machine.os.keyword\\",\\"params\\":{\\"query\\":\\"osx\\"},\\"disabled\\":false,\\"negate\\":false,\\"alias\\":null,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}",
},
"optionsJSON": "{\\"useMargins\\":true,\\"hidePanelTitles\\":false}",
"panelsJSON": "[{\\"version\\":\\"7.9.3\\",\\"type\\":\\"visualization\\",\\"gridData\\":{\\"x\\":0,\\"y\\":0,\\"w\\":24,\\"h\\":15,\\"i\\":\\"82fa0882-9f9e-476a-bbb9-03555e5ced91\\"},\\"panelIndex\\":\\"82fa0882-9f9e-476a-bbb9-03555e5ced91\\",\\"embeddableConfig\\":{\\"enhancements\\":{\\"dynamicActions\\":{\\"events\\":[]}}},\\"panelRefName\\":\\"panel_82fa0882-9f9e-476a-bbb9-03555e5ced91\\"}]",
"timeRestore": false,
"title": "Dashboard A",
"version": 1,
},
"id": "376e6260-1f5e-11eb-91aa-7b6d5f8a61d6",
"references": Array [
Object {
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index",
"type": "index-pattern",
},
Object {
"id": "14e2e710-4258-11e8-b3aa-73fdaf54bfc9",
"name": "82fa0882-9f9e-476a-bbb9-03555e5ced91:panel_82fa0882-9f9e-476a-bbb9-03555e5ced91",
"type": "visualization",
},
],
"type": "dashboard",
}
`);
});
test('should migrate 7.3.0 doc and extract embeddable state', () => {
embeddableSetupMock.extract.mockImplementationOnce((state) => ({
state: { ...state, __extracted: true },
references: [{ id: '__new', name: '__newRefName', type: '__newType' }],
}));
embeddableSetupMock.extract.mockImplementation((state) => {
const stateAndReferences = extractImplementation(state);
const { references } = stateAndReferences;
let { state: newState } = stateAndReferences;
if (state.enhancements !== undefined && Object.keys(state.enhancements).length !== 0) {
newState = { ...state, __extracted: true } as any;
references.push({ id: '__new', name: '__newRefName', type: '__newType' });
}
return { state: newState, references };
});
const newDoc = migration(doc, contextMock);
expect(newDoc).not.toEqual(doc);
expect(newDoc.references).toHaveLength(doc.references.length + 1);
expect(JSON.parse(newDoc.attributes.panelsJSON)[0].embeddableConfig.__extracted).toBe(true);
embeddableSetupMock.extract.mockImplementation(extractImplementation);
});
});
});

View file

@ -6,9 +6,9 @@
* Side Public License, v 1.
*/
import { EmbeddableServerPlugin, EmbeddableSetup } from './plugin';
import { EmbeddableServerPlugin, EmbeddableSetup, EmbeddableStart } from './plugin';
export { EmbeddableSetup };
export { EmbeddableSetup, EmbeddableStart };
export { EnhancementRegistryDefinition, EmbeddableRegistryDefinition } from './types';

View file

@ -29,6 +29,11 @@ export interface EmbeddableSetup extends PersistableStateService<EmbeddableState
registerEnhancement: (enhancement: EnhancementRegistryDefinition) => void;
}
// Warning: (ae-missing-release-tag) "EmbeddableStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type EmbeddableStart = PersistableStateService<EmbeddableStateWithType>;
// Warning: (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "EnhancementRegistryDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//

View file

@ -324,7 +324,7 @@ export default function ({ getService }: FtrProviderContext) {
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
name: 'panel_0',
name: '1:panel_1',
type: 'visualization',
},
],
@ -384,7 +384,7 @@ export default function ({ getService }: FtrProviderContext) {
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
name: 'panel_0',
name: '1:panel_1',
type: 'visualization',
},
],
@ -449,7 +449,7 @@ export default function ({ getService }: FtrProviderContext) {
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
name: 'panel_0',
name: '1:panel_1',
type: 'visualization',
},
],