Use saved object references for dashboard drilldowns (#82602) (#83293)

This commit is contained in:
Anton Dosov 2020-11-12 18:05:14 +01:00 committed by GitHub
parent ae3d844872
commit aec05c49a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 1127 additions and 306 deletions

View file

@ -1,11 +0,0 @@
<!-- 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; [EmbeddableSetup](./kibana-plugin-plugins-embeddable-server.embeddablesetup.md) &gt; [getAttributeService](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md)
## EmbeddableSetup.getAttributeService property
<b>Signature:</b>
```typescript
getAttributeService: any;
```

View file

@ -7,14 +7,13 @@
<b>Signature:</b> <b>Signature:</b>
```typescript ```typescript
export interface EmbeddableSetup export interface EmbeddableSetup extends PersistableStateService<EmbeddableStateWithType>
``` ```
## Properties ## Properties
| Property | Type | Description | | Property | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| [getAttributeService](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md) | <code>any</code> | |
| [registerEmbeddableFactory](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerembeddablefactory.md) | <code>(factory: EmbeddableRegistryDefinition) =&gt; void</code> | | | [registerEmbeddableFactory](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerembeddablefactory.md) | <code>(factory: EmbeddableRegistryDefinition) =&gt; void</code> | |
| [registerEnhancement](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerenhancement.md) | <code>(enhancement: EnhancementRegistryDefinition) =&gt; void</code> | | | [registerEnhancement](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerenhancement.md) | <code>(enhancement: EnhancementRegistryDefinition) =&gt; void</code> | |

View file

@ -0,0 +1,87 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
ExtractDeps,
extractPanelsReferences,
InjectDeps,
injectPanelsReferences,
} from './embeddable_references';
import { createEmbeddablePersistableStateServiceMock } from '../../../embeddable/common/mocks';
import { SavedDashboardPanel } from '../types';
import { EmbeddableStateWithType } from '../../../embeddable/common';
const embeddablePersistableStateService = createEmbeddablePersistableStateServiceMock();
const deps: InjectDeps & ExtractDeps = {
embeddablePersistableStateService,
};
test('inject/extract panel references', () => {
embeddablePersistableStateService.extract.mockImplementationOnce((state) => {
const { HARDCODED_ID, ...restOfState } = (state as unknown) as Record<string, unknown>;
return {
state: restOfState as EmbeddableStateWithType,
references: [{ id: HARDCODED_ID as string, name: 'refName', type: 'type' }],
};
});
embeddablePersistableStateService.inject.mockImplementationOnce((state, references) => {
const ref = references.find((r) => r.name === 'refName');
return {
...state,
HARDCODED_ID: ref!.id,
};
});
const savedDashboardPanel: SavedDashboardPanel = {
type: 'search',
embeddableConfig: {
HARDCODED_ID: 'IMPORTANT_HARDCODED_ID',
},
id: 'savedObjectId',
panelIndex: '123',
gridData: {
x: 0,
y: 0,
h: 15,
w: 15,
i: '123',
},
version: '7.0.0',
};
const [{ panel: extractedPanel, references }] = extractPanelsReferences(
[savedDashboardPanel],
deps
);
expect(extractedPanel.embeddableConfig).toEqual({});
expect(references).toMatchInlineSnapshot(`
Array [
Object {
"id": "IMPORTANT_HARDCODED_ID",
"name": "refName",
"type": "type",
},
]
`);
const [injectedPanel] = injectPanelsReferences([extractedPanel], references, deps);
expect(injectedPanel).toEqual(savedDashboardPanel);
});

View file

@ -0,0 +1,82 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { omit } from 'lodash';
import {
convertSavedDashboardPanelToPanelState,
convertPanelStateToSavedDashboardPanel,
} from './embeddable_saved_object_converters';
import { SavedDashboardPanel } from '../types';
import { SavedObjectReference } from '../../../../core/types';
import { EmbeddablePersistableStateService } from '../../../embeddable/common/types';
export interface InjectDeps {
embeddablePersistableStateService: EmbeddablePersistableStateService;
}
export function injectPanelsReferences(
panels: SavedDashboardPanel[],
references: SavedObjectReference[],
deps: InjectDeps
): SavedDashboardPanel[] {
const result: SavedDashboardPanel[] = [];
for (const panel of panels) {
const embeddableState = convertSavedDashboardPanelToPanelState(panel);
embeddableState.explicitInput = omit(
deps.embeddablePersistableStateService.inject(
{ ...embeddableState.explicitInput, type: panel.type },
references
),
'type'
);
result.push(convertPanelStateToSavedDashboardPanel(embeddableState, panel.version));
}
return result;
}
export interface ExtractDeps {
embeddablePersistableStateService: EmbeddablePersistableStateService;
}
export function extractPanelsReferences(
panels: SavedDashboardPanel[],
deps: ExtractDeps
): Array<{ panel: SavedDashboardPanel; references: SavedObjectReference[] }> {
const result: Array<{ panel: SavedDashboardPanel; references: SavedObjectReference[] }> = [];
for (const panel of panels) {
const embeddable = convertSavedDashboardPanelToPanelState(panel);
const {
state: embeddableInputWithExtractedReferences,
references,
} = deps.embeddablePersistableStateService.extract({
...embeddable.explicitInput,
type: embeddable.type,
});
embeddable.explicitInput = omit(embeddableInputWithExtractedReferences, 'type');
const newPanel = convertPanelStateToSavedDashboardPanel(embeddable, panel.version);
result.push({
panel: newPanel,
references,
});
}
return result;
}

View file

@ -21,9 +21,8 @@ import {
convertSavedDashboardPanelToPanelState, convertSavedDashboardPanelToPanelState,
convertPanelStateToSavedDashboardPanel, convertPanelStateToSavedDashboardPanel,
} from './embeddable_saved_object_converters'; } from './embeddable_saved_object_converters';
import { SavedDashboardPanel } from '../../types'; import { SavedDashboardPanel, DashboardPanelState } from '../types';
import { DashboardPanelState } from '../embeddable'; import { EmbeddableInput } from '../../../embeddable/common/types';
import { EmbeddableInput } from '../../../../embeddable/public';
test('convertSavedDashboardPanelToPanelState', () => { test('convertSavedDashboardPanelToPanelState', () => {
const savedDashboardPanel: SavedDashboardPanel = { const savedDashboardPanel: SavedDashboardPanel = {
@ -135,3 +134,24 @@ test('convertPanelStateToSavedDashboardPanel will not add an undefined id when n
const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel, '8.0.0'); const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel, '8.0.0');
expect(converted.hasOwnProperty('id')).toBe(false); expect(converted.hasOwnProperty('id')).toBe(false);
}); });
test('convertPanelStateToSavedDashboardPanel will not leave title as part of embeddable config', () => {
const dashboardPanel: DashboardPanelState = {
gridData: {
x: 0,
y: 0,
h: 15,
w: 15,
i: '123',
},
explicitInput: {
id: '123',
title: 'title',
} as EmbeddableInput,
type: 'search',
};
const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel, '8.0.0');
expect(converted.embeddableConfig.hasOwnProperty('title')).toBe(false);
expect(converted.title).toBe('title');
});

View file

@ -17,9 +17,8 @@
* under the License. * under the License.
*/ */
import { omit } from 'lodash'; import { omit } from 'lodash';
import { SavedDashboardPanel } from '../../types'; import { DashboardPanelState, SavedDashboardPanel } from '../types';
import { DashboardPanelState } from '../embeddable'; import { SavedObjectEmbeddableInput } from '../../../embeddable/common/';
import { SavedObjectEmbeddableInput } from '../../embeddable_plugin';
export function convertSavedDashboardPanelToPanelState( export function convertSavedDashboardPanelToPanelState(
savedDashboardPanel: SavedDashboardPanel savedDashboardPanel: SavedDashboardPanel
@ -49,7 +48,7 @@ export function convertPanelStateToSavedDashboardPanel(
type: panelState.type, type: panelState.type,
gridData: panelState.gridData, gridData: panelState.gridData,
panelIndex: panelState.explicitInput.id, panelIndex: panelState.explicitInput.id,
embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId']), embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId', 'title']),
...(customTitle && { title: customTitle }), ...(customTitle && { title: customTitle }),
...(savedObjectId !== undefined && { id: savedObjectId }), ...(savedObjectId !== undefined && { id: savedObjectId }),
}; };

View file

