parent
ae3d844872
commit
aec05c49a8
|
@ -1,11 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-embeddable-server](./kibana-plugin-plugins-embeddable-server.md) > [EmbeddableSetup](./kibana-plugin-plugins-embeddable-server.embeddablesetup.md) > [getAttributeService](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md)
|
||||
|
||||
## EmbeddableSetup.getAttributeService property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
getAttributeService: any;
|
||||
```
|
|
@ -7,14 +7,13 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface EmbeddableSetup
|
||||
export interface EmbeddableSetup extends PersistableStateService<EmbeddableStateWithType>
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| 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) => void</code> | |
|
||||
| [registerEnhancement](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerenhancement.md) | <code>(enhancement: EnhancementRegistryDefinition) => void</code> | |
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -21,9 +21,8 @@ import {
|
|||
convertSavedDashboardPanelToPanelState,
|
||||
convertPanelStateToSavedDashboardPanel,
|
||||
} from './embeddable_saved_object_converters';
|
||||
import { SavedDashboardPanel } from '../../types';
|
||||
import { DashboardPanelState } from '../embeddable';
|
||||
import { EmbeddableInput } from '../../../../embeddable/public';
|
||||
import { SavedDashboardPanel, DashboardPanelState } from '../types';
|
||||
import { EmbeddableInput } from '../../../embeddable/common/types';
|
||||
|
||||
test('convertSavedDashboardPanelToPanelState', () => {
|
||||
const savedDashboardPanel: SavedDashboardPanel = {
|
||||
|
@ -135,3 +134,24 @@ test('convertPanelStateToSavedDashboardPanel will not add an undefined id when n
|
|||
const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel, '8.0.0');
|
||||
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');
|
||||
});
|
|
@ -17,9 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { omit } from 'lodash';
|
||||
import { SavedDashboardPanel } from '../../types';
|
||||
import { DashboardPanelState } from '../embeddable';
|
||||
import { SavedObjectEmbeddableInput } from '../../embeddable_plugin';
|
||||
import { DashboardPanelState, SavedDashboardPanel } from '../types';
|
||||
import { SavedObjectEmbeddableInput } from '../../../embeddable/common/';
|
||||
|
||||
export function convertSavedDashboardPanelToPanelState(
|
||||
savedDashboardPanel: SavedDashboardPanel
|
||||
|
@ -49,7 +48,7 @@ export function convertPanelStateToSavedDashboardPanel(
|
|||
type: panelState.type,
|
||||
gridData: panelState.gridData,
|
||||
panelIndex: panelState.explicitInput.id,
|
||||
embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId']),
|
||||
embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId', 'title']),
|
||||
...(customTitle && { title: customTitle }),
|
||||
...(savedObjectId !== undefined && { id: savedObjectId }),
|
||||
};
|
|
@ -17,8 +17,18 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { extractReferences, injectReferences } from './saved_dashboard_references';
|
||||
import { SavedObjectDashboard } from './saved_dashboard';
|
||||
import {
|
||||
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', () => {
|
||||
test('extracts references from panelsJSON', () => {
|
||||
|
@ -41,28 +51,28 @@ describe('extractReferences', () => {
|
|||
},
|
||||
references: [],
|
||||
};
|
||||
const updatedDoc = extractReferences(doc);
|
||||
const updatedDoc = extractReferences(doc, deps);
|
||||
|
||||
expect(updatedDoc).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"foo": true,
|
||||
"panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_0\\"},{\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_1\\"}]",
|
||||
},
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "panel_0",
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"id": "2",
|
||||
"name": "panel_1",
|
||||
"type": "visualization",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"foo": true,
|
||||
"panelsJSON": "[{\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_0\\"},{\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_1\\"}]",
|
||||
},
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "panel_0",
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"id": "2",
|
||||
"name": "panel_1",
|
||||
"type": "visualization",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('fails when "type" attribute is missing from a panel', () => {
|
||||
|
@ -79,7 +89,7 @@ Object {
|
|||
},
|
||||
references: [],
|
||||
};
|
||||
expect(() => extractReferences(doc)).toThrowErrorMatchingInlineSnapshot(
|
||||
expect(() => extractReferences(doc, deps)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"\\"type\\" attribute is missing from panel \\"0\\""`
|
||||
);
|
||||
});
|
||||
|
@ -98,21 +108,21 @@ Object {
|
|||
},
|
||||
references: [],
|
||||
};
|
||||
expect(extractReferences(doc)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"foo": true,
|
||||
"panelsJSON": "[{\\"type\\":\\"visualization\\",\\"title\\":\\"Title 1\\"}]",
|
||||
},
|
||||
"references": Array [],
|
||||
}
|
||||
`);
|
||||
expect(extractReferences(doc, deps)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"foo": true,
|
||||
"panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\"}]",
|
||||
},
|
||||
"references": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('injectReferences', () => {
|
||||
test('injects references into context', () => {
|
||||
const context = {
|
||||
test('returns injected attributes', () => {
|
||||
const attributes = {
|
||||
id: '1',
|
||||
title: 'test',
|
||||
panelsJSON: JSON.stringify([
|
||||
|
@ -125,7 +135,7 @@ describe('injectReferences', () => {
|
|||
title: 'Title 2',
|
||||
},
|
||||
]),
|
||||
} as SavedObjectDashboard;
|
||||
};
|
||||
const references = [
|
||||
{
|
||||
name: 'panel_0',
|
||||
|
@ -138,49 +148,49 @@ describe('injectReferences', () => {
|
|||
id: '2',
|
||||
},
|
||||
];
|
||||
injectReferences(context, references);
|
||||
const newAttributes = injectReferences({ attributes, references }, deps);
|
||||
|
||||
expect(context).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "1",
|
||||
"panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\",\\"type\\":\\"visualization\\"},{\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\",\\"type\\":\\"visualization\\"}]",
|
||||
"title": "test",
|
||||
}
|
||||
`);
|
||||
expect(newAttributes).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "1",
|
||||
"panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\"}]",
|
||||
"title": "test",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('skips when panelsJSON is missing', () => {
|
||||
const context = {
|
||||
const attributes = {
|
||||
id: '1',
|
||||
title: 'test',
|
||||
} as SavedObjectDashboard;
|
||||
injectReferences(context, []);
|
||||
expect(context).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "1",
|
||||
"title": "test",
|
||||
}
|
||||
`);
|
||||
};
|
||||
const newAttributes = injectReferences({ attributes, references: [] }, deps);
|
||||
expect(newAttributes).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "1",
|
||||
"title": "test",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('skips when panelsJSON is not an array', () => {
|
||||
const context = {
|
||||
const attributes = {
|
||||
id: '1',
|
||||
panelsJSON: '{}',
|
||||
title: 'test',
|
||||
} as SavedObjectDashboard;
|
||||
injectReferences(context, []);
|
||||
expect(context).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "1",
|
||||
"panelsJSON": "{}",
|
||||
"title": "test",
|
||||
}
|
||||
`);
|
||||
};
|
||||
const newAttributes = injectReferences({ attributes, references: [] }, deps);
|
||||
expect(newAttributes).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "1",
|
||||
"panelsJSON": "{}",
|
||||
"title": "test",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('skips a panel when panelRefName is missing', () => {
|
||||
const context = {
|
||||
const attributes = {
|
||||
id: '1',
|
||||
title: 'test',
|
||||
panelsJSON: JSON.stringify([
|
||||
|
@ -192,7 +202,7 @@ Object {
|
|||
title: 'Title 2',
|
||||
},
|
||||
]),
|
||||
} as SavedObjectDashboard;
|
||||
};
|
||||
const references = [
|
||||
{
|
||||
name: 'panel_0',
|
||||
|
@ -200,18 +210,18 @@ Object {
|
|||
id: '1',
|
||||
},
|
||||
];
|
||||
injectReferences(context, references);
|
||||
expect(context).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "1",
|
||||
"panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\",\\"type\\":\\"visualization\\"},{\\"title\\":\\"Title 2\\"}]",
|
||||
"title": "test",
|
||||
}
|
||||
`);
|
||||
const newAttributes = injectReferences({ attributes, references }, deps);
|
||||
expect(newAttributes).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "1",
|
||||
"panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\"}]",
|
||||
"title": "test",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test(`fails when it can't find the reference in the array`, () => {
|
||||
const context = {
|
||||
const attributes = {
|
||||
id: '1',
|
||||
title: 'test',
|
||||
panelsJSON: JSON.stringify([
|
||||
|
@ -220,9 +230,9 @@ Object {
|
|||
title: 'Title 1',
|
||||
},
|
||||
]),
|
||||
} as SavedObjectDashboard;
|
||||
expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Could not find reference \\"panel_0\\""`
|
||||
);
|
||||
};
|
||||
expect(() =>
|
||||
injectReferences({ attributes, references: [] }, deps)
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Could not find reference \\"panel_0\\""`);
|
||||
});
|
||||
});
|
|
@ -17,18 +17,47 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { SavedObjectAttributes, SavedObjectReference } from 'kibana/public';
|
||||
import { SavedObjectDashboard } from './saved_dashboard';
|
||||
import { SavedObjectAttributes, SavedObjectReference } from '../../../core/types';
|
||||
import {
|
||||
extractPanelsReferences,
|
||||
injectPanelsReferences,
|
||||
} from './embeddable/embeddable_references';
|
||||
import { SavedDashboardPanel730ToLatest } from './types';
|
||||
import { EmbeddablePersistableStateService } from '../../embeddable/common/types';
|
||||
|
||||
export function extractReferences({
|
||||
attributes,
|
||||
references = [],
|
||||
}: {
|
||||
export interface ExtractDeps {
|
||||
embeddablePersistableStateService: EmbeddablePersistableStateService;
|
||||
}
|
||||
|
||||
export interface SavedObjectAttributesAndReferences {
|
||||
attributes: SavedObjectAttributes;
|
||||
references: SavedObjectReference[];
|
||||
}) {
|
||||
}
|
||||
|
||||
export function extractReferences(
|
||||
{ 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));
|
||||
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) => {
|
||||
if (!panel.type) {
|
||||
throw new Error(`"type" attribute is missing from panel "${i}"`);
|
||||
|
@ -46,6 +75,7 @@ export function extractReferences({
|
|||
delete panel.type;
|
||||
delete panel.id;
|
||||
});
|
||||
|
||||
return {
|
||||
references: [...references, ...panelReferences],
|
||||
attributes: {
|
||||
|
@ -55,21 +85,28 @@ export function extractReferences({
|
|||
};
|
||||
}
|
||||
|
||||
export interface InjectDeps {
|
||||
embeddablePersistableStateService: EmbeddablePersistableStateService;
|
||||
}
|
||||
|
||||
export function injectReferences(
|
||||
savedObject: SavedObjectDashboard,
|
||||
references: SavedObjectReference[]
|
||||
) {
|
||||
{ attributes, references = [] }: SavedObjectAttributesAndReferences,
|
||||
deps: InjectDeps
|
||||
): SavedObjectAttributes {
|
||||
// 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
|
||||
// object has panelsJSON in all previous versions of kibana.
|
||||
if (typeof savedObject.panelsJSON !== 'string') {
|
||||
return;
|
||||
if (typeof attributes.panelsJSON !== 'string') {
|
||||
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.
|
||||
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) => {
|
||||
if (!panel.panelRefName) {
|
||||
return;
|
||||
|
@ -84,5 +121,11 @@ export function injectReferences(
|
|||
panel.type = reference.type;
|
||||
delete panel.panelRefName;
|
||||
});
|
||||
savedObject.panelsJSON = JSON.stringify(panels);
|
||||
|
||||
panels = injectPanelsReferences(panels, references, deps);
|
||||
|
||||
return {
|
||||
...attributes,
|
||||
panelsJSON: JSON.stringify(panels),
|
||||
};
|
||||
}
|
|
@ -17,6 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { EmbeddableInput, PanelState } from '../../../../src/plugins/embeddable/common/types';
|
||||
import { SavedObjectEmbeddableInput } from '../../../../src/plugins/embeddable/common/lib/saved_object_embeddable';
|
||||
import {
|
||||
RawSavedDashboardPanelTo60,
|
||||
RawSavedDashboardPanel610,
|
||||
|
@ -26,6 +28,21 @@ import {
|
|||
RawSavedDashboardPanel730ToLatest,
|
||||
} 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<
|
||||
RawSavedDashboardPanel640To720,
|
||||
Exclude<keyof RawSavedDashboardPanel640To720, 'name'>
|
||||
|
|
|
@ -81,7 +81,6 @@ import { getTopNavConfig } from './top_nav/get_top_nav_config';
|
|||
import { TopNavIds } from './top_nav/top_nav_ids';
|
||||
import { getDashboardTitle } from './dashboard_strings';
|
||||
import { DashboardAppScope } from './dashboard_app';
|
||||
import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters';
|
||||
import { RenderDeps } from './application';
|
||||
import {
|
||||
IKbnUrlStateStorage,
|
||||
|
@ -97,6 +96,7 @@ import {
|
|||
subscribeWithScope,
|
||||
} from '../../../kibana_legacy/public';
|
||||
import { migrateLegacyQuery } from './lib/migrate_legacy_query';
|
||||
import { convertSavedDashboardPanelToPanelState } from '../../common/embeddable/embeddable_saved_object_converters';
|
||||
|
||||
export interface DashboardAppControllerDependencies extends RenderDeps {
|
||||
$scope: DashboardAppScope;
|
||||
|
|
|
@ -30,7 +30,6 @@ import { migrateLegacyQuery } from './lib/migrate_legacy_query';
|
|||
|
||||
import { ViewMode } from '../embeddable_plugin';
|
||||
import { getAppStateDefaults, migrateAppState, getDashboardIdFromUrl } from './lib';
|
||||
import { convertPanelStateToSavedDashboardPanel } from './lib/embeddable_saved_object_converters';
|
||||
import { FilterUtils } from './lib/filter_utils';
|
||||
import {
|
||||
DashboardAppState,
|
||||
|
@ -48,6 +47,7 @@ import {
|
|||
} from '../../../kibana_utils/public';
|
||||
import { SavedObjectDashboard } from '../saved_dashboards';
|
||||
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
|
||||
|
|
|
@ -16,14 +16,4 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { SavedObjectEmbeddableInput } from 'src/plugins/embeddable/public';
|
||||
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;
|
||||
}
|
||||
export * from '../../../common/types';
|
||||
|
|
|
@ -450,6 +450,7 @@ export class DashboardPlugin
|
|||
const savedDashboardLoader = createSavedDashboardLoader({
|
||||
savedObjectsClient: core.savedObjects.client,
|
||||
savedObjects: plugins.savedObjects,
|
||||
embeddableStart: plugins.embeddable,
|
||||
});
|
||||
const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory(
|
||||
DASHBOARD_CONTAINER_TYPE
|
||||
|
|
|
@ -16,6 +16,6 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export * from './saved_dashboard_references';
|
||||
export * from '../../common/saved_dashboard_references';
|
||||
export * from './saved_dashboard';
|
||||
export * from './saved_dashboards';
|
||||
|
|
|
@ -17,10 +17,12 @@
|
|||
* under the License.
|
||||
*/
|
||||
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 { 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 {
|
||||
id?: string;
|
||||
|
@ -41,7 +43,8 @@ export interface SavedObjectDashboard extends SavedObject {
|
|||
|
||||
// Used only by the savedDashboards service, usually no reason to change this
|
||||
export function createSavedDashboardClass(
|
||||
savedObjectStart: SavedObjectsStart
|
||||
savedObjectStart: SavedObjectsStart,
|
||||
embeddableStart: EmbeddableStart
|
||||
): new (id: string) => SavedObjectDashboard {
|
||||
class SavedDashboard extends savedObjectStart.SavedObjectClass {
|
||||
// save these objects with the 'dashboard' type
|
||||
|
@ -77,8 +80,19 @@ export function createSavedDashboardClass(
|
|||
type: SavedDashboard.type,
|
||||
mapping: SavedDashboard.mapping,
|
||||
searchSource: SavedDashboard.searchSource,
|
||||
extractReferences,
|
||||
injectReferences,
|
||||
extractReferences: (opts: {
|
||||
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
|
||||
id,
|
||||
|
|
|
@ -20,16 +20,22 @@
|
|||
import { SavedObjectsClientContract } from 'kibana/public';
|
||||
import { SavedObjectLoader, SavedObjectsStart } from '../../../../plugins/saved_objects/public';
|
||||
import { createSavedDashboardClass } from './saved_dashboard';
|
||||
import { EmbeddableStart } from '../../../embeddable/public';
|
||||
|
||||
interface Services {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
savedObjects: SavedObjectsStart;
|
||||
embeddableStart: EmbeddableStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param services
|
||||
*/
|
||||
export function createSavedDashboardLoader({ savedObjects, savedObjectsClient }: Services) {
|
||||
const SavedDashboard = createSavedDashboardClass(savedObjects);
|
||||
export function createSavedDashboardLoader({
|
||||
savedObjects,
|
||||
savedObjectsClient,
|
||||
embeddableStart,
|
||||
}: Services) {
|
||||
const SavedDashboard = createSavedDashboardClass(savedObjects, embeddableStart);
|
||||
return new SavedObjectLoader(SavedDashboard, savedObjectsClient);
|
||||
}
|
||||
|
|
|
@ -19,9 +19,12 @@
|
|||
|
||||
import { Query, Filter } from 'src/plugins/data/public';
|
||||
import { SavedObject as SavedObjectType, SavedObjectAttributes } from 'src/core/public';
|
||||
import { SavedDashboardPanel730ToLatest } from '../common';
|
||||
|
||||
import { ViewMode } from './embeddable_plugin';
|
||||
|
||||
import { SavedDashboardPanel } from '../common/types';
|
||||
export { SavedDashboardPanel };
|
||||
|
||||
export interface DashboardCapabilities {
|
||||
showWriteControls: boolean;
|
||||
createNew: boolean;
|
||||
|
@ -71,11 +74,6 @@ export interface Field {
|
|||
|
||||
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 {
|
||||
panels: SavedDashboardPanel[];
|
||||
fullScreenMode: boolean;
|
||||
|
|
|
@ -25,22 +25,34 @@ import {
|
|||
Logger,
|
||||
} from '../../../core/server';
|
||||
|
||||
import { dashboardSavedObjectType } from './saved_objects';
|
||||
import { createDashboardSavedObjectType } from './saved_objects';
|
||||
import { capabilitiesProvider } from './capabilities_provider';
|
||||
|
||||
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;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.logger = initializerContext.logger.get();
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup) {
|
||||
public setup(core: CoreSetup, plugins: SetupDeps) {
|
||||
this.logger.debug('dashboard: Setup');
|
||||
|
||||
core.savedObjects.registerType(dashboardSavedObjectType);
|
||||
core.savedObjects.registerType(
|
||||
createDashboardSavedObjectType({
|
||||
migrationDeps: {
|
||||
embeddable: plugins.embeddable,
|
||||
},
|
||||
})
|
||||
);
|
||||
core.capabilities.registerProvider(capabilitiesProvider);
|
||||
|
||||
return {};
|
||||
|
|
|
@ -18,9 +18,16 @@
|
|||
*/
|
||||
|
||||
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',
|
||||
hidden: false,
|
||||
namespaceType: 'single',
|
||||
|
@ -65,5 +72,5 @@ export const dashboardSavedObjectType: SavedObjectsType = {
|
|||
version: { type: 'integer' },
|
||||
},
|
||||
},
|
||||
migrations: dashboardSavedObjectTypeMigrations,
|
||||
};
|
||||
migrations: createDashboardSavedObjectTypeMigrations(migrationDeps),
|
||||
});
|
||||
|
|
|
@ -19,7 +19,14 @@
|
|||
|
||||
import { SavedObjectUnsanitizedDoc } from 'kibana/server';
|
||||
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();
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,11 +18,12 @@
|
|||
*/
|
||||
|
||||
import { get, flow } from 'lodash';
|
||||
|
||||
import { SavedObjectMigrationFn } from 'kibana/server';
|
||||
import { SavedObjectAttributes, SavedObjectMigrationFn } from 'kibana/server';
|
||||
import { migrations730 } from './migrations_730';
|
||||
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) {
|
||||
const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON');
|
||||
|
@ -100,7 +101,57 @@ const migrations700: SavedObjectMigrationFn<any, any> = (doc): DashboardDoc700To
|
|||
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
|
||||
* 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.3.0': flow(migrations730),
|
||||
'7.9.3': flow(migrateMatchAllQuery),
|
||||
};
|
||||
'7.11.0': flow(createExtractPanelReferencesMigration(deps)),
|
||||
});
|
||||
|
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { dashboardSavedObjectType } from './dashboard';
|
||||
export { createDashboardSavedObjectType } from './dashboard';
|
||||
|
|
|
@ -18,12 +18,16 @@
|
|||
*/
|
||||
|
||||
import { savedObjectsServiceMock } from '../../../../core/server/mocks';
|
||||
import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations';
|
||||
import { createDashboardSavedObjectTypeMigrations } from './dashboard_migrations';
|
||||
import { migrations730 } from './migrations_730';
|
||||
import { DashboardDoc700To720, DashboardDoc730ToLatest, DashboardDocPre700 } from '../../common';
|
||||
import { RawSavedDashboardPanel730ToLatest } from '../../common';
|
||||
import { createEmbeddableSetupMock } from '../../../embeddable/server/mocks';
|
||||
|
||||
const mockContext = savedObjectsServiceMock.createMigrationContext();
|
||||
const migrations = createDashboardSavedObjectTypeMigrations({
|
||||
embeddable: createEmbeddableSetupMock(),
|
||||
});
|
||||
|
||||
test('dashboard migration 7.3.0 migrates filters to query on search source', () => {
|
||||
const doc: DashboardDoc700To720 = {
|
||||
|
|
21
src/plugins/embeddable/common/index.ts
Normal file
21
src/plugins/embeddable/common/index.ts
Normal 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';
|
|
@ -22,3 +22,4 @@ export * from './inject';
|
|||
export * from './migrate';
|
||||
export * from './migrate_base_input';
|
||||
export * from './telemetry';
|
||||
export * from './saved_object_embeddable';
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { EmbeddableInput } from '..';
|
||||
import { EmbeddableInput } from '../types';
|
||||
|
||||
export interface SavedObjectEmbeddableInput extends EmbeddableInput {
|
||||
savedObjectId: string;
|
31
src/plugins/embeddable/common/mocks.ts
Normal file
31
src/plugins/embeddable/common/mocks.ts
Normal 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) => ({})),
|
||||
};
|
||||
};
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { SerializableState } from '../../kibana_utils/common';
|
||||
import { PersistableStateService, SerializableState } from '../../kibana_utils/common';
|
||||
import { Query, TimeRange } from '../../data/common/query';
|
||||
import { Filter } from '../../data/common/es_query/filters';
|
||||
|
||||
|
@ -74,8 +74,21 @@ export type EmbeddableInput = {
|
|||
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 EmbeddablePersistableStateService = PersistableStateService<EmbeddableStateWithType>;
|
||||
|
||||
export interface CommonEmbeddableStartContract {
|
||||
getEmbeddableFactory: (embeddableFactoryId: string) => any;
|
||||
getEnhancement: (enhancementId: string) => any;
|
||||
|
|
|
@ -31,7 +31,7 @@ import {
|
|||
import { IContainer, ContainerInput, ContainerOutput, PanelState } from './i_container';
|
||||
import { PanelNotFoundError, EmbeddableFactoryNotFoundError } from '../errors';
|
||||
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>;
|
||||
|
||||
|
|
|
@ -24,17 +24,9 @@ import {
|
|||
ErrorEmbeddable,
|
||||
IEmbeddable,
|
||||
} from '../embeddables';
|
||||
import { PanelState } from '../../../common/types';
|
||||
|
||||
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 { PanelState };
|
||||
|
||||
export interface ContainerOutput extends EmbeddableOutput {
|
||||
embeddableLoaded: { [key: string]: boolean };
|
||||
|
|
|
@ -24,5 +24,5 @@ export * from './default_embeddable_factory_provider';
|
|||
export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable';
|
||||
export { withEmbeddableSubscription } from './with_subscription';
|
||||
export { EmbeddableRoot } from './embeddable_root';
|
||||
export * from './saved_object_embeddable';
|
||||
export * from '../../../common/lib/saved_object_embeddable';
|
||||
export { EmbeddableRenderer, EmbeddableRendererProps } from './embeddable_renderer';
|
||||
|
|
30
src/plugins/embeddable/server/mocks.ts
Normal file
30
src/plugins/embeddable/server/mocks.ts
Normal 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();
|
|
@ -32,23 +32,32 @@ import {
|
|||
getMigrateFunction,
|
||||
getTelemetryFunction,
|
||||
} from '../common/lib';
|
||||
import { SerializableState } from '../../kibana_utils/common';
|
||||
import { PersistableStateService, SerializableState } from '../../kibana_utils/common';
|
||||
import { EmbeddableStateWithType } from '../common/types';
|
||||
|
||||
export interface EmbeddableSetup {
|
||||
getAttributeService: any;
|
||||
export interface EmbeddableSetup extends PersistableStateService<EmbeddableStateWithType> {
|
||||
registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => 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 enhancements: EnhancementsRegistry = new Map();
|
||||
|
||||
public setup(core: CoreSetup) {
|
||||
const commonContract = {
|
||||
getEmbeddableFactory: this.getEmbeddableFactory,
|
||||
getEnhancement: this.getEnhancement,
|
||||
};
|
||||
return {
|
||||
registerEmbeddableFactory: this.registerEmbeddableFactory,
|
||||
registerEnhancement: this.registerEnhancement,
|
||||
telemetry: getTelemetryFunction(commonContract),
|
||||
extract: getExtractFunction(commonContract),
|
||||
inject: getInjectFunction(commonContract),
|
||||
migrate: getMigrateFunction(commonContract),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -18,12 +18,11 @@ export interface EmbeddableRegistryDefinition<P extends EmbeddableStateWithType
|
|||
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)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export interface EmbeddableSetup {
|
||||
// (undocumented)
|
||||
getAttributeService: any;
|
||||
export interface EmbeddableSetup extends PersistableStateService<EmbeddableStateWithType> {
|
||||
// (undocumented)
|
||||
registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void;
|
||||
// (undocumented)
|
||||
|
|
|
@ -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';
|
|
@ -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');
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
};
|
|
@ -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';
|
|
@ -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;
|
||||
};
|
|
@ -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';
|
7
x-pack/plugins/dashboard_enhanced/common/index.ts
Normal file
7
x-pack/plugins/dashboard_enhanced/common/index.ts
Normal 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';
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "dashboardEnhanced",
|
||||
"version": "kibana",
|
||||
"server": false,
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["data", "uiActionsEnhanced", "embeddable", "dashboard", "share"],
|
||||
"configPath": ["xpack", "dashboardEnhanced"],
|
||||
|
|
|
@ -9,19 +9,19 @@ import { DataPublicPluginStart } from 'src/plugins/data/public';
|
|||
import { DashboardStart } from 'src/plugins/dashboard/public';
|
||||
import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import {
|
||||
TriggerId,
|
||||
TriggerContextMapping,
|
||||
TriggerId,
|
||||
} from '../../../../../../../src/plugins/ui_actions/public';
|
||||
import { CollectConfigContainer } from './components';
|
||||
import {
|
||||
UiActionsEnhancedDrilldownDefinition as Drilldown,
|
||||
UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext,
|
||||
AdvancedUiActionsStart,
|
||||
UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext,
|
||||
UiActionsEnhancedDrilldownDefinition as Drilldown,
|
||||
} from '../../../../../ui_actions_enhanced/public';
|
||||
import { txtGoToDashboard } from './i18n';
|
||||
import {
|
||||
StartServicesGetter,
|
||||
CollectConfigProps,
|
||||
StartServicesGetter,
|
||||
} from '../../../../../../../src/plugins/kibana_utils/public';
|
||||
import { KibanaURL } from '../../../../../../../src/plugins/share/public';
|
||||
import { Config } from './types';
|
||||
|
|
|
@ -6,12 +6,8 @@
|
|||
|
||||
import { UiActionsEnhancedBaseActionFactoryContext } from '../../../../../ui_actions_enhanced/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 = {
|
||||
dashboardId?: string;
|
||||
useCurrentFilters: boolean;
|
||||
useCurrentDateRange: boolean;
|
||||
};
|
||||
export type Config = DrilldownConfig;
|
||||
|
||||
export type FactoryContext = UiActionsEnhancedBaseActionFactoryContext<typeof APPLY_FILTER_TRIGGER>;
|
||||
|
|
|
@ -65,6 +65,12 @@ test('getHref is defined', () => {
|
|||
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', () => {
|
||||
/**
|
||||
* A convenience test setup helper
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
} from '../abstract_dashboard_drilldown';
|
||||
import { KibanaURL } from '../../../../../../../src/plugins/share/public';
|
||||
import { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants';
|
||||
import { createExtract, createInject } from '../../../../common';
|
||||
|
||||
type Trigger = typeof APPLY_FILTER_TRIGGER;
|
||||
type Context = TriggerContextMapping[Trigger];
|
||||
|
@ -80,4 +81,8 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown<T
|
|||
|
||||
return url;
|
||||
}
|
||||
|
||||
public readonly inject = createInject({ drilldownId: this.id });
|
||||
|
||||
public readonly extract = createExtract({ drilldownId: this.id });
|
||||
}
|
||||
|
|
19
x-pack/plugins/dashboard_enhanced/server/index.ts
Normal file
19
x-pack/plugins/dashboard_enhanced/server/index.ts
Normal 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);
|
||||
}
|
44
x-pack/plugins/dashboard_enhanced/server/plugin.ts
Normal file
44
x-pack/plugins/dashboard_enhanced/server/plugin.ts
Normal 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() {}
|
||||
}
|
7
x-pack/plugins/ui_actions_enhanced/common/index.ts
Normal file
7
x-pack/plugins/ui_actions_enhanced/common/index.ts
Normal 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';
|
|
@ -7,11 +7,11 @@
|
|||
import { EnhancementRegistryDefinition } from '../../../../src/plugins/embeddable/server';
|
||||
import { SavedObjectReference } from '../../../../src/core/types';
|
||||
import { DynamicActionsState, SerializedEvent } from './types';
|
||||
import { AdvancedUiActionsPublicPlugin } from './plugin';
|
||||
import { AdvancedUiActionsServerPlugin } from './plugin';
|
||||
import { SerializableState } from '../../../../src/plugins/kibana_utils/common';
|
||||
|
||||
export const dynamicActionEnhancement = (
|
||||
uiActionsEnhanced: AdvancedUiActionsPublicPlugin
|
||||
uiActionsEnhanced: AdvancedUiActionsServerPlugin
|
||||
): EnhancementRegistryDefinition => {
|
||||
return {
|
||||
id: 'dynamicActions',
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AdvancedUiActionsPublicPlugin } from './plugin';
|
||||
import { AdvancedUiActionsServerPlugin } from './plugin';
|
||||
|
||||
export function plugin() {
|
||||
return new AdvancedUiActionsPublicPlugin();
|
||||
return new AdvancedUiActionsServerPlugin();
|
||||
}
|
||||
|
||||
export { AdvancedUiActionsPublicPlugin as Plugin };
|
||||
export { AdvancedUiActionsServerPlugin as Plugin };
|
||||
export {
|
||||
SetupContract as AdvancedUiActionsSetup,
|
||||
StartContract as AdvancedUiActionsStart,
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from './types';
|
||||
|
||||
export interface SetupContract {
|
||||
registerActionFactory: any;
|
||||
registerActionFactory: (definition: ActionFactoryDefinition) => void;
|
||||
}
|
||||
|
||||
export type StartContract = void;
|
||||
|
@ -25,7 +25,7 @@ interface SetupDependencies {
|
|||
embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions.
|
||||
}
|
||||
|
||||
export class AdvancedUiActionsPublicPlugin
|
||||
export class AdvancedUiActionsServerPlugin
|
||||
implements Plugin<SetupContract, StartContract, SetupDependencies> {
|
||||
protected readonly actionFactories: ActionFactoryRegistry = new Map();
|
||||
|
||||
|
|
|
@ -14,7 +14,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const dashboardPanelActions = getService('dashboardPanelActions');
|
||||
const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions');
|
||||
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 log = getService('log');
|
||||
const browser = getService('browser');
|
||||
|
@ -22,120 +29,188 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const testSubjects = getService('testSubjects');
|
||||
const filterBar = getService('filterBar');
|
||||
const security = getService('security');
|
||||
const spaces = getService('spaces');
|
||||
|
||||
describe('Dashboard to dashboard drilldown', function () {
|
||||
before(async () => {
|
||||
log.debug('Dashboard Drilldowns:initTests');
|
||||
await security.testUser.setRoles(['test_logstash_reader', 'global_dashboard_all']);
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
await PageObjects.dashboard.preserveCrossAppState();
|
||||
describe('Create & use drilldowns', () => {
|
||||
before(async () => {
|
||||
log.debug('Dashboard Drilldowns:initTests');
|
||||
await security.testUser.setRoles(['test_logstash_reader', 'global_dashboard_all']);
|
||||
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 () => {
|
||||
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);
|
||||
describe('Copy to space', () => {
|
||||
const destinationSpaceId = 'custom_space';
|
||||
before(async () => {
|
||||
await spaces.create({
|
||||
id: destinationSpaceId,
|
||||
name: 'custom_space',
|
||||
disabledFeatures: [],
|
||||
});
|
||||
await PageObjects.settings.navigateTo();
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
});
|
||||
|
||||
// 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();
|
||||
after(async () => {
|
||||
await spaces.delete(destinationSpaceId);
|
||||
});
|
||||
|
||||
expect(await PageObjects.timePicker.getTimeDurationInHours()).to.be(
|
||||
originalTimeRangeDurationHours
|
||||
);
|
||||
it('Dashboards linked by a drilldown are both copied to a space', async () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Binary file not shown.
|
@ -126,7 +126,7 @@
|
|||
"title": "Dashboard Foo",
|
||||
"hits": 0,
|
||||
"description": "",
|
||||
"panelsJSON": "[{}]",
|
||||
"panelsJSON": "[]",
|
||||
"optionsJSON": "{}",
|
||||
"version": 1,
|
||||
"timeRestore": false,
|
||||
|
@ -156,7 +156,7 @@
|
|||
"title": "Dashboard Bar",
|
||||
"hits": 0,
|
||||
"description": "",
|
||||
"panelsJSON": "[{}]",
|
||||
"panelsJSON": "[]",
|
||||
"optionsJSON": "{}",
|
||||
"version": 1,
|
||||
"timeRestore": false,
|
||||
|
|
|
@ -11,6 +11,12 @@
|
|||
"title": "[Logs Sample] Overview ECS",
|
||||
"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",
|
||||
"type": "dashboard"
|
||||
}
|
|
@ -11,6 +11,12 @@
|
|||
"title": "[Logs Sample2] Overview ECS",
|
||||
"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",
|
||||
"type": "dashboard"
|
||||
}
|
|
@ -11,6 +11,12 @@
|
|||
"title": "[Logs Sample] Overview ECS",
|
||||
"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",
|
||||
"type": "dashboard"
|
||||
}
|
Loading…
Reference in a new issue