@ -17,8 +17,18 @@
* under the License. * under the License.
*/ */
import { extractReferences, injectReferences } from './saved_dashboard_references'; import {
import { SavedObjectDashboard } from './saved_dashboard'; extractReferences,
injectReferences,
InjectDeps,
ExtractDeps,
} from './saved_dashboard_references';
import { createEmbeddablePersistableStateServiceMock } from '../../embeddable/common/mocks';
const embeddablePersistableStateServiceMock = createEmbeddablePersistableStateServiceMock();
const deps: InjectDeps & ExtractDeps = {
embeddablePersistableStateService: embeddablePersistableStateServiceMock,
};
describe('extractReferences', () => { describe('extractReferences', () => {
test('extracts references from panelsJSON', () => { test('extracts references from panelsJSON', () => {
@ -41,28 +51,28 @@ describe('extractReferences', () => {
}, },
references: [], references: [],
}; };
const updatedDoc = extractReferences(doc); const updatedDoc = extractReferences(doc, deps);
expect(updatedDoc).toMatchInlineSnapshot(` expect(updatedDoc).toMatchInlineSnapshot(`
Object { Object {
"attributes": Object { "attributes": Object {
"foo": true, "foo": true,
"panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_0\\"},{\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_1\\"}]", "panelsJSON": "[{\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_0\\"},{\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_1\\"}]",
}, },
"references": Array [ "references": Array [
Object { Object {
"id": "1", "id": "1",
"name": "panel_0", "name": "panel_0",
"type": "visualization", "type": "visualization",
}, },
Object { Object {
"id": "2", "id": "2",
"name": "panel_1", "name": "panel_1",
"type": "visualization", "type": "visualization",
}, },
], ],
} }
`); `);
}); });
test('fails when "type" attribute is missing from a panel', () => { test('fails when "type" attribute is missing from a panel', () => {
@ -79,7 +89,7 @@ Object {
}, },
references: [], references: [],
}; };
expect(() => extractReferences(doc)).toThrowErrorMatchingInlineSnapshot( expect(() => extractReferences(doc, deps)).toThrowErrorMatchingInlineSnapshot(
`"\\"type\\" attribute is missing from panel \\"0\\""` `"\\"type\\" attribute is missing from panel \\"0\\""`
); );
}); });
@ -98,21 +108,21 @@ Object {
}, },
references: [], references: [],
}; };
expect(extractReferences(doc)).toMatchInlineSnapshot(` expect(extractReferences(doc, deps)).toMatchInlineSnapshot(`
Object { Object {
"attributes": Object { "attributes": Object {
"foo": true, "foo": true,
"panelsJSON": "[{\\"type\\":\\"visualization\\",\\"title\\":\\"Title 1\\"}]", "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\"}]",
}, },
"references": Array [], "references": Array [],
} }
`); `);
}); });
}); });
describe('injectReferences', () => { describe('injectReferences', () => {
test('injects references into context', () => { test('returns injected attributes', () => {
const context = { const attributes = {
id: '1', id: '1',
title: 'test', title: 'test',
panelsJSON: JSON.stringify([ panelsJSON: JSON.stringify([
@ -125,7 +135,7 @@ describe('injectReferences', () => {
title: 'Title 2', title: 'Title 2',
}, },
]), ]),
} as SavedObjectDashboard; };
const references = [ const references = [
{ {
name: 'panel_0', name: 'panel_0',
@ -138,49 +148,49 @@ describe('injectReferences', () => {
id: '2', id: '2',
}, },
]; ];
injectReferences(context, references); const newAttributes = injectReferences({ attributes, references }, deps);
expect(context).toMatchInlineSnapshot(` expect(newAttributes).toMatchInlineSnapshot(`
Object { Object {
"id": "1", "id": "1",
"panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\",\\"type\\":\\"visualization\\"},{\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\",\\"type\\":\\"visualization\\"}]", "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\"}]",
"title": "test", "title": "test",
} }
`); `);
}); });
test('skips when panelsJSON is missing', () => { test('skips when panelsJSON is missing', () => {
const context = { const attributes = {
id: '1', id: '1',
title: 'test', title: 'test',
} as SavedObjectDashboard; };
injectReferences(context, []); const newAttributes = injectReferences({ attributes, references: [] }, deps);
expect(context).toMatchInlineSnapshot(` expect(newAttributes).toMatchInlineSnapshot(`
Object { Object {
"id": "1", "id": "1",
"title": "test", "title": "test",
} }
`); `);
}); });
test('skips when panelsJSON is not an array', () => { test('skips when panelsJSON is not an array', () => {
const context = { const attributes = {
id: '1', id: '1',
panelsJSON: '{}', panelsJSON: '{}',
title: 'test', title: 'test',
} as SavedObjectDashboard; };
injectReferences(context, []); const newAttributes = injectReferences({ attributes, references: [] }, deps);
expect(context).toMatchInlineSnapshot(` expect(newAttributes).toMatchInlineSnapshot(`
Object { Object {
"id": "1", "id": "1",
"panelsJSON": "{}", "panelsJSON": "{}",
"title": "test", "title": "test",
} }
`); `);
}); });
test('skips a panel when panelRefName is missing', () => { test('skips a panel when panelRefName is missing', () => {
const context = { const attributes = {
id: '1', id: '1',
title: 'test', title: 'test',
panelsJSON: JSON.stringify([ panelsJSON: JSON.stringify([
@ -192,7 +202,7 @@ Object {
title: 'Title 2', title: 'Title 2',
}, },
]), ]),
} as SavedObjectDashboard; };
const references = [ const references = [
{ {
name: 'panel_0', name: 'panel_0',
@ -200,18 +210,18 @@ Object {
id: '1', id: '1',
}, },
]; ];
injectReferences(context, references); const newAttributes = injectReferences({ attributes, references }, deps);
expect(context).toMatchInlineSnapshot(` expect(newAttributes).toMatchInlineSnapshot(`
Object { Object {
"id": "1", "id": "1",
"panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\",\\"type\\":\\"visualization\\"},{\\"title\\":\\"Title 2\\"}]", "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\"}]",
"title": "test", "title": "test",
} }
`); `);
}); });
test(`fails when it can't find the reference in the array`, () => { test(`fails when it can't find the reference in the array`, () => {
const context = { const attributes = {
id: '1', id: '1',
title: 'test', title: 'test',
panelsJSON: JSON.stringify([ panelsJSON: JSON.stringify([
@ -220,9 +230,9 @@ Object {
title: 'Title 1', title: 'Title 1',
}, },
]), ]),
} as SavedObjectDashboard; };
expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot( expect(() =>
`"Could not find reference \\"panel_0\\""` injectReferences({ attributes, references: [] }, deps)
); ).toThrowErrorMatchingInlineSnapshot(`"Could not find reference \\"panel_0\\""`);
}); });
}); });

View file

@ -17,18 +17,47 @@
* under the License. * under the License.
*/ */
import { SavedObjectAttributes, SavedObjectReference } from 'kibana/public'; import { SavedObjectAttributes, SavedObjectReference } from '../../../core/types';
import { SavedObjectDashboard } from './saved_dashboard'; import {
extractPanelsReferences,
injectPanelsReferences,
} from './embeddable/embeddable_references';
import { SavedDashboardPanel730ToLatest } from './types';
import { EmbeddablePersistableStateService } from '../../embeddable/common/types';
export function extractReferences({ export interface ExtractDeps {
attributes, embeddablePersistableStateService: EmbeddablePersistableStateService;
references = [], }
}: {
export interface SavedObjectAttributesAndReferences {
attributes: SavedObjectAttributes; attributes: SavedObjectAttributes;
references: SavedObjectReference[]; references: SavedObjectReference[];
}) { }
export function extractReferences(
{ attributes, references = [] }: SavedObjectAttributesAndReferences,
deps: ExtractDeps
): SavedObjectAttributesAndReferences {
if (typeof attributes.panelsJSON !== 'string') {
return { attributes, references };
}
const panelReferences: SavedObjectReference[] = []; const panelReferences: SavedObjectReference[] = [];
const panels: Array<Record<string, string>> = JSON.parse(String(attributes.panelsJSON)); let panels: Array<Record<string, string>> = JSON.parse(String(attributes.panelsJSON));
const extractedReferencesResult = extractPanelsReferences(
(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);
});
// TODO: This extraction should be done by EmbeddablePersistableStateService
// https://github.com/elastic/kibana/issues/82830
panels.forEach((panel, i) => { panels.forEach((panel, i) => {
if (!panel.type) { if (!panel.type) {
throw new Error(`"type" attribute is missing from panel "${i}"`); throw new Error(`"type" attribute is missing from panel "${i}"`);
@ -46,6 +75,7 @@ export function extractReferences({
delete panel.type; delete panel.type;
delete panel.id; delete panel.id;
}); });
return { return {
references: [...references, ...panelReferences], references: [...references, ...panelReferences],
attributes: { attributes: {
@ -55,21 +85,28 @@ export function extractReferences({
}; };
} }
export interface InjectDeps {
embeddablePersistableStateService: EmbeddablePersistableStateService;
}
export function injectReferences( export function injectReferences(
savedObject: SavedObjectDashboard, { attributes, references = [] }: SavedObjectAttributesAndReferences,
references: SavedObjectReference[] deps: InjectDeps
) { ): SavedObjectAttributes {
// Skip if panelsJSON is missing otherwise this will cause saved object import to fail when // Skip if panelsJSON is missing otherwise this will cause saved object import to fail when
// importing objects without panelsJSON. At development time of this, there is no guarantee each saved // importing objects without panelsJSON. At development time of this, there is no guarantee each saved
// object has panelsJSON in all previous versions of kibana. // object has panelsJSON in all previous versions of kibana.
if (typeof savedObject.panelsJSON !== 'string') { if (typeof attributes.panelsJSON !== 'string') {
return; return attributes;
} }
const panels = JSON.parse(savedObject.panelsJSON); let panels = JSON.parse(attributes.panelsJSON);
// Same here, prevent failing saved object import if ever panels aren't an array. // Same here, prevent failing saved object import if ever panels aren't an array.
if (!Array.isArray(panels)) { if (!Array.isArray(panels)) {
return; return attributes;
} }
// TODO: This injection should be done by EmbeddablePersistableStateService
// https://github.com/elastic/kibana/issues/82830
panels.forEach((panel) => { panels.forEach((panel) => {
if (!panel.panelRefName) { if (!panel.panelRefName) {
return; return;
@ -84,5 +121,11 @@ export function injectReferences(
panel.type = reference.type; panel.type = reference.type;
delete panel.panelRefName; delete panel.panelRefName;
}); });
savedObject.panelsJSON = JSON.stringify(panels);
panels = injectPanelsReferences(panels, references, deps);
return {
...attributes,
panelsJSON: JSON.stringify(panels),
};
} }

View file

@ -17,6 +17,8 @@
* under the License. * under the License.
*/ */
import { EmbeddableInput, PanelState } from '../../../../src/plugins/embeddable/common/types';
import { SavedObjectEmbeddableInput } from '../../../../src/plugins/embeddable/common/lib/saved_object_embeddable';
import { import {
RawSavedDashboardPanelTo60, RawSavedDashboardPanelTo60,
RawSavedDashboardPanel610, RawSavedDashboardPanel610,
@ -26,6 +28,21 @@ import {
RawSavedDashboardPanel730ToLatest, RawSavedDashboardPanel730ToLatest,
} from './bwc/types'; } from './bwc/types';
import { GridData } from './embeddable/types';
export type PanelId = string;
export type SavedObjectId = string;
export interface DashboardPanelState<
TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput
> extends PanelState<TEmbeddableInput> {
readonly gridData: GridData;
}
/**
* This should always represent the latest dashboard panel shape, after all possible migrations.
*/
export type SavedDashboardPanel = SavedDashboardPanel730ToLatest;
export type SavedDashboardPanel640To720 = Pick< export type SavedDashboardPanel640To720 = Pick<
RawSavedDashboardPanel640To720, RawSavedDashboardPanel640To720,
Exclude<keyof RawSavedDashboardPanel640To720, 'name'> Exclude<keyof RawSavedDashboardPanel640To720, 'name'>

View file

@ -81,7 +81,6 @@ import { getTopNavConfig } from './top_nav/get_top_nav_config';
import { TopNavIds } from './top_nav/top_nav_ids'; import { TopNavIds } from './top_nav/top_nav_ids';
import { getDashboardTitle } from './dashboard_strings'; import { getDashboardTitle } from './dashboard_strings';
import { DashboardAppScope } from './dashboard_app'; import { DashboardAppScope } from './dashboard_app';
import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters';
import { RenderDeps } from './application'; import { RenderDeps } from './application';
import { import {
IKbnUrlStateStorage, IKbnUrlStateStorage,
@ -97,6 +96,7 @@ import {
subscribeWithScope, subscribeWithScope,
} from '../../../kibana_legacy/public'; } from '../../../kibana_legacy/public';
import { migrateLegacyQuery } from './lib/migrate_legacy_query'; import { migrateLegacyQuery } from './lib/migrate_legacy_query';
import { convertSavedDashboardPanelToPanelState } from '../../common/embeddable/embeddable_saved_object_converters';
export interface DashboardAppControllerDependencies extends RenderDeps { export interface DashboardAppControllerDependencies extends RenderDeps {
$scope: DashboardAppScope; $scope: DashboardAppScope;

View file

@ -30,7 +30,6 @@ import { migrateLegacyQuery } from './lib/migrate_legacy_query';
import { ViewMode } from '../embeddable_plugin'; import { ViewMode } from '../embeddable_plugin';
import { getAppStateDefaults, migrateAppState, getDashboardIdFromUrl } from './lib'; import { getAppStateDefaults, migrateAppState, getDashboardIdFromUrl } from './lib';
import { convertPanelStateToSavedDashboardPanel } from './lib/embeddable_saved_object_converters';
import { FilterUtils } from './lib/filter_utils'; import { FilterUtils } from './lib/filter_utils';
import { import {
DashboardAppState, DashboardAppState,
@ -48,6 +47,7 @@ import {
} from '../../../kibana_utils/public'; } from '../../../kibana_utils/public';
import { SavedObjectDashboard } from '../saved_dashboards'; import { SavedObjectDashboard } from '../saved_dashboards';
import { DashboardContainer } from './embeddable'; import { DashboardContainer } from './embeddable';
import { convertPanelStateToSavedDashboardPanel } from '../../common/embeddable/embeddable_saved_object_converters';
/** /**
* Dashboard state manager handles connecting angular and redux state between the angular and react portions of the * Dashboard state manager handles connecting angular and redux state between the angular and react portions of the

View file

@ -16,14 +16,4 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { SavedObjectEmbeddableInput } from 'src/plugins/embeddable/public'; export * from '../../../common/types';
import { GridData } from '../../../common';
import { PanelState, EmbeddableInput } from '../../embeddable_plugin';
export type PanelId = string;
export type SavedObjectId = string;
export interface DashboardPanelState<
TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput
> extends PanelState<TEmbeddableInput> {
readonly gridData: GridData;
}

View file

@ -450,6 +450,7 @@ export class DashboardPlugin
const savedDashboardLoader = createSavedDashboardLoader({ const savedDashboardLoader = createSavedDashboardLoader({
savedObjectsClient: core.savedObjects.client, savedObjectsClient: core.savedObjects.client,
savedObjects: plugins.savedObjects, savedObjects: plugins.savedObjects,
embeddableStart: plugins.embeddable,
}); });
const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory( const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory(
DASHBOARD_CONTAINER_TYPE DASHBOARD_CONTAINER_TYPE

View file

@ -16,6 +16,6 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
export * from './saved_dashboard_references'; export * from '../../common/saved_dashboard_references';
export * from './saved_dashboard'; export * from './saved_dashboard';
export * from './saved_dashboards'; export * from './saved_dashboards';

View file

@ -17,10 +17,12 @@
* under the License. * under the License.
*/ */
import { SavedObject, SavedObjectsStart } from '../../../../plugins/saved_objects/public'; import { SavedObject, SavedObjectsStart } from '../../../../plugins/saved_objects/public';
import { extractReferences, injectReferences } from './saved_dashboard_references';
import { Filter, ISearchSource, Query, RefreshInterval } from '../../../../plugins/data/public'; import { Filter, ISearchSource, Query, RefreshInterval } from '../../../../plugins/data/public';
import { createDashboardEditUrl } from '../dashboard_constants'; import { createDashboardEditUrl } from '../dashboard_constants';
import { EmbeddableStart } from '../../../embeddable/public';
import { SavedObjectAttributes, SavedObjectReference } from '../../../../core/types';
import { extractReferences, injectReferences } from '../../common/saved_dashboard_references';
export interface SavedObjectDashboard extends SavedObject { export interface SavedObjectDashboard extends SavedObject {
id?: string; id?: string;
@ -41,7 +43,8 @@ export interface SavedObjectDashboard extends SavedObject {
// Used only by the savedDashboards service, usually no reason to change this // Used only by the savedDashboards service, usually no reason to change this
export function createSavedDashboardClass( export function createSavedDashboardClass(
savedObjectStart: SavedObjectsStart savedObjectStart: SavedObjectsStart,
embeddableStart: EmbeddableStart
): new (id: string) => SavedObjectDashboard { ): new (id: string) => SavedObjectDashboard {
class SavedDashboard extends savedObjectStart.SavedObjectClass { class SavedDashboard extends savedObjectStart.SavedObjectClass {
// save these objects with the 'dashboard' type // save these objects with the 'dashboard' type
@ -77,8 +80,19 @@ export function createSavedDashboardClass(
type: SavedDashboard.type, type: SavedDashboard.type,
mapping: SavedDashboard.mapping, mapping: SavedDashboard.mapping,
searchSource: SavedDashboard.searchSource, searchSource: SavedDashboard.searchSource,
extractReferences, extractReferences: (opts: {
injectReferences, attributes: SavedObjectAttributes;
references: SavedObjectReference[];
}) => extractReferences(opts, { embeddablePersistableStateService: embeddableStart }),
injectReferences: (so: SavedObjectDashboard, references: SavedObjectReference[]) => {
const newAttributes = injectReferences(
{ attributes: so._serialize().attributes, references },
{
embeddablePersistableStateService: embeddableStart,
}
);
Object.assign(so, newAttributes);
},
// if this is null/undefined then the SavedObject will be assigned the defaults // if this is null/undefined then the SavedObject will be assigned the defaults
id, id,

View file

@ -20,16 +20,22 @@
import { SavedObjectsClientContract } from 'kibana/public'; import { SavedObjectsClientContract } from 'kibana/public';
import { SavedObjectLoader, SavedObjectsStart } from '../../../../plugins/saved_objects/public'; import { SavedObjectLoader, SavedObjectsStart } from '../../../../plugins/saved_objects/public';
import { createSavedDashboardClass } from './saved_dashboard'; import { createSavedDashboardClass } from './saved_dashboard';
import { EmbeddableStart } from '../../../embeddable/public';
interface Services { interface Services {
savedObjectsClient: SavedObjectsClientContract; savedObjectsClient: SavedObjectsClientContract;
savedObjects: SavedObjectsStart; savedObjects: SavedObjectsStart;
embeddableStart: EmbeddableStart;
} }
/** /**
* @param services * @param services
*/ */
export function createSavedDashboardLoader({ savedObjects, savedObjectsClient }: Services) { export function createSavedDashboardLoader({
const SavedDashboard = createSavedDashboardClass(savedObjects); savedObjects,
savedObjectsClient,
embeddableStart,
}: Services) {
const SavedDashboard = createSavedDashboardClass(savedObjects, embeddableStart);
return new SavedObjectLoader(SavedDashboard, savedObjectsClient); return new SavedObjectLoader(SavedDashboard, savedObjectsClient);
} }

View file

@ -19,9 +19,12 @@
import { Query, Filter } from 'src/plugins/data/public'; import { Query, Filter } from 'src/plugins/data/public';
import { SavedObject as SavedObjectType, SavedObjectAttributes } from 'src/core/public'; import { SavedObject as SavedObjectType, SavedObjectAttributes } from 'src/core/public';
import { SavedDashboardPanel730ToLatest } from '../common';
import { ViewMode } from './embeddable_plugin'; import { ViewMode } from './embeddable_plugin';
import { SavedDashboardPanel } from '../common/types';
export { SavedDashboardPanel };
export interface DashboardCapabilities { export interface DashboardCapabilities {
showWriteControls: boolean; showWriteControls: boolean;
createNew: boolean; createNew: boolean;
@ -71,11 +74,6 @@ export interface Field {
export type NavAction = (anchorElement?: any) => void; export type NavAction = (anchorElement?: any) => void;
/**
* This should always represent the latest dashboard panel shape, after all possible migrations.
*/
export type SavedDashboardPanel = SavedDashboardPanel730ToLatest;
export interface DashboardAppState { export interface DashboardAppState {
panels: SavedDashboardPanel[]; panels: SavedDashboardPanel[];
fullScreenMode: boolean; fullScreenMode: boolean;

View file

@ -25,22 +25,34 @@ import {
Logger, Logger,
} from '../../../core/server'; } from '../../../core/server';
import { dashboardSavedObjectType } from './saved_objects'; import { createDashboardSavedObjectType } from './saved_objects';
import { capabilitiesProvider } from './capabilities_provider'; import { capabilitiesProvider } from './capabilities_provider';
import { DashboardPluginSetup, DashboardPluginStart } from './types'; import { DashboardPluginSetup, DashboardPluginStart } from './types';
import { EmbeddableSetup } from '../../embeddable/server';
export class DashboardPlugin implements Plugin<DashboardPluginSetup, DashboardPluginStart> { interface SetupDeps {
embeddable: EmbeddableSetup;
}
export class DashboardPlugin
implements Plugin<DashboardPluginSetup, DashboardPluginStart, SetupDeps> {
private readonly logger: Logger; private readonly logger: Logger;
constructor(initializerContext: PluginInitializerContext) { constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get(); this.logger = initializerContext.logger.get();
} }
public setup(core: CoreSetup) { public setup(core: CoreSetup, plugins: SetupDeps) {
this.logger.debug('dashboard: Setup'); this.logger.debug('dashboard: Setup');
core.savedObjects.registerType(dashboardSavedObjectType); core.savedObjects.registerType(
createDashboardSavedObjectType({
migrationDeps: {
embeddable: plugins.embeddable,
},
})
);
core.capabilities.registerProvider(capabilitiesProvider); core.capabilities.registerProvider(capabilitiesProvider);
return {}; return {};

View file

@ -18,9 +18,16 @@
*/ */
import { SavedObjectsType } from 'kibana/server'; import { SavedObjectsType } from 'kibana/server';
import { dashboardSavedObjectTypeMigrations } from './dashboard_migrations'; import {
createDashboardSavedObjectTypeMigrations,
DashboardSavedObjectTypeMigrationsDeps,
} from './dashboard_migrations';
export const dashboardSavedObjectType: SavedObjectsType = { export const createDashboardSavedObjectType = ({
migrationDeps,
}: {
migrationDeps: DashboardSavedObjectTypeMigrationsDeps;
}): SavedObjectsType => ({
name: 'dashboard', name: 'dashboard',
hidden: false, hidden: false,
namespaceType: 'single', namespaceType: 'single',
@ -65,5 +72,5 @@ export const dashboardSavedObjectType: SavedObjectsType = {
version: { type: 'integer' }, version: { type: 'integer' },
}, },
}, },
migrations: dashboardSavedObjectTypeMigrations, migrations: createDashboardSavedObjectTypeMigrations(migrationDeps),
}; });

View file

@ -19,7 +19,14 @@
import { SavedObjectUnsanitizedDoc } from 'kibana/server'; import { SavedObjectUnsanitizedDoc } from 'kibana/server';
import { savedObjectsServiceMock } from '../../../../core/server/mocks'; import { savedObjectsServiceMock } from '../../../../core/server/mocks';
import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; import { createEmbeddableSetupMock } from '../../../embeddable/server/mocks';
import { createDashboardSavedObjectTypeMigrations } from './dashboard_migrations';
import { DashboardDoc730ToLatest } from '../../common';
const embeddableSetupMock = createEmbeddableSetupMock();
const migrations = createDashboardSavedObjectTypeMigrations({
embeddable: embeddableSetupMock,
});
const contextMock = savedObjectsServiceMock.createMigrationContext(); const contextMock = savedObjectsServiceMock.createMigrationContext();
@ -448,4 +455,50 @@ Object {
`); `);
}); });
}); });
describe('7.11.0 - embeddable persistable state extraction', () => {
const migration = migrations['7.11.0'];
const doc: DashboardDoc730ToLatest = {
attributes: {
description: '',
kibanaSavedObjectMeta: {
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","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_0"}]',
timeRestore: false,
title: 'Dashboard A',
version: 1,
},
id: '376e6260-1f5e-11eb-91aa-7b6d5f8a61d6',
references: [
{
id: '90943e30-9a47-11e8-b64d-95841ca0b247',
name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index',
type: 'index-pattern',
},
{ id: '14e2e710-4258-11e8-b3aa-73fdaf54bfc9', name: 'panel_0', type: 'visualization' },
],
type: 'dashboard',
};
test('should migrate 7.3.0 doc without embeddable state to extract', () => {
const newDoc = migration(doc, contextMock);
expect(newDoc).toEqual(doc);
});
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' }],
}));
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);
});
});
}); });

View file

@ -18,11 +18,12 @@
*/ */
import { get, flow } from 'lodash'; import { get, flow } from 'lodash';
import { SavedObjectAttributes, SavedObjectMigrationFn } from 'kibana/server';
import { SavedObjectMigrationFn } from 'kibana/server';
import { migrations730 } from './migrations_730'; import { migrations730 } from './migrations_730';
import { migrateMatchAllQuery } from './migrate_match_all_query'; import { migrateMatchAllQuery } from './migrate_match_all_query';
import { DashboardDoc700To720 } from '../../common'; import { DashboardDoc700To720, DashboardDoc730ToLatest } from '../../common';
import { EmbeddableSetup } from '../../../embeddable/server';
import { injectReferences, extractReferences } from '../../common/saved_dashboard_references';
function migrateIndexPattern(doc: DashboardDoc700To720) { function migrateIndexPattern(doc: DashboardDoc700To720) {
const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON');
@ -100,7 +101,57 @@ const migrations700: SavedObjectMigrationFn<any, any> = (doc): DashboardDoc700To
return doc as DashboardDoc700To720; return doc as DashboardDoc700To720;
}; };
export const dashboardSavedObjectTypeMigrations = { /**
* In 7.8.0 we introduced dashboard drilldowns which are stored inside dashboard saved object as part of embeddable state
* In 7.11.0 we created an embeddable references/migrations system that allows to properly extract embeddable persistable state
* https://github.com/elastic/kibana/issues/71409
* The idea of this migration is to inject all the embeddable panel references and then run the extraction again.
* As the result of the extraction:
* 1. In addition to regular `panel_` we will get new references which are extracted by `embeddablePersistableStateService` (dashboard drilldown references)
* 2. `panel_` references will be regenerated
* All other references like index-patterns are forwarded non touched
* @param deps
*/
function createExtractPanelReferencesMigration(
deps: DashboardSavedObjectTypeMigrationsDeps
): SavedObjectMigrationFn<DashboardDoc730ToLatest['attributes']> {
return (doc) => {
const references = doc.references ?? [];
/**
* Remembering this because dashboard's extractReferences won't return those
* All other references like `panel_` will be overwritten
*/
const oldNonPanelReferences = references.filter((ref) => !ref.name.startsWith('panel_'));
const injectedAttributes = injectReferences(
{
attributes: (doc.attributes as unknown) as SavedObjectAttributes,
references,
},
{ embeddablePersistableStateService: deps.embeddable }
);
const { attributes, references: newPanelReferences } = extractReferences(
{ attributes: injectedAttributes, references: [] },
{ embeddablePersistableStateService: deps.embeddable }
);
return {
...doc,
references: [...oldNonPanelReferences, ...newPanelReferences],
attributes,
};
};
}
export interface DashboardSavedObjectTypeMigrationsDeps {
embeddable: EmbeddableSetup;
}
export const createDashboardSavedObjectTypeMigrations = (
deps: DashboardSavedObjectTypeMigrationsDeps
) => ({
/** /**
* We need to have this migration twice, once with a version prior to 7.0.0 once with a version * We need to have this migration twice, once with a version prior to 7.0.0 once with a version
* after it. The reason for that is, that this migration has been introduced once 7.0.0 was already * after it. The reason for that is, that this migration has been introduced once 7.0.0 was already
@ -115,4 +166,5 @@ export const dashboardSavedObjectTypeMigrations = {
'7.0.0': flow(migrations700), '7.0.0': flow(migrations700),
'7.3.0': flow(migrations730), '7.3.0': flow(migrations730),
'7.9.3': flow(migrateMatchAllQuery), '7.9.3': flow(migrateMatchAllQuery),
}; '7.11.0': flow(createExtractPanelReferencesMigration(deps)),
});

View file

@ -17,4 +17,4 @@
* under the License. * under the License.
*/ */
export { dashboardSavedObjectType } from './dashboard'; export { createDashboardSavedObjectType } from './dashboard';

View file

@ -18,12 +18,16 @@
*/ */
import { savedObjectsServiceMock } from '../../../../core/server/mocks'; import { savedObjectsServiceMock } from '../../../../core/server/mocks';
import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; import { createDashboardSavedObjectTypeMigrations } from './dashboard_migrations';
import { migrations730 } from './migrations_730'; import { migrations730 } from './migrations_730';
import { DashboardDoc700To720, DashboardDoc730ToLatest, DashboardDocPre700 } from '../../common'; import { DashboardDoc700To720, DashboardDoc730ToLatest, DashboardDocPre700 } from '../../common';
import { RawSavedDashboardPanel730ToLatest } from '../../common'; import { RawSavedDashboardPanel730ToLatest } from '../../common';
import { createEmbeddableSetupMock } from '../../../embeddable/server/mocks';
const mockContext = savedObjectsServiceMock.createMigrationContext(); const mockContext = savedObjectsServiceMock.createMigrationContext();
const migrations = createDashboardSavedObjectTypeMigrations({
embeddable: createEmbeddableSetupMock(),
});
test('dashboard migration 7.3.0 migrates filters to query on search source', () => { test('dashboard migration 7.3.0 migrates filters to query on search source', () => {
const doc: DashboardDoc700To720 = { const doc: DashboardDoc700To720 = {

View file

@ -0,0 +1,21 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export * from './types';
export * from './lib';

View file

@ -22,3 +22,4 @@ export * from './inject';
export * from './migrate'; export * from './migrate';
export * from './migrate_base_input'; export * from './migrate_base_input';
export * from './telemetry'; export * from './telemetry';
export * from './saved_object_embeddable';

View file

@ -17,7 +17,7 @@
* under the License. * under the License.
*/ */
import { EmbeddableInput } from '..'; import { EmbeddableInput } from '../types';
export interface SavedObjectEmbeddableInput extends EmbeddableInput { export interface SavedObjectEmbeddableInput extends EmbeddableInput {
savedObjectId: string; savedObjectId: string;

View file

@ -0,0 +1,31 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { EmbeddablePersistableStateService } from './types';
export const createEmbeddablePersistableStateServiceMock = (): jest.Mocked<
EmbeddablePersistableStateService
> => {
return {
inject: jest.fn((state, references) => state),
extract: jest.fn((state) => ({ state, references: [] })),
migrate: jest.fn((state, version) => state),
telemetry: jest.fn((state, collector) => ({})),
};
};

View file

@ -17,7 +17,7 @@
* under the License. * under the License.
*/ */
import { SerializableState } from '../../kibana_utils/common'; import { PersistableStateService, SerializableState } from '../../kibana_utils/common';
import { Query, TimeRange } from '../../data/common/query'; import { Query, TimeRange } from '../../data/common/query';
import { Filter } from '../../data/common/es_query/filters'; import { Filter } from '../../data/common/es_query/filters';
@ -74,8 +74,21 @@ export type EmbeddableInput = {
searchSessionId?: string; searchSessionId?: string;
}; };
export interface PanelState<E extends EmbeddableInput & { id: string } = { id: string }> {
// The type of embeddable in this panel. Will be used to find the factory in which to
// load the embeddable.
type: string;
// Stores input for this embeddable that is specific to this embeddable. Other parts of embeddable input
// will be derived from the container's input. **Any state in here will override any state derived from
// the container.**
explicitInput: Partial<E> & { id: string };
}
export type EmbeddableStateWithType = EmbeddableInput & { type: string }; export type EmbeddableStateWithType = EmbeddableInput & { type: string };
export type EmbeddablePersistableStateService = PersistableStateService<EmbeddableStateWithType>;
export interface CommonEmbeddableStartContract { export interface CommonEmbeddableStartContract {
getEmbeddableFactory: (embeddableFactoryId: string) => any; getEmbeddableFactory: (embeddableFactoryId: string) => any;
getEnhancement: (enhancementId: string) => any; getEnhancement: (enhancementId: string) => any;

View file

@ -31,7 +31,7 @@ import {
import { IContainer, ContainerInput, ContainerOutput, PanelState } from './i_container'; import { IContainer, ContainerInput, ContainerOutput, PanelState } from './i_container';
import { PanelNotFoundError, EmbeddableFactoryNotFoundError } from '../errors'; import { PanelNotFoundError, EmbeddableFactoryNotFoundError } from '../errors';
import { EmbeddableStart } from '../../plugin'; import { EmbeddableStart } from '../../plugin';
import { isSavedObjectEmbeddableInput } from '../embeddables/saved_object_embeddable'; import { isSavedObjectEmbeddableInput } from '../../../common/lib/saved_object_embeddable';
const getKeys = <T extends {}>(o: T): Array<keyof T> => Object.keys(o) as Array<keyof T>; const getKeys = <T extends {}>(o: T): Array<keyof T> => Object.keys(o) as Array<keyof T>;

View file

@ -24,17 +24,9 @@ import {
ErrorEmbeddable, ErrorEmbeddable,
IEmbeddable, IEmbeddable,
} from '../embeddables'; } from '../embeddables';
import { PanelState } from '../../../common/types';
export interface PanelState<E extends EmbeddableInput & { id: string } = { id: string }> { export { PanelState };
// The type of embeddable in this panel. Will be used to find the factory in which to
// load the embeddable.
type: string;
// Stores input for this embeddable that is specific to this embeddable. Other parts of embeddable input
// will be derived from the container's input. **Any state in here will override any state derived from
// the container.**
explicitInput: Partial<E> & { id: string };
}
export interface ContainerOutput extends EmbeddableOutput { export interface ContainerOutput extends EmbeddableOutput {
embeddableLoaded: { [key: string]: boolean }; embeddableLoaded: { [key: string]: boolean };

View file

@ -24,5 +24,5 @@ export * from './default_embeddable_factory_provider';
export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable'; export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable';
export { withEmbeddableSubscription } from './with_subscription'; export { withEmbeddableSubscription } from './with_subscription';
export { EmbeddableRoot } from './embeddable_root'; export { EmbeddableRoot } from './embeddable_root';
export * from './saved_object_embeddable'; export * from '../../../common/lib/saved_object_embeddable';
export { EmbeddableRenderer, EmbeddableRendererProps } from './embeddable_renderer'; export { EmbeddableRenderer, EmbeddableRendererProps } from './embeddable_renderer';

View file

@ -0,0 +1,30 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { createEmbeddablePersistableStateServiceMock } from '../common/mocks';
import { EmbeddableSetup, EmbeddableStart } from './plugin';
export const createEmbeddableSetupMock = (): jest.Mocked<EmbeddableSetup> => ({
...createEmbeddablePersistableStateServiceMock(),
registerEmbeddableFactory: jest.fn(),
registerEnhancement: jest.fn(),
});
export const createEmbeddableStartMock = (): jest.Mocked<EmbeddableStart> =>
createEmbeddablePersistableStateServiceMock();

View file

@ -32,23 +32,32 @@ import {
getMigrateFunction, getMigrateFunction,
getTelemetryFunction, getTelemetryFunction,
} from '../common/lib'; } from '../common/lib';
import { SerializableState } from '../../kibana_utils/common'; import { PersistableStateService, SerializableState } from '../../kibana_utils/common';
import { EmbeddableStateWithType } from '../common/types'; import { EmbeddableStateWithType } from '../common/types';
export interface EmbeddableSetup { export interface EmbeddableSetup extends PersistableStateService<EmbeddableStateWithType> {
getAttributeService: any;
registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void; registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void;
registerEnhancement: (enhancement: EnhancementRegistryDefinition) => void; registerEnhancement: (enhancement: EnhancementRegistryDefinition) => void;
} }
export class EmbeddableServerPlugin implements Plugin<object, object> { export type EmbeddableStart = PersistableStateService<EmbeddableStateWithType>;
export class EmbeddableServerPlugin implements Plugin<EmbeddableSetup, EmbeddableStart> {
private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map(); private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map();
private readonly enhancements: EnhancementsRegistry = new Map(); private readonly enhancements: EnhancementsRegistry = new Map();
public setup(core: CoreSetup) { public setup(core: CoreSetup) {
const commonContract = {
getEmbeddableFactory: this.getEmbeddableFactory,
getEnhancement: this.getEnhancement,
};
return { return {
registerEmbeddableFactory: this.registerEmbeddableFactory, registerEmbeddableFactory: this.registerEmbeddableFactory,
registerEnhancement: this.registerEnhancement, registerEnhancement: this.registerEnhancement,
telemetry: getTelemetryFunction(commonContract),
extract: getExtractFunction(commonContract),
inject: getInjectFunction(commonContract),
migrate: getMigrateFunction(commonContract),
}; };
} }

View file

@ -18,12 +18,11 @@ export interface EmbeddableRegistryDefinition<P extends EmbeddableStateWithType
id: string; id: string;
} }
// Warning: (ae-forgotten-export) The symbol "PersistableStateService" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "EmbeddableSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // Warning: (ae-missing-release-tag) "EmbeddableSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
// //
// @public (undocumented) // @public (undocumented)
export interface EmbeddableSetup { export interface EmbeddableSetup extends PersistableStateService<EmbeddableStateWithType> {
// (undocumented)
getAttributeService: any;
// (undocumented) // (undocumented)
registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void; registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void;
// (undocumented) // (undocumented)

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/**
* NOTE: DO NOT CHANGE THIS STRING WITHOUT CAREFUL CONSIDERATOIN, BECAUSE IT IS
* STORED IN SAVED OBJECTS.
*
* Also temporary dashboard drilldown migration code inside embeddable plugin relies on it
* x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts
*/
export const EMBEDDABLE_TO_DASHBOARD_DRILLDOWN = 'DASHBOARD_TO_DASHBOARD_DRILLDOWN';

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { createExtract, createInject } from './dashboard_drilldown_persistable_state';
import { SerializedEvent } from '../../../../ui_actions_enhanced/common';
const drilldownId = 'test_id';
const extract = createExtract({ drilldownId });
const inject = createInject({ drilldownId });
const state: SerializedEvent = {
eventId: 'event_id',
triggers: [],
action: {
factoryId: drilldownId,
name: 'name',
config: {
dashboardId: 'dashboardId_1',
},
},
};
test('should extract and injected dashboard reference', () => {
const { state: extractedState, references } = extract(state);
expect(extractedState).not.toEqual(state);
expect(extractedState.action.config.dashboardId).toBeUndefined();
expect(references).toMatchInlineSnapshot(`
Array [
Object {
"id": "dashboardId_1",
"name": "drilldown:test_id:event_id:dashboardId",
"type": "dashboard",
},
]
`);
let injectedState = inject(extractedState, references);
expect(injectedState).toEqual(state);
references[0].id = 'dashboardId_2';
injectedState = inject(extractedState, references);
expect(injectedState).not.toEqual(extractedState);
expect(injectedState.action.config.dashboardId).toBe('dashboardId_2');
});

View file

@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObjectReference } from '../../../../../../src/core/types';
import { PersistableStateService } from '../../../../../../src/plugins/kibana_utils/common';
import { SerializedAction, SerializedEvent } from '../../../../ui_actions_enhanced/common';
import { DrilldownConfig } from './types';
type DashboardDrilldownPersistableState = PersistableStateService<SerializedEvent>;
const generateRefName = (state: SerializedEvent, id: string) =>
`drilldown:${id}:${state.eventId}:dashboardId`;
const injectDashboardId = (state: SerializedEvent, dashboardId: string): SerializedEvent => {
return {
...state,
action: {
...state.action,
config: {
...state.action.config,
dashboardId,
},
},
};
};
export const createInject = ({
drilldownId,
}: {
drilldownId: string;
}): DashboardDrilldownPersistableState['inject'] => {
return (state: SerializedEvent, references: SavedObjectReference[]) => {
const action = state.action as SerializedAction<DrilldownConfig>;
const refName = generateRefName(state, drilldownId);
const ref = references.find((r) => r.name === refName);
if (!ref) return state;
if (ref.id && ref.id === action.config.dashboardId) return state;
return injectDashboardId(state, ref.id);
};
};
export const createExtract = ({
drilldownId,
}: {
drilldownId: string;
}): DashboardDrilldownPersistableState['extract'] => {
return (state: SerializedEvent) => {
const action = state.action as SerializedAction<DrilldownConfig>;
const references: SavedObjectReference[] = action.config.dashboardId
? [
{
name: generateRefName(state, drilldownId),
type: 'dashboard',
id: action.config.dashboardId,
},
]
: [];
const { dashboardId, ...restOfConfig } = action.config;
return {
state: {
...state,
action: ({
...state.action,
config: restOfConfig,
} as unknown) as SerializedAction,
},
references,
};
};
};

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { createExtract, createInject } from './dashboard_drilldown_persistable_state';
export { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants';
export { DrilldownConfig } from './types';

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type DrilldownConfig = {
dashboardId?: string;
useCurrentFilters: boolean;
useCurrentDateRange: boolean;
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './dashboard_drilldown';

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './drilldowns';

View file

@ -1,7 +1,7 @@
{ {
"id": "dashboardEnhanced", "id": "dashboardEnhanced",
"version": "kibana", "version": "kibana",
"server": false, "server": true,
"ui": true, "ui": true,
"requiredPlugins": ["data", "uiActionsEnhanced", "embeddable", "dashboard", "share"], "requiredPlugins": ["data", "uiActionsEnhanced", "embeddable", "dashboard", "share"],
"configPath": ["xpack", "dashboardEnhanced"], "configPath": ["xpack", "dashboardEnhanced"],

View file

@ -9,19 +9,19 @@ import { DataPublicPluginStart } from 'src/plugins/data/public';
import { DashboardStart } from 'src/plugins/dashboard/public'; import { DashboardStart } from 'src/plugins/dashboard/public';
import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public'; import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public';
import { import {
TriggerId,
TriggerContextMapping, TriggerContextMapping,
TriggerId,
} from '../../../../../../../src/plugins/ui_actions/public'; } from '../../../../../../../src/plugins/ui_actions/public';
import { CollectConfigContainer } from './components'; import { CollectConfigContainer } from './components';
import { import {
UiActionsEnhancedDrilldownDefinition as Drilldown,
UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext,
AdvancedUiActionsStart, AdvancedUiActionsStart,
UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext,
UiActionsEnhancedDrilldownDefinition as Drilldown,
} from '../../../../../ui_actions_enhanced/public'; } from '../../../../../ui_actions_enhanced/public';
import { txtGoToDashboard } from './i18n'; import { txtGoToDashboard } from './i18n';
import { import {
StartServicesGetter,
CollectConfigProps, CollectConfigProps,
StartServicesGetter,
} from '../../../../../../../src/plugins/kibana_utils/public'; } from '../../../../../../../src/plugins/kibana_utils/public';
import { KibanaURL } from '../../../../../../../src/plugins/share/public'; import { KibanaURL } from '../../../../../../../src/plugins/share/public';
import { Config } from './types'; import { Config } from './types';

View file

@ -6,12 +6,8 @@
import { UiActionsEnhancedBaseActionFactoryContext } from '../../../../../ui_actions_enhanced/public'; import { UiActionsEnhancedBaseActionFactoryContext } from '../../../../../ui_actions_enhanced/public';
import { APPLY_FILTER_TRIGGER } from '../../../../../../../src/plugins/ui_actions/public'; import { APPLY_FILTER_TRIGGER } from '../../../../../../../src/plugins/ui_actions/public';
import { DrilldownConfig } from '../../../../common';
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type Config = DrilldownConfig;
export type Config = {
dashboardId?: string;
useCurrentFilters: boolean;
useCurrentDateRange: boolean;
};
export type FactoryContext = UiActionsEnhancedBaseActionFactoryContext<typeof APPLY_FILTER_TRIGGER>; export type FactoryContext = UiActionsEnhancedBaseActionFactoryContext<typeof APPLY_FILTER_TRIGGER>;

View file

@ -65,6 +65,12 @@ test('getHref is defined', () => {
expect(drilldown.getHref).toBeDefined(); expect(drilldown.getHref).toBeDefined();
}); });
test('inject/extract are defined', () => {
const drilldown = new EmbeddableToDashboardDrilldown({} as any);
expect(drilldown.extract).toBeDefined();
expect(drilldown.inject).toBeDefined();
});
describe('.execute() & getHref', () => { describe('.execute() & getHref', () => {
/** /**
* A convenience test setup helper * A convenience test setup helper

View file

@ -22,6 +22,7 @@ import {
} from '../abstract_dashboard_drilldown'; } from '../abstract_dashboard_drilldown';
import { KibanaURL } from '../../../../../../../src/plugins/share/public'; import { KibanaURL } from '../../../../../../../src/plugins/share/public';
import { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants'; import { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants';
import { createExtract, createInject } from '../../../../common';
type Trigger = typeof APPLY_FILTER_TRIGGER; type Trigger = typeof APPLY_FILTER_TRIGGER;
type Context = TriggerContextMapping[Trigger]; type Context = TriggerContextMapping[Trigger];
@ -80,4 +81,8 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown<T
return url; return url;
} }
public readonly inject = createInject({ drilldownId: this.id });
public readonly extract = createExtract({ drilldownId: this.id });
} }

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { PluginInitializerContext } from 'src/core/server';
import { DashboardEnhancedPlugin } from './plugin';
export {
SetupContract as DashboardEnhancedSetupContract,
SetupDependencies as DashboardEnhancedSetupDependencies,
StartContract as DashboardEnhancedStartContract,
StartDependencies as DashboardEnhancedStartDependencies,
} from './plugin';
export function plugin(context: PluginInitializerContext) {
return new DashboardEnhancedPlugin(context);
}

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server';
import { AdvancedUiActionsSetup, AdvancedUiActionsStart } from '../../ui_actions_enhanced/server';
import { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN, createExtract, createInject } from '../common';
export interface SetupDependencies {
uiActionsEnhanced: AdvancedUiActionsSetup;
}
export interface StartDependencies {
uiActionsEnhanced: AdvancedUiActionsStart;
}
// eslint-disable-next-line
export interface SetupContract {}
// eslint-disable-next-line
export interface StartContract {}
export class DashboardEnhancedPlugin
implements Plugin<SetupContract, StartContract, SetupDependencies, StartDependencies> {
constructor(protected readonly context: PluginInitializerContext) {}
public setup(core: CoreSetup<StartDependencies>, plugins: SetupDependencies): SetupContract {
plugins.uiActionsEnhanced.registerActionFactory({
id: EMBEDDABLE_TO_DASHBOARD_DRILLDOWN,
inject: createInject({ drilldownId: EMBEDDABLE_TO_DASHBOARD_DRILLDOWN }),
extract: createExtract({ drilldownId: EMBEDDABLE_TO_DASHBOARD_DRILLDOWN }),
});
return {};
}
public start(core: CoreStart, plugins: StartDependencies): StartContract {
return {};
}
public stop() {}
}

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './types';

View file

@ -7,11 +7,11 @@
import { EnhancementRegistryDefinition } from '../../../../src/plugins/embeddable/server'; import { EnhancementRegistryDefinition } from '../../../../src/plugins/embeddable/server';
import { SavedObjectReference } from '../../../../src/core/types'; import { SavedObjectReference } from '../../../../src/core/types';
import { DynamicActionsState, SerializedEvent } from './types'; import { DynamicActionsState, SerializedEvent } from './types';
import { AdvancedUiActionsPublicPlugin } from './plugin'; import { AdvancedUiActionsServerPlugin } from './plugin';
import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; import { SerializableState } from '../../../../src/plugins/kibana_utils/common';
export const dynamicActionEnhancement = ( export const dynamicActionEnhancement = (
uiActionsEnhanced: AdvancedUiActionsPublicPlugin uiActionsEnhanced: AdvancedUiActionsServerPlugin
): EnhancementRegistryDefinition => { ): EnhancementRegistryDefinition => {
return { return {
id: 'dynamicActions', id: 'dynamicActions',

View file

@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { AdvancedUiActionsPublicPlugin } from './plugin'; import { AdvancedUiActionsServerPlugin } from './plugin';
export function plugin() { export function plugin() {
return new AdvancedUiActionsPublicPlugin(); return new AdvancedUiActionsServerPlugin();
} }
export { AdvancedUiActionsPublicPlugin as Plugin }; export { AdvancedUiActionsServerPlugin as Plugin };
export { export {
SetupContract as AdvancedUiActionsSetup, SetupContract as AdvancedUiActionsSetup,
StartContract as AdvancedUiActionsStart, StartContract as AdvancedUiActionsStart,

View file

@ -16,7 +16,7 @@ import {
} from './types'; } from './types';
export interface SetupContract { export interface SetupContract {
registerActionFactory: any; registerActionFactory: (definition: ActionFactoryDefinition) => void;
} }
export type StartContract = void; export type StartContract = void;
@ -25,7 +25,7 @@ interface SetupDependencies {
embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions.
} }
export class AdvancedUiActionsPublicPlugin export class AdvancedUiActionsServerPlugin
implements Plugin<SetupContract, StartContract, SetupDependencies> { implements Plugin<SetupContract, StartContract, SetupDependencies> {
protected readonly actionFactories: ActionFactoryRegistry = new Map(); protected readonly actionFactories: ActionFactoryRegistry = new Map();

View file

@ -14,7 +14,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardPanelActions = getService('dashboardPanelActions');
const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions'); const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions');
const dashboardDrilldownsManage = getService('dashboardDrilldownsManage'); const dashboardDrilldownsManage = getService('dashboardDrilldownsManage');
const PageObjects = getPageObjects(['dashboard', 'common', 'header', 'timePicker']); const PageObjects = getPageObjects([
'dashboard',
'common',
'header',
'timePicker',
'settings',
'copySavedObjectsToSpace',
]);
const pieChart = getService('pieChart'); const pieChart = getService('pieChart');
const log = getService('log'); const log = getService('log');
const browser = getService('browser'); const browser = getService('browser');
@ -22,120 +29,188 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const testSubjects = getService('testSubjects'); const testSubjects = getService('testSubjects');
const filterBar = getService('filterBar'); const filterBar = getService('filterBar');
const security = getService('security'); const security = getService('security');
const spaces = getService('spaces');
describe('Dashboard to dashboard drilldown', function () { describe('Dashboard to dashboard drilldown', function () {
before(async () => { describe('Create & use drilldowns', () => {
log.debug('Dashboard Drilldowns:initTests'); before(async () => {
await security.testUser.setRoles(['test_logstash_reader', 'global_dashboard_all']); log.debug('Dashboard Drilldowns:initTests');
await PageObjects.common.navigateToApp('dashboard'); await security.testUser.setRoles(['test_logstash_reader', 'global_dashboard_all']);
await PageObjects.dashboard.preserveCrossAppState(); await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.preserveCrossAppState();
});
after(async () => {
await security.testUser.restoreDefaults();
});
it('should create dashboard to dashboard drilldown, use it, and then delete it', async () => {
await PageObjects.dashboard.gotoDashboardEditMode(
dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME
);
// create drilldown
await dashboardPanelActions.openContextMenu();
await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction();
await dashboardDrilldownPanelActions.clickCreateDrilldown();
await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen();
await dashboardDrilldownsManage.fillInDashboardToDashboardDrilldownWizard({
drilldownName: DRILLDOWN_TO_AREA_CHART_NAME,
destinationDashboardTitle: dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME,
});
await dashboardDrilldownsManage.saveChanges();
await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose();
// check that drilldown notification badge is shown
expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(1);
// save dashboard, navigate to view mode
await PageObjects.dashboard.saveDashboard(
dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME,
{
saveAsNew: false,
waitDialogIsClosed: true,
}
);
// trigger drilldown action by clicking on a pie and picking drilldown action by it's name
await pieChart.clickOnPieSlice('40,000');
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();
const href = await dashboardDrilldownPanelActions.getActionHrefByText(
DRILLDOWN_TO_AREA_CHART_NAME
);
expect(typeof href).to.be('string'); // checking that action has a href
const dashboardIdFromHref = PageObjects.dashboard.getDashboardIdFromUrl(href);
await navigateWithinDashboard(async () => {
await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_AREA_CHART_NAME);
});
// checking that href is at least pointing to the same dashboard that we are navigated to by regular click
expect(dashboardIdFromHref).to.be(
await PageObjects.dashboard.getDashboardIdFromCurrentUrl()
);
// check that we drilled-down with filter from pie chart
expect(await filterBar.getFilterCount()).to.be(1);
const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
// brush area chart and drilldown back to pie chat dashboard
await brushAreaChart();
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();
await navigateWithinDashboard(async () => {
await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME);
});
// because filters are preserved during navigation, we expect that only one slice is displayed (filter is still applied)
expect(await filterBar.getFilterCount()).to.be(1);
await pieChart.expectPieSliceCount(1);
// check that new time range duration was applied
const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours);
// delete drilldown
await PageObjects.dashboard.switchToEditMode();
await dashboardPanelActions.openContextMenu();
await dashboardDrilldownPanelActions.expectExistsManageDrilldownsAction();
await dashboardDrilldownPanelActions.clickManageDrilldowns();
await dashboardDrilldownsManage.expectsManageDrilldownsFlyoutOpen();
await dashboardDrilldownsManage.deleteDrilldownsByTitles([DRILLDOWN_TO_AREA_CHART_NAME]);
await dashboardDrilldownsManage.closeFlyout();
// check that drilldown notification badge is shown
expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(0);
});
it('browser back/forward navigation works after drilldown navigation', async () => {
await PageObjects.dashboard.loadSavedDashboard(
dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME
);
const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
await brushAreaChart();
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();
await navigateWithinDashboard(async () => {
await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME);
});
// check that new time range duration was applied
const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours);
await navigateWithinDashboard(async () => {
await browser.goBack();
});
expect(await PageObjects.timePicker.getTimeDurationInHours()).to.be(
originalTimeRangeDurationHours
);
});
}); });
after(async () => { describe('Copy to space', () => {
await security.testUser.restoreDefaults(); const destinationSpaceId = 'custom_space';
}); before(async () => {
await spaces.create({
it('should create dashboard to dashboard drilldown, use it, and then delete it', async () => { id: destinationSpaceId,
await PageObjects.dashboard.gotoDashboardEditMode( name: 'custom_space',
dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME disabledFeatures: [],
); });
await PageObjects.settings.navigateTo();
// create drilldown await PageObjects.settings.clickKibanaSavedObjects();
await dashboardPanelActions.openContextMenu();
await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction();
await dashboardDrilldownPanelActions.clickCreateDrilldown();
await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen();
await dashboardDrilldownsManage.fillInDashboardToDashboardDrilldownWizard({
drilldownName: DRILLDOWN_TO_AREA_CHART_NAME,
destinationDashboardTitle: dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME,
});
await dashboardDrilldownsManage.saveChanges();
await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose();
// check that drilldown notification badge is shown
expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(1);
// save dashboard, navigate to view mode
await PageObjects.dashboard.saveDashboard(
dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME,
{
saveAsNew: false,
waitDialogIsClosed: true,
}
);
// trigger drilldown action by clicking on a pie and picking drilldown action by it's name
await pieChart.clickOnPieSlice('40,000');
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();
const href = await dashboardDrilldownPanelActions.getActionHrefByText(
DRILLDOWN_TO_AREA_CHART_NAME
);
expect(typeof href).to.be('string'); // checking that action has a href
const dashboardIdFromHref = PageObjects.dashboard.getDashboardIdFromUrl(href);
await navigateWithinDashboard(async () => {
await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_AREA_CHART_NAME);
});
// checking that href is at least pointing to the same dashboard that we are navigated to by regular click
expect(dashboardIdFromHref).to.be(await PageObjects.dashboard.getDashboardIdFromCurrentUrl());
// check that we drilled-down with filter from pie chart
expect(await filterBar.getFilterCount()).to.be(1);
const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
// brush area chart and drilldown back to pie chat dashboard
await brushAreaChart();
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();
await navigateWithinDashboard(async () => {
await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME);
}); });
// because filters are preserved during navigation, we expect that only one slice is displayed (filter is still applied) after(async () => {
expect(await filterBar.getFilterCount()).to.be(1); await spaces.delete(destinationSpaceId);
await pieChart.expectPieSliceCount(1);
// check that new time range duration was applied
const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours);
// delete drilldown
await PageObjects.dashboard.switchToEditMode();
await dashboardPanelActions.openContextMenu();
await dashboardDrilldownPanelActions.expectExistsManageDrilldownsAction();
await dashboardDrilldownPanelActions.clickManageDrilldowns();
await dashboardDrilldownsManage.expectsManageDrilldownsFlyoutOpen();
await dashboardDrilldownsManage.deleteDrilldownsByTitles([DRILLDOWN_TO_AREA_CHART_NAME]);
await dashboardDrilldownsManage.closeFlyout();
// check that drilldown notification badge is shown
expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(0);
});
it('browser back/forward navigation works after drilldown navigation', async () => {
await PageObjects.dashboard.loadSavedDashboard(
dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME
);
const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
await brushAreaChart();
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();
await navigateWithinDashboard(async () => {
await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME);
});
// check that new time range duration was applied
const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours);
await navigateWithinDashboard(async () => {
await browser.goBack();
}); });
expect(await PageObjects.timePicker.getTimeDurationInHours()).to.be( it('Dashboards linked by a drilldown are both copied to a space', async () => {
originalTimeRangeDurationHours await PageObjects.copySavedObjectsToSpace.openCopyToSpaceFlyoutForObject(
); dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME
);
await PageObjects.copySavedObjectsToSpace.setupForm({
destinationSpaceId,
});
await PageObjects.copySavedObjectsToSpace.startCopy();
// Wait for successful copy
await testSubjects.waitForDeleted(`cts-summary-indicator-loading-${destinationSpaceId}`);
await testSubjects.existOrFail(`cts-summary-indicator-success-${destinationSpaceId}`);
const summaryCounts = await PageObjects.copySavedObjectsToSpace.getSummaryCounts();
expect(summaryCounts).to.eql({
success: 5, // 2 dashboards (linked by a drilldown) + 2 visualizations + 1 index pattern
pending: 0,
skipped: 0,
errors: 0,
});
await PageObjects.copySavedObjectsToSpace.finishCopy();
// Actually use copied dashboards in a new space:
await PageObjects.common.navigateToApp('dashboard', {
basePath: `/s/${destinationSpaceId}`,
});
await PageObjects.dashboard.preserveCrossAppState();
await PageObjects.dashboard.loadSavedDashboard(
dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME
);
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.dashboard.waitForRenderComplete();
// brush area chart and drilldown back to pie chat dashboard
await brushAreaChart();
await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();
await navigateWithinDashboard(async () => {
await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME);
});
await pieChart.expectPieSliceCount(10);
});
}); });
}); });

View file

@ -126,7 +126,7 @@
"title": "Dashboard Foo", "title": "Dashboard Foo",
"hits": 0, "hits": 0,
"description": "", "description": "",
"panelsJSON": "[{}]", "panelsJSON": "[]",
"optionsJSON": "{}", "optionsJSON": "{}",
"version": 1, "version": 1,
"timeRestore": false, "timeRestore": false,
@ -156,7 +156,7 @@
"title": "Dashboard Bar", "title": "Dashboard Bar",
"hits": 0, "hits": 0,
"description": "", "description": "",
"panelsJSON": "[{}]", "panelsJSON": "[]",
"optionsJSON": "{}", "optionsJSON": "{}",
"version": 1, "version": 1,
"timeRestore": false, "timeRestore": false,

View file

@ -11,6 +11,12 @@
"title": "[Logs Sample] Overview ECS", "title": "[Logs Sample] Overview ECS",
"version": 1 "version": 1
}, },
"references": [
{ "id": "sample_visualization", "name": "panel_0", "type": "visualization" },
{ "id": "sample_search", "name": "panel_1", "type": "search" },
{ "id": "sample_search", "name": "panel_2", "type": "search" },
{ "id": "sample_visualization", "name": "panel_3", "type": "visualization" }
],
"id": "sample_dashboard", "id": "sample_dashboard",
"type": "dashboard" "type": "dashboard"
} }

View file

@ -11,6 +11,12 @@
"title": "[Logs Sample2] Overview ECS", "title": "[Logs Sample2] Overview ECS",
"version": 1 "version": 1
}, },
"references": [
{ "id": "sample_visualization", "name": "panel_0", "type": "visualization" },
{ "id": "sample_search", "name": "panel_1", "type": "search" },
{ "id": "sample_search", "name": "panel_2", "type": "search" },
{ "id": "sample_visualization", "name": "panel_3", "type": "visualization" }
],
"id": "sample_dashboard2", "id": "sample_dashboard2",
"type": "dashboard" "type": "dashboard"
} }

View file

@ -11,6 +11,12 @@
"title": "[Logs Sample] Overview ECS", "title": "[Logs Sample] Overview ECS",
"version": 1 "version": 1
}, },
"references": [
{ "id": "sample_visualization", "name": "panel_0", "type": "visualization" },
{ "id": "sample_search2", "name": "panel_1", "type": "search" },
{ "id": "sample_search2", "name": "panel_2", "type": "search" },
{ "id": "sample_visualization", "name": "panel_3", "type": "visualization" }
],
"id": "sample_dashboard", "id": "sample_dashboard",
"type": "dashboard" "type": "dashboard"
} }