[Dashboard] Content Management onboard (#158761)

Onboard Dashboard onto Content Management.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Devon Thomson 2023-06-09 16:03:23 -04:00 committed by GitHub
parent f895c5c205
commit b6f9825253
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
73 changed files with 1189 additions and 862 deletions

View file

@ -81,7 +81,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"connector_token": "5a9ac29fe9c740eb114e9c40517245c71706b005",
"core-usage-stats": "b3c04da317c957741ebcdedfea4524049fdc79ff",
"csp-rule-template": "c151324d5f85178169395eecb12bac6b96064654",
"dashboard": "cf7c9c2334decab716fe519780cb4dc52967a91d",
"dashboard": "1635368413415b340ae6f43fcd0a55c5dcdd4f41",
"endpoint:user-artifact-manifest": "1c3533161811a58772e30cdc77bac4631da3ef2b",
"enterprise_search_telemetry": "9ac912e1417fc8681e0cd383775382117c9e3d3d",
"epm-packages": "2449bb565f987eff70b1b39578bb17e90c404c6e",

View file

@ -105,7 +105,7 @@ export const controlGroupInputToRawControlGroupAttributes = (
export const rawControlGroupAttributesToControlGroupInput = (
rawControlGroupAttributes: RawControlGroupAttributes
): Omit<ControlGroupInput, 'id'> | undefined => {
): PersistableControlGroupInput | undefined => {
const defaultControlGroupInput = getDefaultControlGroupInput();
const { chainingSystem, controlStyle, ignoreParentSettingsJSON, panelsJSON } =
rawControlGroupAttributes;

View file

@ -8,29 +8,28 @@
import type { SavedObjectReference } from '@kbn/core/public';
import type { Serializable } from '@kbn/utility-types';
import { GridData } from '../content_management';
import { GridData } from '..';
interface SavedObjectAttributes {
interface KibanaAttributes {
kibanaSavedObjectMeta: {
searchSourceJSON: string;
};
}
interface Doc<Attributes extends SavedObjectAttributes = SavedObjectAttributes> {
interface Doc<Attributes extends KibanaAttributes = KibanaAttributes> {
references: SavedObjectReference[];
attributes: Attributes;
id: string;
type: string;
}
interface DocPre700<Attributes extends SavedObjectAttributes = SavedObjectAttributes> {
interface DocPre700<Attributes extends KibanaAttributes = KibanaAttributes> {
attributes: Attributes;
id: string;
type: string;
}
interface DashboardAttributes extends SavedObjectAttributes {
interface DashboardAttributes extends KibanaAttributes {
panelsJSON: string;
description: string;
version: number;
@ -40,7 +39,7 @@ interface DashboardAttributes extends SavedObjectAttributes {
optionsJSON?: string;
}
interface DashboardAttributesTo720 extends SavedObjectAttributes {
interface DashboardAttributesTo720 extends KibanaAttributes {
panelsJSON: string;
description: string;
uiStateJSON?: string;

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type {
ContentManagementServicesDefinition as ServicesDefinition,
Version,
} from '@kbn/object-versioning';
// We export the versioned service definition from this file and not the barrel to avoid adding
// the schemas in the "public" js bundle
import { serviceDefinition as v1 } from './v1/cm_services';
export const cmServicesDefinition: { [version: Version]: ServicesDefinition } = {
1: v1,
};

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const LATEST_VERSION = 1;
export const CONTENT_ID = 'dashboard';

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { LATEST_VERSION, CONTENT_ID } from './constants';
export type { DashboardContentType } from './types';
export type {
GridData,
DashboardItem,
DashboardCrudTypes,
DashboardAttributes,
SavedDashboardPanel,
} from './latest';
// Today "v1" === "latest" so the export under DashboardV1 namespace is not really useful
// We leave it as a reference for future version when it will be needed to export/support older types
// in the UIs.
export * as DashboardV1 from './v1';

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
// Latest version is 1
export * from './v1';

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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export type DashboardContentType = 'dashboard';

View file

@ -0,0 +1,126 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { schema } from '@kbn/config-schema';
import type { ContentManagementServicesDefinition as ServicesDefinition } from '@kbn/object-versioning';
import {
savedObjectSchema,
objectTypeToGetResultSchema,
createOptionsSchemas,
createResultSchema,
} from '@kbn/content-management-utils';
const dashboardAttributesSchema = schema.object(
{
// General
title: schema.string(),
description: schema.string({ defaultValue: '' }),
// Search
kibanaSavedObjectMeta: schema.object({
searchSourceJSON: schema.maybe(schema.string()),
}),
// Time
timeRestore: schema.maybe(schema.boolean()),
timeFrom: schema.maybe(schema.string()),
timeTo: schema.maybe(schema.string()),
refreshInterval: schema.maybe(
schema.object({
pause: schema.boolean(),
value: schema.number(),
display: schema.maybe(schema.string()),
section: schema.maybe(schema.number()),
})
),
// Dashboard Content
controlGroupInput: schema.maybe(
schema.object({
panelsJSON: schema.maybe(schema.string()),
controlStyle: schema.maybe(schema.string()),
chainingSystem: schema.maybe(schema.string()),
ignoreParentSettingsJSON: schema.maybe(schema.string()),
})
),
panelsJSON: schema.string({ defaultValue: '[]' }),
optionsJSON: schema.string({ defaultValue: '{}' }),
// Legacy
hits: schema.maybe(schema.number()),
version: schema.maybe(schema.number()),
},
{ unknowns: 'forbid' }
);
const dashboardSavedObjectSchema = savedObjectSchema(dashboardAttributesSchema);
const searchOptionsSchema = schema.maybe(
schema.object(
{
onlyTitle: schema.maybe(schema.boolean()),
},
{ unknowns: 'forbid' }
)
);
const createOptionsSchema = schema.object({
id: schema.maybe(createOptionsSchemas.id),
overwrite: schema.maybe(createOptionsSchemas.overwrite),
references: schema.maybe(createOptionsSchemas.references),
});
// Content management service definition.
// We need it for BWC support between different versions of the content
export const serviceDefinition: ServicesDefinition = {
get: {
out: {
result: {
schema: objectTypeToGetResultSchema(dashboardSavedObjectSchema),
},
},
},
create: {
in: {
options: {
schema: createOptionsSchema,
},
data: {
schema: dashboardAttributesSchema,
},
},
out: {
result: {
schema: createResultSchema(dashboardSavedObjectSchema),
},
},
},
update: {
in: {
options: {
schema: createOptionsSchema, // same schema as "create"
},
data: {
schema: dashboardAttributesSchema,
},
},
},
search: {
in: {
options: {
schema: searchOptionsSchema,
},
},
},
mSearch: {
out: {
result: {
schema: dashboardSavedObjectSchema,
},
},
},
};

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { DashboardCrudTypes } from './types';
export type {
GridData,
DashboardCrudTypes,
DashboardAttributes,
SavedDashboardPanel,
} from './types';
export type DashboardItem = DashboardCrudTypes['Item'];

View file

@ -6,38 +6,39 @@
* Side Public License, v 1.
*/
import type {
ContentManagementCrudTypes,
SavedObjectCreateOptions,
SavedObjectUpdateOptions,
} from '@kbn/content-management-utils';
import { Serializable } from '@kbn/utility-types';
import { RefreshInterval } from '@kbn/data-plugin/common';
import { RawControlGroupAttributes } from '@kbn/controls-plugin/common';
import { Serializable } from '@kbn/utility-types';
import { DashboardOptions, GridData } from '../types';
import { DashboardContentType } from '../types';
export type DashboardCrudTypes = ContentManagementCrudTypes<
DashboardContentType,
DashboardAttributes,
Pick<SavedObjectCreateOptions, 'id' | 'references' | 'overwrite'>,
Pick<SavedObjectUpdateOptions, 'references'>,
{
/** Flag to indicate to only search the text on the "title" field */
onlyTitle?: boolean;
}
>;
/**
* The attributes of the dashboard saved object. This interface should be the
* source of truth for the latest dashboard attributes shape after all migrations.
* Grid type for React Grid Layout
*/
export interface DashboardAttributes {
controlGroupInput?: RawControlGroupAttributes;
refreshInterval?: RefreshInterval;
timeRestore: boolean;
optionsJSON?: string;
useMargins?: boolean;
description: string;
panelsJSON: string;
timeFrom?: string;
version: number;
timeTo?: string;
title: string;
kibanaSavedObjectMeta: {
searchSourceJSON: string;
};
export interface GridData {
w: number;
h: number;
x: number;
y: number;
i: string;
}
export type ParsedDashboardAttributes = Omit<DashboardAttributes, 'panelsJSON' | 'optionsJSON'> & {
panels: SavedDashboardPanel[];
options: DashboardOptions;
};
/**
* A saved dashboard panel parsed directly from the Dashboard Attributes panels JSON
*/
@ -51,3 +52,21 @@ export interface SavedDashboardPanel {
version: string;
title?: string;
}
/* eslint-disable-next-line @typescript-eslint/consistent-type-definitions */
export type DashboardAttributes = {
controlGroupInput?: RawControlGroupAttributes;
refreshInterval?: RefreshInterval;
timeRestore: boolean;
optionsJSON?: string;
useMargins?: boolean;
description: string;
panelsJSON: string;
timeFrom?: string;
version: number;
timeTo?: string;
title: string;
kibanaSavedObjectMeta: {
searchSourceJSON: string;
};
};

View file

@ -8,11 +8,12 @@
import { createExtract, createInject } from './dashboard_container_references';
import { createEmbeddablePersistableStateServiceMock } from '@kbn/embeddable-plugin/common/mocks';
import { DashboardContainerStateWithType } from '../../types';
import { ParsedDashboardAttributesWithType } from '../../types';
import { SavedObjectEmbeddableInput } from '@kbn/embeddable-plugin/public';
const persistableStateService = createEmbeddablePersistableStateServiceMock();
const dashboardWithExtractedPanel: DashboardContainerStateWithType = {
const dashboardWithExtractedPanel: ParsedDashboardAttributesWithType = {
id: 'id',
type: 'dashboard',
panels: {
@ -33,7 +34,7 @@ const extractedSavedObjectPanelRef = {
id: 'object-id',
};
const unextractedDashboardState: DashboardContainerStateWithType = {
const unextractedDashboardState: ParsedDashboardAttributesWithType = {
id: 'id',
type: 'dashboard',
panels: {
@ -56,7 +57,7 @@ describe('inject/extract by reference panel', () => {
const injected = inject(
dashboardWithExtractedPanel,
references
) as DashboardContainerStateWithType;
) as ParsedDashboardAttributesWithType;
expect(injected).toEqual(unextractedDashboardState);
});
@ -71,7 +72,7 @@ describe('inject/extract by reference panel', () => {
});
});
const dashboardWithExtractedByValuePanel: DashboardContainerStateWithType = {
const dashboardWithExtractedByValuePanel: ParsedDashboardAttributesWithType = {
id: 'id',
type: 'dashboard',
panels: {
@ -81,7 +82,7 @@ const dashboardWithExtractedByValuePanel: DashboardContainerStateWithType = {
explicitInput: {
id: 'panel_1',
extracted_reference: 'ref',
},
} as Partial<SavedObjectEmbeddableInput> & { id: string; extracted_reference: string },
},
},
};
@ -92,7 +93,7 @@ const extractedByValueRef = {
type: 'panel_type',
};
const unextractedDashboardByValueState: DashboardContainerStateWithType = {
const unextractedDashboardByValueState: ParsedDashboardAttributesWithType = {
id: 'id',
type: 'dashboard',
panels: {
@ -102,7 +103,7 @@ const unextractedDashboardByValueState: DashboardContainerStateWithType = {
explicitInput: {
id: 'panel_1',
value: 'id',
},
} as Partial<SavedObjectEmbeddableInput> & { id: string; value: string },
},
},
};

View file

@ -8,13 +8,14 @@
import {
EmbeddableInput,
EmbeddablePersistableStateService,
EmbeddableStateWithType,
EmbeddablePersistableStateService,
} from '@kbn/embeddable-plugin/common';
import { SavedObjectReference } from '@kbn/core/types';
import { Reference } from '@kbn/content-management-utils';
import { CONTROL_GROUP_TYPE, PersistableControlGroupInput } from '@kbn/controls-plugin/common';
import { DashboardPanelState } from '../types';
import { DashboardContainerStateWithType } from '../../types';
import { ParsedDashboardAttributesWithType } from '../../types';
const getPanelStatePrefix = (state: DashboardPanelState) => `${state.explicitInput.id}:`;
@ -24,8 +25,10 @@ const controlGroupId = 'dashboard_control_group';
export const createInject = (
persistableStateService: EmbeddablePersistableStateService
): EmbeddablePersistableStateService['inject'] => {
return (state: EmbeddableStateWithType, references: SavedObjectReference[]) => {
const workingState = { ...state } as EmbeddableStateWithType | DashboardContainerStateWithType;
return (state: EmbeddableStateWithType, references: Reference[]) => {
const workingState = { ...state } as
| EmbeddableStateWithType
| ParsedDashboardAttributesWithType;
if ('panels' in workingState) {
workingState.panels = { ...workingState.panels };
@ -103,9 +106,11 @@ export const createExtract = (
persistableStateService: EmbeddablePersistableStateService
): EmbeddablePersistableStateService['extract'] => {
return (state: EmbeddableStateWithType) => {
const workingState = { ...state } as EmbeddableStateWithType | DashboardContainerStateWithType;
const workingState = { ...state } as
| EmbeddableStateWithType
| ParsedDashboardAttributesWithType;
const references: SavedObjectReference[] = [];
const references: Reference[] = [];
if ('panels' in workingState) {
workingState.panels = { ...workingState.panels };
@ -125,7 +130,6 @@ export const createExtract = (
});
delete panel.explicitInput.savedObjectId;
delete panel.explicitInput.type;
}
const { state: panelState, references: panelReferences } = persistableStateService.extract({

View file

@ -17,7 +17,8 @@ import { RefreshInterval } from '@kbn/data-plugin/common';
import { PersistableControlGroupInput } from '@kbn/controls-plugin/common';
import { KibanaExecutionContext } from '@kbn/core-execution-context-common';
import { DashboardOptions, GridData } from '../types';
import { DashboardOptions } from '../types';
import { GridData } from '../content_management';
export interface DashboardPanelMap {
[key: string]: DashboardPanelState;

View file

@ -9,8 +9,7 @@
import {
extractReferences,
injectReferences,
InjectDeps,
ExtractDeps,
InjectExtractDeps,
} from './dashboard_saved_object_references';
import {
@ -18,6 +17,7 @@ import {
createInject,
} from '../../dashboard_container/persistable_state/dashboard_container_references';
import { createEmbeddablePersistableStateServiceMock } from '@kbn/embeddable-plugin/common/mocks';
import { DashboardAttributes } from '../../content_management';
const embeddablePersistableStateServiceMock = createEmbeddablePersistableStateServiceMock();
const dashboardInject = createInject(embeddablePersistableStateServiceMock);
@ -38,15 +38,25 @@ embeddablePersistableStateServiceMock.inject.mockImplementation((state, referenc
return state;
});
const deps: InjectDeps & ExtractDeps = {
const deps: InjectExtractDeps = {
embeddablePersistableStateService: embeddablePersistableStateServiceMock,
};
const commonAttributes: DashboardAttributes = {
kibanaSavedObjectMeta: { searchSourceJSON: '' },
timeRestore: false,
panelsJSON: '',
version: 1,
description: '',
title: '',
};
describe('legacy extract references', () => {
test('extracts references from panelsJSON', () => {
const doc = {
id: '1',
attributes: {
...commonAttributes,
foo: true,
panelsJSON: JSON.stringify([
{
@ -70,8 +80,15 @@ describe('legacy extract references', () => {
expect(updatedDoc).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"description": "",
"foo": true,
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "",
},
"panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"version\\":\\"7.0.0\\",\\"panelRefName\\":\\"panel_0\\"},{\\"title\\":\\"Title 2\\",\\"version\\":\\"7.0.0\\",\\"panelRefName\\":\\"panel_1\\"}]",
"timeRestore": false,
"title": "",
"version": 1,
},
"references": Array [
Object {
@ -93,6 +110,7 @@ describe('legacy extract references', () => {
const doc = {
id: '1',
attributes: {
...commonAttributes,
foo: true,
panelsJSON: JSON.stringify([
{
@ -113,6 +131,7 @@ describe('legacy extract references', () => {
const doc = {
id: '1',
attributes: {
...commonAttributes,
foo: true,
panelsJSON: JSON.stringify([
{
@ -127,8 +146,15 @@ describe('legacy extract references', () => {
expect(extractReferences(doc, deps)).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"description": "",
"foo": true,
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "",
},
"panelsJSON": "[{\\"version\\":\\"7.9.1\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\"}]",
"timeRestore": false,
"title": "",
"version": 1,
},
"references": Array [],
}
@ -214,6 +240,7 @@ describe('extractReferences', () => {
const doc = {
id: '1',
attributes: {
...commonAttributes,
foo: true,
panelsJSON: JSON.stringify([
{
@ -239,8 +266,15 @@ describe('extractReferences', () => {
expect(updatedDoc).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"description": "",
"foo": true,
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "",
},
"panelsJSON": "[{\\"version\\":\\"7.9.1\\",\\"type\\":\\"visualization\\",\\"panelIndex\\":\\"panel-1\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_panel-1\\"},{\\"version\\":\\"7.9.1\\",\\"type\\":\\"visualization\\",\\"panelIndex\\":\\"panel-2\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_panel-2\\"}]",
"timeRestore": false,
"title": "",
"version": 1,
},
"references": Array [
Object {
@ -262,6 +296,7 @@ describe('extractReferences', () => {
const doc = {
id: '1',
attributes: {
...commonAttributes,
foo: true,
panelsJSON: JSON.stringify([
{
@ -274,7 +309,7 @@ describe('extractReferences', () => {
references: [],
};
expect(() => extractReferences(doc, deps)).toThrowErrorMatchingInlineSnapshot(
`"\\"type\\" attribute is missing from panel \\"0\\""`
`"\\"type\\" attribute is missing from panel \\"undefined\\""`
);
});
@ -282,6 +317,7 @@ describe('extractReferences', () => {
const doc = {
id: '1',
attributes: {
...commonAttributes,
foo: true,
panelsJSON: JSON.stringify([
{
@ -296,8 +332,15 @@ describe('extractReferences', () => {
expect(extractReferences(doc, deps)).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"description": "",
"foo": true,
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "",
},
"panelsJSON": "[{\\"version\\":\\"7.9.1\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\"}]",
"timeRestore": false,
"title": "",
"version": 1,
},
"references": Array [],
}
@ -308,6 +351,7 @@ describe('extractReferences', () => {
describe('injectReferences', () => {
test('returns injected attributes', () => {
const attributes = {
...commonAttributes,
id: '1',
title: 'test',
panelsJSON: JSON.stringify([
@ -339,9 +383,15 @@ describe('injectReferences', () => {
expect(newAttributes).toMatchInlineSnapshot(`
Object {
"description": "",
"id": "1",
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "",
},
"panelsJSON": "[{\\"version\\":\\"7.9.0\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"version\\":\\"7.9.0\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\"}]",
"timeRestore": false,
"title": "test",
"version": 1,
}
`);
});
@ -350,11 +400,12 @@ describe('injectReferences', () => {
const attributes = {
id: '1',
title: 'test',
};
} as unknown as DashboardAttributes;
const newAttributes = injectReferences({ attributes, references: [] }, deps);
expect(newAttributes).toMatchInlineSnapshot(`
Object {
"id": "1",
"panelsJSON": "[]",
"title": "test",
}
`);
@ -362,6 +413,7 @@ describe('injectReferences', () => {
test('skips when panelsJSON is not an array', () => {
const attributes = {
...commonAttributes,
id: '1',
panelsJSON: '{}',
title: 'test',
@ -369,15 +421,22 @@ describe('injectReferences', () => {
const newAttributes = injectReferences({ attributes, references: [] }, deps);
expect(newAttributes).toMatchInlineSnapshot(`
Object {
"description": "",
"id": "1",
"panelsJSON": "{}",
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "",
},
"panelsJSON": "[]",
"timeRestore": false,
"title": "test",
"version": 1,
}
`);
});
test('skips a panel when panelRefName is missing', () => {
const attributes = {
...commonAttributes,
id: '1',
title: 'test',
panelsJSON: JSON.stringify([
@ -400,15 +459,22 @@ describe('injectReferences', () => {
const newAttributes = injectReferences({ attributes, references }, deps);
expect(newAttributes).toMatchInlineSnapshot(`
Object {
"description": "",
"id": "1",
"panelsJSON": "[{\\"version\\":\\"\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"version\\":\\"\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\"}]",
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "",
},
"panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\"}]",
"timeRestore": false,
"title": "test",
"version": 1,
}
`);
});
test(`fails when it can't find the reference in the array`, () => {
const attributes = {
...commonAttributes,
id: '1',
title: 'test',
panelsJSON: JSON.stringify([

View file

@ -7,130 +7,66 @@
*/
import semverGt from 'semver/functions/gt';
import {
RawControlGroupAttributes,
PersistableControlGroupInput,
} from '@kbn/controls-plugin/common';
import { SavedObjectAttributes, SavedObjectReference } from '@kbn/core/types';
import { Reference } from '@kbn/content-management-utils';
import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common/types';
import { rawControlGroupAttributesToControlGroupInput } from '@kbn/controls-plugin/common';
import { SavedDashboardPanel } from '../types';
import {
convertPanelStateToSavedDashboardPanel,
convertSavedDashboardPanelToPanelState,
convertPanelMapToSavedPanels,
convertSavedPanelsToPanelMap,
} from '../../lib/dashboard_panel_converters';
import { DashboardPanelState } from '../../dashboard_container/types';
import { DashboardContainerStateWithType } from '../../types';
import { DashboardAttributesAndReferences, ParsedDashboardAttributesWithType } from '../../types';
import { DashboardAttributes, SavedDashboardPanel } from '../../content_management';
export interface ExtractDeps {
export interface InjectExtractDeps {
embeddablePersistableStateService: EmbeddablePersistableStateService;
}
export interface InjectDeps {
embeddablePersistableStateService: EmbeddablePersistableStateService;
}
interface SavedObjectAttributesAndReferences {
attributes: SavedObjectAttributes;
references: SavedObjectReference[];
}
const isPre730Panel = (panel: Record<string, string>): boolean => {
return 'version' in panel ? semverGt('7.3.0', panel.version) : true;
return 'version' in panel && panel.version ? semverGt('7.3.0', panel.version) : true;
};
function dashboardAttributesToState(attributes: SavedObjectAttributes): {
state: DashboardContainerStateWithType;
panels: SavedDashboardPanel[];
} {
let inputPanels = [] as SavedDashboardPanel[];
function parseDashboardAttributesWithType(
attributes: DashboardAttributes
): ParsedDashboardAttributesWithType {
let parsedPanels = [] as SavedDashboardPanel[];
if (typeof attributes.panelsJSON === 'string') {
inputPanels = JSON.parse(attributes.panelsJSON) as SavedDashboardPanel[];
}
let controlGroupInput: PersistableControlGroupInput | undefined;
if (attributes.controlGroupInput) {
const rawControlGroupInput =
attributes.controlGroupInput as unknown as RawControlGroupAttributes;
if (rawControlGroupInput.panelsJSON && typeof rawControlGroupInput.panelsJSON === 'string') {
const controlGroupPanels = JSON.parse(rawControlGroupInput.panelsJSON);
if (controlGroupPanels && typeof controlGroupPanels === 'object') {
controlGroupInput = {
...rawControlGroupInput,
panels: controlGroupPanels,
};
}
const parsedJSON = JSON.parse(attributes.panelsJSON);
if (Array.isArray(parsedJSON)) {
parsedPanels = parsedJSON as SavedDashboardPanel[];
}
}
return {
panels: inputPanels,
state: {
id: attributes.id as string,
controlGroupInput,
type: 'dashboard',
panels: inputPanels.reduce<Record<string, DashboardPanelState>>((current, panel, index) => {
const panelIndex = panel.panelIndex || `${index}`;
current[panelIndex] = convertSavedDashboardPanelToPanelState(panel);
return current;
}, {}),
},
};
}
function panelStatesToPanels(
panelStates: DashboardContainerStateWithType['panels'],
originalPanels: SavedDashboardPanel[]
): SavedDashboardPanel[] {
return Object.entries(panelStates).map(([id, panelState]) => {
// Find matching original panel to get the version
let originalPanel = originalPanels.find((p) => p.panelIndex === id);
if (!originalPanel) {
// Maybe original panel doesn't have a panel index and it's just straight up based on its index
const numericId = parseInt(id, 10);
originalPanel = isNaN(numericId) ? originalPanel : originalPanels[numericId];
}
return convertPanelStateToSavedDashboardPanel(
panelState,
originalPanel?.version ? originalPanel.version : ''
);
});
controlGroupInput:
attributes.controlGroupInput &&
rawControlGroupAttributesToControlGroupInput(attributes.controlGroupInput),
type: 'dashboard',
panels: convertSavedPanelsToPanelMap(parsedPanels),
} as ParsedDashboardAttributesWithType;
}
export function injectReferences(
{ 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 attributes.panelsJSON !== 'string') {
return attributes;
}
const parsedPanels = JSON.parse(attributes.panelsJSON);
// Same here, prevent failing saved object import if ever panels aren't an array.
if (!Array.isArray(parsedPanels)) {
return attributes;
}
const { panels, state } = dashboardAttributesToState(attributes);
{ attributes, references = [] }: DashboardAttributesAndReferences,
deps: InjectExtractDeps
): DashboardAttributes {
const parsedAttributes = parseDashboardAttributesWithType(attributes);
// inject references back into panels via the Embeddable persistable state service.
const injectedState = deps.embeddablePersistableStateService.inject(
state,
parsedAttributes,
references
) as DashboardContainerStateWithType;
const injectedPanels = panelStatesToPanels(injectedState.panels, panels);
) as ParsedDashboardAttributesWithType;
const injectedPanels = convertPanelMapToSavedPanels(injectedState.panels);
const newAttributes = {
...attributes,
panelsJSON: JSON.stringify(injectedPanels),
} as SavedObjectAttributes;
} as DashboardAttributes;
if (injectedState.controlGroupInput) {
if (attributes.controlGroupInput && injectedState.controlGroupInput) {
newAttributes.controlGroupInput = {
...(attributes.controlGroupInput as SavedObjectAttributes),
...attributes.controlGroupInput,
panelsJSON: JSON.stringify(injectedState.controlGroupInput.panels),
};
}
@ -139,41 +75,39 @@ export function injectReferences(
}
export function extractReferences(
{ attributes, references = [] }: SavedObjectAttributesAndReferences,
deps: ExtractDeps
): SavedObjectAttributesAndReferences {
if (typeof attributes.panelsJSON !== 'string') {
return { attributes, references };
{ attributes, references = [] }: DashboardAttributesAndReferences,
deps: InjectExtractDeps
): DashboardAttributesAndReferences {
const parsedAttributes = parseDashboardAttributesWithType(attributes);
const panels = parsedAttributes.panels;
if ((Object.values(panels) as unknown as Array<Record<string, string>>).some(isPre730Panel)) {
return pre730ExtractReferences({ attributes, references });
}
const { panels, state } = dashboardAttributesToState(attributes);
if (!Array.isArray(panels)) {
return { attributes, references };
const panelMissingType = Object.values(panels).find((panel) => panel.type === undefined);
if (panelMissingType) {
throw new Error(
`"type" attribute is missing from panel "${panelMissingType.explicitInput.id}"`
);
}
if ((panels as unknown as Array<Record<string, string>>).some(isPre730Panel)) {
return pre730ExtractReferences({ attributes, references }, deps);
}
const missingTypeIndex = panels.findIndex((panel) => panel.type === undefined);
if (missingTypeIndex >= 0) {
throw new Error(`"type" attribute is missing from panel "${missingTypeIndex}"`);
}
const { references: extractedReferences, state: rawExtractedState } =
deps.embeddablePersistableStateService.extract(state);
const extractedState = rawExtractedState as DashboardContainerStateWithType;
const extractedPanels = panelStatesToPanels(extractedState.panels, panels);
const { references: extractedReferences, state: extractedState } =
deps.embeddablePersistableStateService.extract(parsedAttributes) as {
references: Reference[];
state: ParsedDashboardAttributesWithType;
};
const extractedPanels = convertPanelMapToSavedPanels(extractedState.panels);
const newAttributes = {
...attributes,
panelsJSON: JSON.stringify(extractedPanels),
} as SavedObjectAttributes;
} as DashboardAttributes;
if (extractedState.controlGroupInput) {
if (attributes.controlGroupInput && extractedState.controlGroupInput) {
newAttributes.controlGroupInput = {
...(attributes.controlGroupInput as SavedObjectAttributes),
...attributes.controlGroupInput,
panelsJSON: JSON.stringify(extractedState.controlGroupInput.panels),
};
}
@ -184,14 +118,14 @@ export function extractReferences(
};
}
function pre730ExtractReferences(
{ attributes, references = [] }: SavedObjectAttributesAndReferences,
deps: ExtractDeps
): SavedObjectAttributesAndReferences {
function pre730ExtractReferences({
attributes,
references = [],
}: DashboardAttributesAndReferences): DashboardAttributesAndReferences {
if (typeof attributes.panelsJSON !== 'string') {
return { attributes, references };
}
const panelReferences: SavedObjectReference[] = [];
const panelReferences: Reference[] = [];
const panels: Array<Record<string, string>> = JSON.parse(String(attributes.panelsJSON));
panels.forEach((panel, i) => {

View file

@ -6,12 +6,7 @@
* Side Public License, v 1.
*/
export type {
GridData,
DashboardOptions,
DashboardCapabilities,
SharedDashboardState,
} from './types';
export type { DashboardOptions, DashboardCapabilities, SharedDashboardState } from './types';
export type {
DashboardPanelMap,
@ -20,11 +15,7 @@ export type {
DashboardContainerByReferenceInput,
} from './dashboard_container/types';
export type {
DashboardAttributes,
ParsedDashboardAttributes,
SavedDashboardPanel,
} from './dashboard_saved_object/types';
export type { DashboardAttributes } from './content_management';
export {
injectReferences,

View file

@ -10,9 +10,9 @@ import {
convertSavedDashboardPanelToPanelState,
convertPanelStateToSavedDashboardPanel,
} from './dashboard_panel_converters';
import { EmbeddableInput } from '@kbn/embeddable-plugin/common/types';
import { SavedDashboardPanel } from '../dashboard_saved_object/types';
import { SavedDashboardPanel } from '../content_management';
import { DashboardPanelState } from '../dashboard_container/types';
import { EmbeddableInput } from '@kbn/embeddable-plugin/common/types';
test('convertSavedDashboardPanelToPanelState', () => {
const savedDashboardPanel: SavedDashboardPanel = {
@ -46,6 +46,8 @@ test('convertSavedDashboardPanelToPanelState', () => {
savedObjectId: 'savedObjectId',
},
type: 'search',
panelRefName: undefined,
version: '7.0.0',
});
});

View file

@ -9,13 +9,15 @@
import { omit } from 'lodash';
import { EmbeddableInput, SavedObjectEmbeddableInput } from '@kbn/embeddable-plugin/common';
import { DashboardPanelMap, DashboardPanelState, SavedDashboardPanel } from '..';
import { DashboardPanelMap, DashboardPanelState } from '..';
import { SavedDashboardPanel } from '../content_management';
export function convertSavedDashboardPanelToPanelState<
TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput
>(savedDashboardPanel: SavedDashboardPanel): DashboardPanelState<TEmbeddableInput> {
return {
type: savedDashboardPanel.type,
version: savedDashboardPanel.version,
gridData: savedDashboardPanel.gridData,
panelRefName: savedDashboardPanel.panelRefName,
explicitInput: {
@ -29,11 +31,11 @@ export function convertSavedDashboardPanelToPanelState<
export function convertPanelStateToSavedDashboardPanel(
panelState: DashboardPanelState,
version: string
version?: string
): SavedDashboardPanel {
const savedObjectId = (panelState.explicitInput as SavedObjectEmbeddableInput).savedObjectId;
return {
version,
version: version ?? (panelState.version as string), // temporary cast. Version will be mandatory at a later date.
type: panelState.type,
gridData: panelState.gridData,
panelIndex: panelState.explicitInput.id,
@ -52,8 +54,11 @@ export const convertSavedPanelsToPanelMap = (panels?: SavedDashboardPanel[]): Da
return panelsMap;
};
export const convertPanelMapToSavedPanels = (panels: DashboardPanelMap, version: string) => {
export const convertPanelMapToSavedPanels = (
panels: DashboardPanelMap,
versionOverride?: string
) => {
return Object.values(panels).map((panel) =>
convertPanelStateToSavedDashboardPanel(panel, version)
convertPanelStateToSavedDashboardPanel(panel, versionOverride)
);
};

View file

@ -6,11 +6,12 @@
* Side Public License, v 1.
*/
import { EmbeddableInput, EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
import { Reference } from '@kbn/content-management-utils';
import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
import { PersistableControlGroupInput } from '@kbn/controls-plugin/common';
import { SavedDashboardPanel } from './dashboard_saved_object/types';
import { DashboardContainerInput, DashboardPanelState } from './dashboard_container/types';
import { DashboardAttributes, SavedDashboardPanel } from './content_management';
import { DashboardContainerInput, DashboardPanelMap } from './dashboard_container/types';
export interface DashboardOptions {
hidePanelTitles: boolean;
@ -36,26 +37,15 @@ export type SharedDashboardState = Partial<
>;
/**
* Grid type for React Grid Layout
* A partially parsed version of the Dashboard Attributes used for inject and extract logic for both the Dashboard Container and the Dashboard Saved Object.
*/
export interface GridData {
w: number;
h: number;
x: number;
y: number;
i: string;
}
/**
* Types below this line are copied here because so many important types are tied up in public. These types should be
* moved from public into common.
*
* TODO replace this type with a type that uses the real Dashboard Input type.
* See https://github.com/elastic/kibana/issues/147488 for more information.
*/
export interface DashboardContainerStateWithType extends EmbeddableStateWithType {
panels: {
[panelId: string]: DashboardPanelState<EmbeddableInput & { [k: string]: unknown }>;
};
export type ParsedDashboardAttributesWithType = EmbeddableStateWithType & {
controlGroupInput?: PersistableControlGroupInput;
panels: DashboardPanelMap;
type: 'dashboard';
};
export interface DashboardAttributesAndReferences {
attributes: DashboardAttributes;
references: Reference[];
}

View file

@ -18,6 +18,7 @@
"savedObjects",
"savedObjectsFinder",
"savedObjectsManagement",
"contentManagement",
"share",
"screenshotMode",
"uiActions",
@ -34,10 +35,6 @@
"usageCollection",
"taskManager"
],
"requiredBundles": [
"kibanaReact",
"kibanaUtils",
"presentationUtil"
]
"requiredBundles": ["kibanaReact", "kibanaUtils", "presentationUtil"]
}
}

View file

@ -9,7 +9,9 @@
import _ from 'lodash';
import { v4 as uuidv4 } from 'uuid';
// TODO Remove this usage of the SavedObjectsStart contract.
import { SavedObjectsStart } from '@kbn/core/public';
import {
ViewMode,
PanelState,
@ -150,6 +152,7 @@ export class ClonePanelAction implements Action<ClonePanelActionContext> {
embeddable: IEmbeddable,
objectIdToClone: string
): Promise<string> {
// TODO: Remove this entire functionality. See https://github.com/elastic/kibana/issues/158632 for more info.
const savedObjectToClone = await this.savedObjects.client.get<SavedObject>(
embeddable.type,
objectIdToClone
@ -183,6 +186,7 @@ export class ClonePanelAction implements Action<ClonePanelActionContext> {
title: newTitle,
hidePanelTitles: panelToClone.explicitInput.hidePanelTitles,
},
version: panelToClone.version,
};
} else {
panelState = {
@ -191,7 +195,10 @@ export class ClonePanelAction implements Action<ClonePanelActionContext> {
...panelToClone.explicitInput,
id: uuidv4(),
},
version: panelToClone.version,
};
// TODO Remove the entire `addCloneToLibrary` section from here.
if (panelToClone.explicitInput.savedObjectId) {
const clonedSavedObjectId = await this.addCloneToLibrary(
embeddable,

View file

@ -13,7 +13,7 @@ import { pluginServices } from '../../services/plugin_services';
import { createDashboardEditUrl } from '../../dashboard_constants';
import { getDashboardURL404String } from '../_dashboard_app_strings';
import { useDashboardMountContext } from './dashboard_mount_context';
import { LoadDashboardFromSavedObjectReturn } from '../../services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object';
import { LoadDashboardReturn } from '../../services/dashboard_content_management/types';
export const useDashboardOutcomeValidation = ({
redirectTo,
@ -37,7 +37,7 @@ export const useDashboardOutcomeValidation = ({
} = pluginServices.getServices();
const validateOutcome = useCallback(
({ dashboardFound, resolveMeta, dashboardId }: LoadDashboardFromSavedObjectReturn) => {
({ dashboardFound, resolveMeta, dashboardId }: LoadDashboardReturn) => {
if (!dashboardFound) {
toasts.addDanger(getDashboardURL404String());
redirectTo({ destination: 'listing' });
@ -45,11 +45,7 @@ export const useDashboardOutcomeValidation = ({
}
if (resolveMeta && dashboardId) {
const {
outcome: loadOutcome,
alias_target_id: alias,
alias_purpose: aliasPurpose,
} = resolveMeta;
const { outcome: loadOutcome, aliasTargetId: alias, aliasPurpose } = resolveMeta;
/**
* Handle saved object resolve alias outcome by redirecting.
*/

View file

@ -88,7 +88,7 @@ test('When given a title that matches multiple dashboards, filter on the title',
props.title = title;
(
pluginServices.getServices().dashboardSavedObject.findDashboards.findByTitle as jest.Mock
pluginServices.getServices().dashboardContentManagement.findDashboards.findByTitle as jest.Mock
).mockResolvedValue(undefined);
let component: ReactWrapper;
@ -110,7 +110,7 @@ test('When given a title that matches one dashboard, redirect to dashboard', asy
const props = makeDefaultProps();
props.title = title;
(
pluginServices.getServices().dashboardSavedObject.findDashboards.findByTitle as jest.Mock
pluginServices.getServices().dashboardContentManagement.findDashboards.findByTitle as jest.Mock
).mockResolvedValue({ id: 'you_found_me' });
let component: ReactWrapper;

View file

@ -38,7 +38,7 @@ export const DashboardListingPage = ({
const {
data: { query },
chrome: { setBreadcrumbs },
dashboardSavedObject: { findDashboards },
dashboardContentManagement: { findDashboards },
} = pluginServices.getServices();
const [showNoDataPage, setShowNoDataPage] = useState<boolean>(false);

View file

@ -15,8 +15,9 @@ import { SerializableControlGroupInput } from '@kbn/controls-plugin/common';
import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
import type { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public';
import type { DashboardContainerInput } from '../../../common';
import { SavedDashboardPanel } from '../../../common/content_management';
import { DASHBOARD_APP_ID, SEARCH_SESSION_ID } from '../../dashboard_constants';
import type { DashboardContainerInput, SavedDashboardPanel } from '../../../common';
/**
* Useful for ensuring that we don't pass any non-serializable values to history.push (for example, functions).

View file

@ -19,7 +19,7 @@ import { topNavStrings } from '../_dashboard_app_strings';
import { ShowShareModal } from './share/show_share_modal';
import { pluginServices } from '../../services/plugin_services';
import { CHANGE_CHECK_DEBOUNCE } from '../../dashboard_constants';
import { SaveDashboardReturn } from '../../services/dashboard_saved_object/types';
import { SaveDashboardReturn } from '../../services/dashboard_content_management/types';
import { confirmDiscardUnsavedChanges } from '../../dashboard_listing/confirm_overlays';
export const useDashboardMenuItems = ({

View file

@ -15,7 +15,6 @@ import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/common';
import {
DashboardPanelMap,
SavedDashboardPanel,
SharedDashboardState,
convertSavedPanelsToPanelMap,
DashboardContainerInput,
@ -24,7 +23,8 @@ import { DashboardAPI } from '../../dashboard_container';
import { pluginServices } from '../../services/plugin_services';
import { getPanelTooOldErrorString } from '../_dashboard_app_strings';
import { DASHBOARD_STATE_STORAGE_KEY } from '../../dashboard_constants';
import { migrateLegacyQuery } from '../../services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object';
import { SavedDashboardPanel } from '../../../common/content_management';
import { migrateLegacyQuery } from '../../services/dashboard_content_management/lib/load_dashboard_state';
/**
* We no longer support loading panels from a version older than 7.3 in the URL.

View file

@ -53,7 +53,6 @@ export const DASHBOARD_UI_METRIC_ID = 'dashboard';
export const DASHBOARD_APP_ID = 'dashboards';
export const LEGACY_DASHBOARD_APP_ID = 'dashboard';
export const SEARCH_SESSION_ID = 'searchSessionId';
export const DASHBOARD_SAVED_OBJECT_TYPE = 'dashboard';
// ------------------------------------------------------------------
// Grid
@ -66,6 +65,8 @@ export const DEFAULT_PANEL_WIDTH = DASHBOARD_GRID_COLUMN_COUNT / 2;
export const CHANGE_CHECK_DEBOUNCE = 100;
export { CONTENT_ID as DASHBOARD_CONTENT_ID } from '../common/content_management/constants';
// ------------------------------------------------------------------
// Default State
// ------------------------------------------------------------------

View file

@ -8,7 +8,8 @@
import _ from 'lodash';
import { PanelNotFoundError } from '@kbn/embeddable-plugin/public';
import { DashboardPanelState, GridData } from '../../../../common';
import { DashboardPanelState } from '../../../../common';
import { GridData } from '../../../../common/content_management';
import { DASHBOARD_GRID_COLUMN_COUNT } from '../../../dashboard_constants';
export type PanelPlacementMethod<PlacementArgs extends IPanelPlacementArgs> = (

View file

@ -39,7 +39,7 @@ const DUPLICATE_TITLE_CALLOUT_ID = 'duplicateTitleCallout';
export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
const {
savedObjectsTagging: { components },
dashboardSavedObject: { checkForDuplicateDashboardTitle },
dashboardContentManagement: { checkForDuplicateDashboardTitle },
} = pluginServices.getServices();
const dashboard = useDashboardContainer();

View file

@ -11,14 +11,14 @@ import { batch } from 'react-redux';
import { showSaveModal } from '@kbn/saved-objects-plugin/public';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import { DASHBOARD_SAVED_OBJECT_TYPE, SAVED_OBJECT_POST_TIME } from '../../../dashboard_constants';
import { DASHBOARD_CONTENT_ID, SAVED_OBJECT_POST_TIME } from '../../../dashboard_constants';
import { DashboardSaveOptions, DashboardStateFromSaveModal } from '../../types';
import { DashboardSaveModal } from './overlays/save_modal';
import { DashboardContainer } from '../dashboard_container';
import { showCloneModal } from './overlays/show_clone_modal';
import { pluginServices } from '../../../services/plugin_services';
import { DashboardContainerInput } from '../../../../common';
import { SaveDashboardReturn } from '../../../services/dashboard_saved_object/types';
import { SaveDashboardReturn } from '../../../services/dashboard_content_management/types';
export function runSaveAs(this: DashboardContainer) {
const {
@ -28,7 +28,7 @@ export function runSaveAs(this: DashboardContainer) {
},
},
savedObjectsTagging: { hasApi: hasSavedObjectsTagging },
dashboardSavedObject: { checkForDuplicateDashboardTitle, saveDashboardStateToSavedObject },
dashboardContentManagement: { checkForDuplicateDashboardTitle, saveDashboardState },
} = pluginServices.getServices();
const {
@ -81,7 +81,7 @@ export function runSaveAs(this: DashboardContainer) {
...stateFromSaveModal,
};
const beforeAddTime = window.performance.now();
const saveResult = await saveDashboardStateToSavedObject({
const saveResult = await saveDashboardState({
currentState: stateToSave,
saveOptions,
lastSavedId,
@ -91,7 +91,7 @@ export function runSaveAs(this: DashboardContainer) {
eventName: SAVED_OBJECT_POST_TIME,
duration: addDuration,
meta: {
saved_object_type: DASHBOARD_SAVED_OBJECT_TYPE,
saved_object_type: DASHBOARD_CONTENT_ID,
},
});
@ -127,7 +127,7 @@ export function runSaveAs(this: DashboardContainer) {
*/
export async function runQuickSave(this: DashboardContainer) {
const {
dashboardSavedObject: { saveDashboardStateToSavedObject },
dashboardContentManagement: { saveDashboardState },
} = pluginServices.getServices();
const {
@ -135,7 +135,7 @@ export async function runQuickSave(this: DashboardContainer) {
componentState: { lastSavedId },
} = this.getState();
const saveResult = await saveDashboardStateToSavedObject({
const saveResult = await saveDashboardState({
lastSavedId,
currentState,
saveOptions: {},
@ -147,7 +147,7 @@ export async function runQuickSave(this: DashboardContainer) {
export async function runClone(this: DashboardContainer) {
const {
dashboardSavedObject: { saveDashboardStateToSavedObject, checkForDuplicateDashboardTitle },
dashboardContentManagement: { saveDashboardState, checkForDuplicateDashboardTitle },
} = pluginServices.getServices();
const { explicitInput: currentState } = this.getState();
@ -170,7 +170,7 @@ export async function runClone(this: DashboardContainer) {
// do not clone if title is duplicate and is unconfirmed
return {};
}
const saveResult = await saveDashboardStateToSavedObject({
const saveResult = await saveDashboardState({
saveOptions: { saveAsCopy: true },
currentState: { ...currentState, title: newTitle },
});

View file

@ -52,7 +52,7 @@ test('throws error when provided validation function returns invalid', async ()
});
test('pulls state from dashboard saved object when given a saved object id', async () => {
pluginServices.getServices().dashboardSavedObject.loadDashboardStateFromSavedObject = jest
pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest
.fn()
.mockResolvedValue({
dashboardInput: {
@ -62,13 +62,13 @@ test('pulls state from dashboard saved object when given a saved object id', asy
});
const dashboard = await createDashboard({}, 0, 'wow-such-id');
expect(
pluginServices.getServices().dashboardSavedObject.loadDashboardStateFromSavedObject
pluginServices.getServices().dashboardContentManagement.loadDashboardState
).toHaveBeenCalledWith({ id: 'wow-such-id' });
expect(dashboard.getState().explicitInput.description).toBe(`wow would you look at that? Wow.`);
});
test('pulls state from session storage which overrides state from saved object', async () => {
pluginServices.getServices().dashboardSavedObject.loadDashboardStateFromSavedObject = jest
pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest
.fn()
.mockResolvedValue({
dashboardInput: {
@ -86,7 +86,7 @@ test('pulls state from session storage which overrides state from saved object',
});
test('pulls state from creation options initial input which overrides all other state sources', async () => {
pluginServices.getServices().dashboardSavedObject.loadDashboardStateFromSavedObject = jest
pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest
.fn()
.mockResolvedValue({
dashboardInput: {

View file

@ -24,10 +24,10 @@ import { pluginServices } from '../../../services/plugin_services';
import { DEFAULT_DASHBOARD_INPUT } from '../../../dashboard_constants';
import { DashboardCreationOptions } from '../dashboard_container_factory';
import { startSyncingDashboardDataViews } from './data_views/sync_dashboard_data_views';
import { LoadDashboardReturn } from '../../../services/dashboard_content_management/types';
import { syncUnifiedSearchState } from './unified_search/sync_dashboard_unified_search_state';
import { startSyncingDashboardControlGroup } from './controls/dashboard_control_group_integration';
import { startDashboardSearchSessionIntegration } from './search_sessions/start_dashboard_search_session_integration';
import { LoadDashboardFromSavedObjectReturn } from '../../../services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object';
/**
* Builds a new Dashboard from scratch.
@ -39,7 +39,7 @@ export const createDashboard = async (
): Promise<DashboardContainer> => {
const {
data: { dataViews },
dashboardSavedObject: { loadDashboardStateFromSavedObject },
dashboardContentManagement: { loadDashboardState },
} = pluginServices.getServices();
// --------------------------------------------------------------------------------------
@ -59,7 +59,7 @@ export const createDashboard = async (
// --------------------------------------------------------------------------------------
const reduxEmbeddablePackagePromise = lazyLoadReduxToolsPackage();
const defaultDataViewAssignmentPromise = dataViews.getDefaultDataView();
const dashboardSavedObjectPromise = loadDashboardStateFromSavedObject({ id: savedObjectId });
const dashboardSavedObjectPromise = loadDashboardState({ id: savedObjectId });
const [reduxEmbeddablePackage, savedObjectResult, defaultDataView] = await Promise.all([
reduxEmbeddablePackagePromise,
@ -106,7 +106,7 @@ export const initializeDashboard = async ({
creationOptions,
controlGroup,
}: {
loadDashboardReturn: LoadDashboardFromSavedObjectReturn;
loadDashboardReturn: LoadDashboardReturn;
untilDashboardReady: () => Promise<DashboardContainer>;
creationOptions?: DashboardCreationOptions;
controlGroup?: ControlGroupContainer;

View file

@ -355,12 +355,12 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
this.stopSyncingWithUnifiedSearch?.();
const {
dashboardSavedObject: { loadDashboardStateFromSavedObject },
dashboardContentManagement: { loadDashboardState },
} = pluginServices.getServices();
if (newCreationOptions) {
this.creationOptions = { ...this.creationOptions, ...newCreationOptions };
}
const loadDashboardReturn = await loadDashboardStateFromSavedObject({ id: newSavedObjectId });
const loadDashboardReturn = await loadDashboardState({ id: newSavedObjectId });
const dashboardContainerReady$ = new Subject<DashboardContainer>();
const untilDashboardReady = () =>

View file

@ -25,7 +25,7 @@ import { DASHBOARD_CONTAINER_TYPE } from '..';
import type { DashboardContainer } from './dashboard_container';
import { DEFAULT_DASHBOARD_INPUT } from '../../dashboard_constants';
import { createInject, createExtract, DashboardContainerInput } from '../../../common';
import { LoadDashboardFromSavedObjectReturn } from '../../services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object';
import { LoadDashboardReturn } from '../../services/dashboard_content_management/types';
export type DashboardContainerFactory = EmbeddableFactory<
DashboardContainerInput,
@ -55,7 +55,7 @@ export interface DashboardCreationOptions {
useUnifiedSearchIntegration?: boolean;
unifiedSearchSettings?: { kbnUrlStateStorage: IKbnUrlStateStorage };
validateLoadedSavedObject?: (result: LoadDashboardFromSavedObjectReturn) => boolean;
validateLoadedSavedObject?: (result: LoadDashboardReturn) => boolean;
}
export class DashboardContainerFactoryDefinition

View file

@ -17,21 +17,21 @@ import {
} from '@kbn/content-management-table-list';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import type { SavedObjectsFindOptionsReference } from '@kbn/core/public';
import { toMountPoint, useExecutionContext } from '@kbn/kibana-react-plugin/public';
import type { SavedObjectsFindOptionsReference, SimpleSavedObject } from '@kbn/core/public';
import {
DASHBOARD_CONTENT_ID,
SAVED_OBJECT_DELETE_TIME,
SAVED_OBJECT_LOADED_TIME,
DASHBOARD_SAVED_OBJECT_TYPE,
} from '../dashboard_constants';
import {
dashboardListingTableStrings,
dashboardListingErrorStrings,
} from './_dashboard_listing_strings';
import { DashboardAttributes } from '../../common';
import { pluginServices } from '../services/plugin_services';
import { confirmCreateWithUnsaved } from './confirm_overlays';
import { DashboardItem } from '../../common/content_management';
import { DashboardUnsavedListing } from './dashboard_unsaved_listing';
import { DashboardApplicationService } from '../services/application/types';
import { DashboardListingEmptyPrompt } from './dashboard_listing_empty_prompt';
@ -53,15 +53,13 @@ interface DashboardSavedObjectUserContent extends UserContentCommonSchema {
};
}
const toTableListViewSavedObject = (
savedObject: SimpleSavedObject<DashboardAttributes>
): DashboardSavedObjectUserContent => {
const { title, description, timeRestore } = savedObject.attributes;
const toTableListViewSavedObject = (hit: DashboardItem): DashboardSavedObjectUserContent => {
const { title, description, timeRestore } = hit.attributes;
return {
type: 'dashboard',
id: savedObject.id,
updatedAt: savedObject.updatedAt!,
references: savedObject.references,
id: hit.id,
updatedAt: hit.updatedAt!,
references: hit.references,
attributes: {
title,
description,
@ -95,7 +93,7 @@ export const DashboardListing = ({
notifications: { toasts },
coreContext: { executionContext },
dashboardCapabilities: { showWriteControls },
dashboardSavedObject: { findDashboards, savedObjectsClient },
dashboardContentManagement: { findDashboards, deleteDashboards },
} = pluginServices.getServices();
const [unsavedDashboardIds, setUnsavedDashboardIds] = useState<string[]>(
@ -133,8 +131,9 @@ export const DashboardListing = ({
} = {}
) => {
const searchStartTime = window.performance.now();
return findDashboards
.findSavedObjects({
.search({
search: searchTerm,
size: listingLimit,
hasReference: references,
@ -147,7 +146,7 @@ export const DashboardListing = ({
eventName: SAVED_OBJECT_LOADED_TIME,
duration: searchDuration,
meta: {
saved_object_type: DASHBOARD_SAVED_OBJECT_TYPE,
saved_object_type: DASHBOARD_CONTENT_ID,
},
});
return {
@ -164,10 +163,10 @@ export const DashboardListing = ({
try {
const deleteStartTime = window.performance.now();
await Promise.all(
await deleteDashboards(
dashboardsToDelete.map(({ id }) => {
dashboardSessionStorage.clearState(id);
return savedObjectsClient.delete(DASHBOARD_SAVED_OBJECT_TYPE, id);
return id;
})
);
@ -176,7 +175,7 @@ export const DashboardListing = ({
eventName: SAVED_OBJECT_DELETE_TIME,
duration: deleteDuration,
meta: {
saved_object_type: DASHBOARD_SAVED_OBJECT_TYPE,
saved_object_type: DASHBOARD_CONTENT_ID,
total: dashboardsToDelete.length,
},
});
@ -188,7 +187,7 @@ export const DashboardListing = ({
setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges());
},
[savedObjectsClient, dashboardSessionStorage, toasts]
[dashboardSessionStorage, deleteDashboards, toasts]
);
const editItem = useCallback(

View file

@ -39,7 +39,7 @@ describe('Unsaved listing', () => {
mountWith({});
await waitFor(() => {
expect(
pluginServices.getServices().dashboardSavedObject.findDashboards.findByIds
pluginServices.getServices().dashboardContentManagement.findDashboards.findByIds
).toHaveBeenCalledTimes(1);
});
});
@ -50,7 +50,7 @@ describe('Unsaved listing', () => {
mountWith({ props });
await waitFor(() => {
expect(
pluginServices.getServices().dashboardSavedObject.findDashboards.findByIds
pluginServices.getServices().dashboardContentManagement.findDashboards.findByIds
).toHaveBeenCalledWith(['dashboardUnsavedOne']);
});
});
@ -99,7 +99,7 @@ describe('Unsaved listing', () => {
it('removes unsaved changes from any dashboard which errors on fetch', async () => {
(
pluginServices.getServices().dashboardSavedObject.findDashboards.findByIds as jest.Mock
pluginServices.getServices().dashboardContentManagement.findDashboards.findByIds as jest.Mock
).mockResolvedValue([
{
id: 'failCase1',

View file

@ -19,9 +19,9 @@ import React, { useCallback, useEffect, useState } from 'react';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { DashboardAttributes } from '../../common';
import { pluginServices } from '../services/plugin_services';
import { confirmDiscardUnsavedChanges } from './confirm_overlays';
import { DashboardAttributes } from '../../common/content_management';
import { dashboardUnsavedListingStrings, getNewDashboardTitle } from './_dashboard_listing_strings';
import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_session_storage/dashboard_session_storage_service';
@ -117,7 +117,7 @@ export const DashboardUnsavedListing = ({
}: DashboardUnsavedListingProps) => {
const {
dashboardSessionStorage,
dashboardSavedObject: { savedObjectsClient, findDashboards },
dashboardContentManagement: { findDashboards },
} = pluginServices.getServices();
const [items, setItems] = useState<UnsavedItemMap>({});
@ -173,13 +173,7 @@ export const DashboardUnsavedListing = ({
return () => {
canceled = true;
};
}, [
refreshUnsavedDashboards,
dashboardSessionStorage,
unsavedDashboardIds,
savedObjectsClient,
findDashboards,
]);
}, [refreshUnsavedDashboards, dashboardSessionStorage, unsavedDashboardIds, findDashboards]);
return unsavedDashboardIds.length === 0 ? null : (
<>

View file

@ -20,7 +20,6 @@ import {
AppMountParameters,
DEFAULT_APP_CATEGORIES,
PluginInitializerContext,
SavedObjectsClientContract,
} from '@kbn/core/public';
import type {
ScreenshotModePluginSetup,
@ -45,6 +44,10 @@ import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/publ
import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type {
ContentManagementPublicSetup,
ContentManagementPublicStart,
} from '@kbn/content-management-plugin/public';
import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { UrlForwardingSetup, UrlForwardingStart } from '@kbn/url-forwarding-plugin/public';
import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public';
@ -64,7 +67,8 @@ import {
} from './dashboard_constants';
import { DashboardMountContextProps } from './dashboard_app/types';
import { PlaceholderEmbeddableFactory } from './placeholder_embeddable';
import type { FindDashboardsService } from './services/dashboard_saved_object/types';
import type { FindDashboardsService } from './services/dashboard_content_management/types';
import { CONTENT_ID, LATEST_VERSION } from '../common/content_management';
export interface DashboardFeatureFlagConfig {
allowByValueEmbeddables: boolean;
@ -74,6 +78,7 @@ export interface DashboardSetupDependencies {
data: DataPublicPluginSetup;
embeddable: EmbeddableSetup;
home?: HomePublicPluginSetup;
contentManagement: ContentManagementPublicSetup;
screenshotMode: ScreenshotModePluginSetup;
share?: SharePluginSetup;
usageCollection?: UsageCollectionSetup;
@ -90,7 +95,7 @@ export interface DashboardStartDependencies {
navigation: NavigationPublicPluginStart;
presentationUtil: PresentationUtilPluginStart;
savedObjects: SavedObjectsStart;
savedObjectsClient: SavedObjectsClientContract;
contentManagement: ContentManagementPublicStart;
savedObjectsManagement: SavedObjectsManagementPluginStart;
savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart;
screenshotMode: ScreenshotModePluginStart;
@ -141,7 +146,7 @@ export class DashboardPlugin
public setup(
core: CoreSetup<DashboardStartDependencies, DashboardStart>,
{ share, embeddable, home, urlForwarding, data }: DashboardSetupDependencies
{ share, embeddable, home, urlForwarding, data, contentManagement }: DashboardSetupDependencies
): DashboardSetup {
this.dashboardFeatureFlagConfig =
this.initializerContext.config.get<DashboardFeatureFlagConfig>();
@ -153,12 +158,9 @@ export class DashboardPlugin
getDashboardFilterFields: async (dashboardId: string) => {
const { pluginServices } = await import('./services/plugin_services');
const {
dashboardSavedObject: { loadDashboardStateFromSavedObject },
dashboardContentManagement: { loadDashboardState },
} = pluginServices.getServices();
return (
(await loadDashboardStateFromSavedObject({ id: dashboardId })).dashboardInput
?.filters ?? []
);
return (await loadDashboardState({ id: dashboardId })).dashboardInput?.filters ?? [];
},
})
);
@ -274,13 +276,14 @@ export class DashboardPlugin
// persisted dashboard, probably with url state
return `#/view/${id}${tail || ''}`;
});
const dashboardAppTitle = i18n.translate('dashboard.featureCatalogue.dashboardTitle', {
defaultMessage: 'Dashboard',
});
if (home) {
home.featureCatalogue.register({
id: LEGACY_DASHBOARD_APP_ID,
title: i18n.translate('dashboard.featureCatalogue.dashboardTitle', {
defaultMessage: 'Dashboard',
}),
title: dashboardAppTitle,
subtitle: i18n.translate('dashboard.featureCatalogue.dashboardSubtitle', {
defaultMessage: 'Analyze data in dashboards.',
}),
@ -296,6 +299,15 @@ export class DashboardPlugin
});
}
// register content management
contentManagement.registry.register({
id: CONTENT_ID,
version: {
latest: LATEST_VERSION,
},
name: dashboardAppTitle,
});
return {
locator: this.locator,
};
@ -317,7 +329,7 @@ export class DashboardPlugin
findDashboardsService: async () => {
const { pluginServices } = await import('./services/plugin_services');
const {
dashboardSavedObject: { findDashboards },
dashboardContentManagement: { findDashboards },
} = pluginServices.getServices();
return findDashboards;
},

View file

@ -0,0 +1,77 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import { DashboardContentManagementService, LoadDashboardReturn } from './types';
import { DashboardAttributes } from '../../../common/content_management';
import { SearchDashboardsResponse } from './lib/find_dashboards';
export type DashboardContentManagementServiceFactory =
PluginServiceFactory<DashboardContentManagementService>;
export const dashboardContentManagementServiceFactory: DashboardContentManagementServiceFactory =
() => {
return {
loadDashboardState: jest.fn().mockImplementation(() =>
Promise.resolve({
dashboardInput: {},
} as LoadDashboardReturn)
),
saveDashboardState: jest.fn(),
findDashboards: {
search: jest.fn().mockImplementation(({ search, size }) => {
const sizeToUse = size ?? 10;
const hits: SearchDashboardsResponse['hits'] = [];
for (let i = 0; i < sizeToUse; i++) {
hits.push({
type: 'dashboard',
id: `dashboard${i}`,
attributes: {
description: `dashboard${i} desc`,
title: `dashboard${i} - ${search} - title`,
},
references: [] as SearchDashboardsResponse['hits'][0]['references'],
} as SearchDashboardsResponse['hits'][0]);
}
return Promise.resolve({
total: sizeToUse,
hits,
});
}),
findByIds: jest.fn().mockImplementation(() =>
Promise.resolve([
{
id: `dashboardUnsavedOne`,
status: 'success',
attributes: {
title: `Dashboard Unsaved One`,
} as unknown as DashboardAttributes,
},
{
id: `dashboardUnsavedTwo`,
status: 'success',
attributes: {
title: `Dashboard Unsaved Two`,
} as unknown as DashboardAttributes,
},
{
id: `dashboardUnsavedThree`,
status: 'success',
attributes: {
title: `Dashboard Unsaved Three`,
} as unknown as DashboardAttributes,
},
])
),
findByTitle: jest.fn(),
},
deleteDashboards: jest.fn(),
checkForDuplicateDashboardTitle: jest.fn(),
};
};

View file

@ -0,0 +1,83 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import type { DashboardStartDependencies } from '../../plugin';
import { checkForDuplicateDashboardTitle } from './lib/check_for_duplicate_dashboard_title';
import {
searchDashboards,
findDashboardsByIds,
findDashboardIdByTitle,
} from './lib/find_dashboards';
import { saveDashboardState } from './lib/save_dashboard_state';
import type {
DashboardContentManagementRequiredServices,
DashboardContentManagementService,
} from './types';
import { loadDashboardState } from './lib/load_dashboard_state';
import { deleteDashboards } from './lib/delete_dashboards';
export type DashboardContentManagementServiceFactory = KibanaPluginServiceFactory<
DashboardContentManagementService,
DashboardStartDependencies,
DashboardContentManagementRequiredServices
>;
export const dashboardContentManagementServiceFactory: DashboardContentManagementServiceFactory = (
{ startPlugins: { contentManagement } },
requiredServices
) => {
const {
data,
embeddable,
notifications,
initializerContext,
savedObjectsTagging,
dashboardSessionStorage,
} = requiredServices;
return {
loadDashboardState: ({ id }) =>
loadDashboardState({
id,
data,
embeddable,
contentManagement,
savedObjectsTagging,
}),
saveDashboardState: ({ currentState, saveOptions, lastSavedId }) =>
saveDashboardState({
data,
embeddable,
saveOptions,
lastSavedId,
currentState,
notifications,
contentManagement,
initializerContext,
savedObjectsTagging,
dashboardSessionStorage,
}),
findDashboards: {
search: ({ hasReference, hasNoReference, search, size }) =>
searchDashboards({
contentManagement,
hasNoReference,
hasReference,
search,
size,
}),
findByIds: (ids) => findDashboardsByIds(contentManagement, ids),
findByTitle: (title) => findDashboardIdByTitle(contentManagement, title),
},
checkForDuplicateDashboardTitle: (props) =>
checkForDuplicateDashboardTitle(props, contentManagement),
deleteDashboards: (ids) => deleteDashboards(ids, contentManagement),
};
};

View file

@ -6,10 +6,9 @@
* Side Public License, v 1.
*/
import type { SavedObjectsClientContract } from '@kbn/core/public';
import { DashboardAttributes } from '../../../../common';
import { DASHBOARD_SAVED_OBJECT_TYPE } from '../../../dashboard_constants';
import { DashboardStartDependencies } from '../../../plugin';
import { DASHBOARD_CONTENT_ID } from '../../../dashboard_constants';
import { DashboardCrudTypes } from '../../../../common/content_management';
export interface DashboardDuplicateTitleCheckProps {
title: string;
@ -32,7 +31,7 @@ export async function checkForDuplicateDashboardTitle(
onTitleDuplicate,
isTitleDuplicateConfirmed,
}: DashboardDuplicateTitleCheckProps,
savedObjectsClient: SavedObjectsClientContract
contentManagement: DashboardStartDependencies['contentManagement']
): Promise<boolean> {
// Don't check for duplicates if user has already confirmed save with duplicate title
if (isTitleDuplicateConfirmed) {
@ -44,16 +43,19 @@ export async function checkForDuplicateDashboardTitle(
if (title === lastSavedTitle && !copyOnSave) {
return true;
}
const response = await savedObjectsClient.find<DashboardAttributes>({
perPage: 10,
fields: ['title'],
search: `"${title}"`,
searchFields: ['title'],
type: DASHBOARD_SAVED_OBJECT_TYPE,
const { hits } = await contentManagement.client.search<
DashboardCrudTypes['SearchIn'],
DashboardCrudTypes['SearchOut']
>({
contentTypeId: DASHBOARD_CONTENT_ID,
query: {
text: title ? `${title}*` : undefined,
limit: 10,
},
options: { onlyTitle: true },
});
const duplicate = response.savedObjects.find(
(obj) => obj.get('title').toLowerCase() === title.toLowerCase()
);
const duplicate = hits.find((hit) => hit.attributes.title.toLowerCase() === title.toLowerCase());
if (!duplicate) {
return true;
}

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { DashboardStartDependencies } from '../../../plugin';
import { DASHBOARD_CONTENT_ID } from '../../../dashboard_constants';
import { DashboardCrudTypes } from '../../../../common/content_management';
export const deleteDashboards = async (
ids: string[],
contentManagement: DashboardStartDependencies['contentManagement']
) => {
const deletePromises = ids.map((id) =>
contentManagement.client.delete<
DashboardCrudTypes['DeleteIn'],
DashboardCrudTypes['DeleteOut']
>({
contentTypeId: DASHBOARD_CONTENT_ID,
id,
})
);
await Promise.all(deletePromises);
};

View file

@ -0,0 +1,108 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SavedObjectError, SavedObjectsFindOptionsReference } from '@kbn/core/public';
import {
DashboardItem,
DashboardCrudTypes,
DashboardAttributes,
} from '../../../../common/content_management';
import { DashboardStartDependencies } from '../../../plugin';
import { DASHBOARD_CONTENT_ID } from '../../../dashboard_constants';
export interface SearchDashboardsArgs {
contentManagement: DashboardStartDependencies['contentManagement'];
hasNoReference?: SavedObjectsFindOptionsReference[];
hasReference?: SavedObjectsFindOptionsReference[];
search: string;
size: number;
}
export interface SearchDashboardsResponse {
total: number;
hits: DashboardItem[];
}
export async function searchDashboards({
contentManagement,
hasNoReference,
hasReference,
search,
size,
}: SearchDashboardsArgs): Promise<SearchDashboardsResponse> {
const {
hits,
pagination: { total },
} = await contentManagement.client.search<
DashboardCrudTypes['SearchIn'],
DashboardCrudTypes['SearchOut']
>({
contentTypeId: DASHBOARD_CONTENT_ID,
query: {
text: search ? `${search}*` : undefined,
limit: size,
tags: {
included: (hasReference ?? []).map(({ id }) => id),
excluded: (hasNoReference ?? []).map(({ id }) => id),
},
},
});
return {
total,
hits,
};
}
export type FindDashboardsByIdResponse = { id: string } & (
| { status: 'success'; attributes: DashboardAttributes }
| { status: 'error'; error: SavedObjectError }
);
export async function findDashboardsByIds(
contentManagement: DashboardStartDependencies['contentManagement'],
ids: string[]
): Promise<FindDashboardsByIdResponse[]> {
const findPromises = ids.map((id) =>
contentManagement.client.get<DashboardCrudTypes['GetIn'], DashboardCrudTypes['GetOut']>({
contentTypeId: DASHBOARD_CONTENT_ID,
id,
})
);
const results = await Promise.all(findPromises);
return results.map((result) => {
if (result.item.error) return { status: 'error', error: result.item.error, id: result.item.id };
const { attributes, id } = result.item;
return { id, status: 'success', attributes };
});
}
export async function findDashboardIdByTitle(
contentManagement: DashboardStartDependencies['contentManagement'],
title: string
): Promise<{ id: string } | undefined> {
const { hits } = await contentManagement.client.search<
DashboardCrudTypes['SearchIn'],
DashboardCrudTypes['SearchOut']
>({
contentTypeId: DASHBOARD_CONTENT_ID,
query: {
text: title ? `${title}*` : undefined,
limit: 10,
},
options: { onlyTitle: true },
});
// The search isn't an exact match, lets see if we can find a single exact match to use
const matchingDashboards = hits.filter(
(hit) => hit.attributes.title.toLowerCase() === title.toLowerCase()
);
if (matchingDashboards.length === 1) {
return { id: matchingDashboards[0].id };
}
}

View file

@ -8,11 +8,6 @@
import { v4 as uuidv4 } from 'uuid';
import { has } from 'lodash';
import {
ResolvedSimpleSavedObject,
SavedObjectAttributes,
SavedObjectsClientContract,
} from '@kbn/core/public';
import { Filter, Query } from '@kbn/es-query';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { cleanFiltersForSerialize } from '@kbn/presentation-util-plugin/public';
@ -20,14 +15,13 @@ import { rawControlGroupAttributesToControlGroupInput } from '@kbn/controls-plug
import { parseSearchSourceJSON, injectSearchSourceReferences } from '@kbn/data-plugin/public';
import {
convertSavedPanelsToPanelMap,
DashboardContainerInput,
DashboardAttributes,
DashboardOptions,
injectReferences,
type DashboardOptions,
convertSavedPanelsToPanelMap,
} from '../../../../common';
import { DashboardSavedObjectRequiredServices } from '../types';
import { DASHBOARD_SAVED_OBJECT_TYPE, DEFAULT_DASHBOARD_INPUT } from '../../../dashboard_constants';
import { DashboardCrudTypes } from '../../../../common/content_management';
import type { LoadDashboardFromSavedObjectProps, LoadDashboardReturn } from '../types';
import { DASHBOARD_CONTENT_ID, DEFAULT_DASHBOARD_INPUT } from '../../../dashboard_constants';
export function migrateLegacyQuery(query: Query | { [key: string]: any } | string): Query {
// Lucene was the only option before, so language-less queries are all lucene
@ -38,25 +32,13 @@ export function migrateLegacyQuery(query: Query | { [key: string]: any } | strin
return query as Query;
}
export type LoadDashboardFromSavedObjectProps = DashboardSavedObjectRequiredServices & {
id?: string;
savedObjectsClient: SavedObjectsClientContract;
};
export interface LoadDashboardFromSavedObjectReturn {
dashboardFound: boolean;
dashboardId?: string;
resolveMeta?: Omit<ResolvedSimpleSavedObject, 'saved_object'>;
dashboardInput: DashboardContainerInput;
}
export const loadDashboardStateFromSavedObject = async ({
savedObjectsTagging,
savedObjectsClient,
embeddable,
data,
export const loadDashboardState = async ({
id,
}: LoadDashboardFromSavedObjectProps): Promise<LoadDashboardFromSavedObjectReturn> => {
data,
embeddable,
contentManagement,
savedObjectsTagging,
}: LoadDashboardFromSavedObjectProps): Promise<LoadDashboardReturn> => {
const {
search: dataSearchService,
query: { queryString },
@ -73,29 +55,31 @@ export const loadDashboardStateFromSavedObject = async ({
if (!savedObjectId) return { dashboardInput: newDashboardState, dashboardFound: true };
/**
* Load the saved object
* Load the saved object from Content Management
*/
const { saved_object: rawDashboardSavedObject, ...resolveMeta } =
await savedObjectsClient.resolve<DashboardAttributes>(
DASHBOARD_SAVED_OBJECT_TYPE,
savedObjectId
);
if (!rawDashboardSavedObject._version) {
return { dashboardInput: newDashboardState, dashboardFound: false, dashboardId: savedObjectId };
const { item: rawDashboardContent, meta: resolveMeta } = await contentManagement.client.get<
DashboardCrudTypes['GetIn'],
DashboardCrudTypes['GetOut']
>({ contentTypeId: DASHBOARD_CONTENT_ID, id });
if (!rawDashboardContent.version) {
return {
dashboardInput: newDashboardState,
dashboardFound: false,
dashboardId: savedObjectId,
};
}
/**
* Inject saved object references back into the saved object attributes
*/
const { references, attributes: rawAttributes } = rawDashboardSavedObject;
const { references, attributes: rawAttributes } = rawDashboardContent;
const attributes = (() => {
if (!references || references.length === 0) return rawAttributes;
return injectReferences(
{ references, attributes: rawAttributes as unknown as SavedObjectAttributes },
{ references, attributes: rawAttributes },
{
embeddablePersistableStateService: embeddable,
}
) as unknown as DashboardAttributes;
);
})();
/**

View file

@ -15,30 +15,23 @@ import {
controlGroupInputToRawControlGroupAttributes,
} from '@kbn/controls-plugin/common';
import { isFilterPinned } from '@kbn/es-query';
import { SavedObjectsClientContract } from '@kbn/core/public';
import { SavedObjectAttributes } from '@kbn/core-saved-objects-common';
import { SavedObjectSaveOpts } from '@kbn/saved-objects-plugin/public';
import { extractSearchSourceReferences, RefreshInterval } from '@kbn/data-plugin/public';
import {
extractReferences,
DashboardAttributes,
convertPanelMapToSavedPanels,
DashboardContainerInput,
convertPanelMapToSavedPanels,
} from '../../../../common';
import { DashboardSavedObjectRequiredServices } from '../types';
import { DASHBOARD_SAVED_OBJECT_TYPE } from '../../../dashboard_constants';
import {
SaveDashboardProps,
SaveDashboardReturn,
DashboardContentManagementRequiredServices,
} from '../types';
import { DashboardStartDependencies } from '../../../plugin';
import { DASHBOARD_CONTENT_ID } from '../../../dashboard_constants';
import { DashboardCrudTypes, DashboardAttributes } from '../../../../common/content_management';
import { dashboardSaveToastStrings } from '../../../dashboard_container/_dashboard_container_strings';
export type SavedDashboardSaveOpts = SavedObjectSaveOpts & { saveAsCopy?: boolean };
export type SaveDashboardProps = DashboardSavedObjectRequiredServices & {
savedObjectsClient: SavedObjectsClientContract;
currentState: DashboardContainerInput;
saveOptions: SavedDashboardSaveOpts;
lastSavedId?: string;
};
export const serializeControlGroupInput = (
controlGroupInput: DashboardContainerInput['controlGroupInput']
) => {
@ -62,24 +55,28 @@ export const convertTimeToUTCString = (time?: string | Moment): undefined | stri
}
};
export interface SaveDashboardReturn {
id?: string;
error?: string;
redirectRequired?: boolean;
}
type SaveDashboardStateProps = SaveDashboardProps & {
data: DashboardContentManagementRequiredServices['data'];
contentManagement: DashboardStartDependencies['contentManagement'];
embeddable: DashboardContentManagementRequiredServices['embeddable'];
notifications: DashboardContentManagementRequiredServices['notifications'];
initializerContext: DashboardContentManagementRequiredServices['initializerContext'];
savedObjectsTagging: DashboardContentManagementRequiredServices['savedObjectsTagging'];
dashboardSessionStorage: DashboardContentManagementRequiredServices['dashboardSessionStorage'];
};
export const saveDashboardStateToSavedObject = async ({
export const saveDashboardState = async ({
data,
embeddable,
lastSavedId,
saveOptions,
currentState,
savedObjectsClient,
contentManagement,
savedObjectsTagging,
dashboardSessionStorage,
notifications: { toasts },
initializerContext: { kibanaVersion },
}: SaveDashboardProps): Promise<SaveDashboardReturn> => {
}: SaveDashboardStateProps): Promise<SaveDashboardReturn> => {
const {
search: dataSearchService,
query: {
@ -167,7 +164,7 @@ export const saveDashboardStateToSavedObject = async ({
*/
const { attributes, references: dashboardReferences } = extractReferences(
{
attributes: rawDashboardAttributes as unknown as SavedObjectAttributes,
attributes: rawDashboardAttributes,
references: searchSourceReferences,
},
{ embeddablePersistableStateService: embeddable }
@ -177,15 +174,19 @@ export const saveDashboardStateToSavedObject = async ({
: dashboardReferences;
/**
* Save the saved object using the saved objects client
* Save the saved object using the content management
*/
const idToSaveTo = saveOptions.saveAsCopy ? undefined : lastSavedId;
try {
const { id: newId } = await savedObjectsClient.create(DASHBOARD_SAVED_OBJECT_TYPE, attributes, {
id: idToSaveTo,
overwrite: true,
references,
const result = await contentManagement.client.create<
DashboardCrudTypes['CreateIn'],
DashboardCrudTypes['CreateOut']
>({
contentTypeId: DASHBOARD_CONTENT_ID,
data: attributes,
options: { id: idToSaveTo, references, overwrite: true },
});
const newId = result.item.id;
if (newId) {
toasts.addSuccess({

View file

@ -0,0 +1,94 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SavedObjectSaveOpts } from '@kbn/saved-objects-plugin/public';
import {
FindDashboardsByIdResponse,
SearchDashboardsArgs,
SearchDashboardsResponse,
} from './lib/find_dashboards';
import { DashboardDataService } from '../data/types';
import { DashboardSpacesService } from '../spaces/types';
import { DashboardContainerInput } from '../../../common';
import { DashboardStartDependencies } from '../../plugin';
import { DashboardEmbeddableService } from '../embeddable/types';
import { DashboardNotificationsService } from '../notifications/types';
import { DashboardCrudTypes } from '../../../common/content_management';
import { DashboardScreenshotModeService } from '../screenshot_mode/types';
import { DashboardInitializerContextService } from '../initializer_context/types';
import { DashboardSavedObjectsTaggingService } from '../saved_objects_tagging/types';
import { DashboardSessionStorageServiceType } from '../dashboard_session_storage/types';
import { DashboardDuplicateTitleCheckProps } from './lib/check_for_duplicate_dashboard_title';
export interface DashboardContentManagementRequiredServices {
data: DashboardDataService;
spaces: DashboardSpacesService;
embeddable: DashboardEmbeddableService;
notifications: DashboardNotificationsService;
screenshotMode: DashboardScreenshotModeService;
initializerContext: DashboardInitializerContextService;
savedObjectsTagging: DashboardSavedObjectsTaggingService;
dashboardSessionStorage: DashboardSessionStorageServiceType;
}
export interface DashboardContentManagementService {
findDashboards: FindDashboardsService;
deleteDashboards: (ids: string[]) => void;
loadDashboardState: (props: { id?: string }) => Promise<LoadDashboardReturn>;
saveDashboardState: (props: SaveDashboardProps) => Promise<SaveDashboardReturn>;
checkForDuplicateDashboardTitle: (meta: DashboardDuplicateTitleCheckProps) => Promise<boolean>;
}
/**
* Types for Loading Dashboards
*/
export interface LoadDashboardFromSavedObjectProps {
id?: string;
data: DashboardContentManagementRequiredServices['data'];
contentManagement: DashboardStartDependencies['contentManagement'];
embeddable: DashboardContentManagementRequiredServices['embeddable'];
savedObjectsTagging: DashboardContentManagementRequiredServices['savedObjectsTagging'];
}
type DashboardResolveMeta = DashboardCrudTypes['GetOut']['meta'];
export interface LoadDashboardReturn {
dashboardFound: boolean;
dashboardId?: string;
resolveMeta?: DashboardResolveMeta;
dashboardInput: DashboardContainerInput;
}
/**
* Types for Saving Dashboards
*/
export type SavedDashboardSaveOpts = SavedObjectSaveOpts & { saveAsCopy?: boolean };
export interface SaveDashboardProps {
currentState: DashboardContainerInput;
saveOptions: SavedDashboardSaveOpts;
lastSavedId?: string;
}
export interface SaveDashboardReturn {
id?: string;
error?: string;
redirectRequired?: boolean;
}
/**
* Types for Finding Dashboards
*/
export interface FindDashboardsService {
search: (
props: Pick<SearchDashboardsArgs, 'hasReference' | 'hasNoReference' | 'search' | 'size'>
) => Promise<SearchDashboardsResponse>;
findByIds: (ids: string[]) => Promise<FindDashboardsByIdResponse[]>;
findByTitle: (title: string) => Promise<{ id: string } | undefined>;
}

View file

@ -1,78 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { savedObjectsServiceMock } from '@kbn/core/public/mocks';
import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import { FindDashboardSavedObjectsResponse } from './lib/find_dashboard_saved_objects';
import { DashboardSavedObjectService } from './types';
import { DashboardAttributes } from '../../../common';
import { LoadDashboardFromSavedObjectReturn } from './lib/load_dashboard_state_from_saved_object';
type DashboardSavedObjectServiceFactory = PluginServiceFactory<DashboardSavedObjectService>;
export const dashboardSavedObjectServiceFactory: DashboardSavedObjectServiceFactory = () => {
const { client: savedObjectsClient } = savedObjectsServiceMock.createStartContract();
return {
loadDashboardStateFromSavedObject: jest.fn().mockImplementation(() =>
Promise.resolve({
dashboardInput: {},
} as LoadDashboardFromSavedObjectReturn)
),
saveDashboardStateToSavedObject: jest.fn(),
findDashboards: {
findSavedObjects: jest.fn().mockImplementation(({ search, size }) => {
const sizeToUse = size ?? 10;
const hits: FindDashboardSavedObjectsResponse['hits'] = [];
for (let i = 0; i < sizeToUse; i++) {
hits.push({
type: 'dashboard',
id: `dashboard${i}`,
attributes: {
description: `dashboard${i} desc`,
title: `dashboard${i} - ${search} - title`,
},
references: [] as FindDashboardSavedObjectsResponse['hits'][0]['references'],
} as FindDashboardSavedObjectsResponse['hits'][0]);
}
return Promise.resolve({
total: sizeToUse,
hits,
});
}),
findByIds: jest.fn().mockImplementation(() =>
Promise.resolve([
{
id: `dashboardUnsavedOne`,
status: 'success',
attributes: {
title: `Dashboard Unsaved One`,
} as unknown as DashboardAttributes,
},
{
id: `dashboardUnsavedTwo`,
status: 'success',
attributes: {
title: `Dashboard Unsaved Two`,
} as unknown as DashboardAttributes,
},
{
id: `dashboardUnsavedThree`,
status: 'success',
attributes: {
title: `Dashboard Unsaved Three`,
} as unknown as DashboardAttributes,
},
])
),
findByTitle: jest.fn(),
},
checkForDuplicateDashboardTitle: jest.fn(),
savedObjectsClient,
};
};

View file

@ -1,67 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import type { DashboardStartDependencies } from '../../plugin';
import { checkForDuplicateDashboardTitle } from './lib/check_for_duplicate_dashboard_title';
import {
findDashboardIdByTitle,
findDashboardSavedObjects,
findDashboardSavedObjectsByIds,
} from './lib/find_dashboard_saved_objects';
import { saveDashboardStateToSavedObject } from './lib/save_dashboard_state_to_saved_object';
import { loadDashboardStateFromSavedObject } from './lib/load_dashboard_state_from_saved_object';
import type { DashboardSavedObjectRequiredServices, DashboardSavedObjectService } from './types';
export type DashboardSavedObjectServiceFactory = KibanaPluginServiceFactory<
DashboardSavedObjectService,
DashboardStartDependencies,
DashboardSavedObjectRequiredServices
>;
export const dashboardSavedObjectServiceFactory: DashboardSavedObjectServiceFactory = (
{ coreStart },
requiredServices
) => {
const {
savedObjects: { client: savedObjectsClient },
} = coreStart;
return {
loadDashboardStateFromSavedObject: ({ id }) =>
loadDashboardStateFromSavedObject({
id,
savedObjectsClient,
...requiredServices,
}),
saveDashboardStateToSavedObject: ({ currentState, saveOptions, lastSavedId }) =>
saveDashboardStateToSavedObject({
saveOptions,
lastSavedId,
currentState,
savedObjectsClient,
...requiredServices,
}),
findDashboards: {
findSavedObjects: ({ hasReference, hasNoReference, search, size }) =>
findDashboardSavedObjects({
hasReference,
hasNoReference,
search,
size,
savedObjectsClient,
}),
findByIds: (ids) => findDashboardSavedObjectsByIds(savedObjectsClient, ids),
findByTitle: (title) => findDashboardIdByTitle(title, savedObjectsClient),
},
checkForDuplicateDashboardTitle: (props) =>
checkForDuplicateDashboardTitle(props, savedObjectsClient),
savedObjectsClient,
};
};

View file

@ -1,96 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
SavedObjectError,
SavedObjectsClientContract,
SavedObjectsFindOptionsReference,
SimpleSavedObject,
} from '@kbn/core/public';
import { DashboardAttributes } from '../../../../common';
import { DASHBOARD_SAVED_OBJECT_TYPE } from '../../../dashboard_constants';
export interface FindDashboardSavedObjectsArgs {
hasReference?: SavedObjectsFindOptionsReference[];
hasNoReference?: SavedObjectsFindOptionsReference[];
savedObjectsClient: SavedObjectsClientContract;
search: string;
size: number;
}
export interface FindDashboardSavedObjectsResponse {
total: number;
hits: Array<SimpleSavedObject<DashboardAttributes>>;
}
export async function findDashboardSavedObjects({
savedObjectsClient,
hasReference,
hasNoReference,
search,
size,
}: FindDashboardSavedObjectsArgs): Promise<FindDashboardSavedObjectsResponse> {
const { total, savedObjects } = await savedObjectsClient.find<DashboardAttributes>({
type: DASHBOARD_SAVED_OBJECT_TYPE,
search: search ? `${search}*` : undefined,
searchFields: ['title^3', 'description'],
defaultSearchOperator: 'AND' as 'AND',
perPage: size,
hasReference,
hasNoReference,
page: 1,
});
return {
total,
hits: savedObjects,
};
}
export type FindDashboardBySavedObjectIdsResult = { id: string } & (
| { status: 'success'; attributes: DashboardAttributes }
| { status: 'error'; error: SavedObjectError }
);
export async function findDashboardSavedObjectsByIds(
savedObjectsClient: SavedObjectsClientContract,
ids: string[]
): Promise<FindDashboardBySavedObjectIdsResult[]> {
const { savedObjects } = await savedObjectsClient.bulkGet(
ids.map((id) => ({ id, type: DASHBOARD_SAVED_OBJECT_TYPE }))
);
return savedObjects.map((savedObjectResult) => {
if (savedObjectResult.error)
return { status: 'error', error: savedObjectResult.error, id: savedObjectResult.id };
const { attributes, id } = savedObjectResult;
return {
id,
status: 'success',
attributes: attributes as DashboardAttributes,
};
});
}
export async function findDashboardIdByTitle(
title: string,
savedObjectsClient: SavedObjectsClientContract
): Promise<{ id: string } | undefined> {
const results = await savedObjectsClient.find<DashboardAttributes>({
search: `"${title}"`,
searchFields: ['title'],
type: 'dashboard',
});
// The search isn't an exact match, lets see if we can find a single exact match to use
const matchingDashboards = results.savedObjects.filter(
(dashboard) => dashboard.attributes.title.toLowerCase() === title.toLowerCase()
);
if (matchingDashboards.length === 1) {
return { id: matchingDashboards[0].id };
}
}

View file

@ -1,70 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SavedObjectsClientContract } from '@kbn/core/public';
import { DashboardDataService } from '../data/types';
import { DashboardSpacesService } from '../spaces/types';
import { DashboardEmbeddableService } from '../embeddable/types';
import { DashboardNotificationsService } from '../notifications/types';
import { DashboardScreenshotModeService } from '../screenshot_mode/types';
import { DashboardInitializerContextService } from '../initializer_context/types';
import { DashboardSavedObjectsTaggingService } from '../saved_objects_tagging/types';
import { DashboardSessionStorageServiceType } from '../dashboard_session_storage/types';
import {
LoadDashboardFromSavedObjectProps,
LoadDashboardFromSavedObjectReturn,
} from './lib/load_dashboard_state_from_saved_object';
import {
SaveDashboardProps,
SaveDashboardReturn,
} from './lib/save_dashboard_state_to_saved_object';
import {
FindDashboardBySavedObjectIdsResult,
FindDashboardSavedObjectsArgs,
FindDashboardSavedObjectsResponse,
} from './lib/find_dashboard_saved_objects';
import { DashboardDuplicateTitleCheckProps } from './lib/check_for_duplicate_dashboard_title';
export interface DashboardSavedObjectRequiredServices {
screenshotMode: DashboardScreenshotModeService;
embeddable: DashboardEmbeddableService;
spaces: DashboardSpacesService;
data: DashboardDataService;
initializerContext: DashboardInitializerContextService;
notifications: DashboardNotificationsService;
savedObjectsTagging: DashboardSavedObjectsTaggingService;
dashboardSessionStorage: DashboardSessionStorageServiceType;
}
export interface FindDashboardsService {
findSavedObjects: (
props: Pick<
FindDashboardSavedObjectsArgs,
'hasReference' | 'hasNoReference' | 'search' | 'size'
>
) => Promise<FindDashboardSavedObjectsResponse>;
findByIds: (ids: string[]) => Promise<FindDashboardBySavedObjectIdsResult[]>;
findByTitle: (title: string) => Promise<{ id: string } | undefined>;
}
export interface DashboardSavedObjectService {
loadDashboardStateFromSavedObject: (
props: Pick<LoadDashboardFromSavedObjectProps, 'id'>
) => Promise<LoadDashboardFromSavedObjectReturn>;
saveDashboardStateToSavedObject: (
props: Pick<SaveDashboardProps, 'currentState' | 'saveOptions' | 'lastSavedId'>
) => Promise<SaveDashboardReturn>;
findDashboards: FindDashboardsService;
checkForDuplicateDashboardTitle: (meta: DashboardDuplicateTitleCheckProps) => Promise<boolean>;
savedObjectsClient: SavedObjectsClientContract;
}
export type { SaveDashboardReturn };

View file

@ -37,12 +37,12 @@ import { usageCollectionServiceFactory } from './usage_collection/usage_collecti
import { spacesServiceFactory } from './spaces/spaces.stub';
import { urlForwardingServiceFactory } from './url_forwarding/url_fowarding.stub';
import { visualizationsServiceFactory } from './visualizations/visualizations.stub';
import { dashboardSavedObjectServiceFactory } from './dashboard_saved_object/dashboard_saved_object.stub';
import { dashboardContentManagementServiceFactory } from './dashboard_content_management/dashboard_content_management.stub';
import { customBrandingServiceFactory } from './custom_branding/custom_branding.stub';
import { savedObjectsManagementServiceFactory } from './saved_objects_management/saved_objects_management_service.stub';
export const providers: PluginServiceProviders<DashboardServices> = {
dashboardSavedObject: new PluginServiceProvider(dashboardSavedObjectServiceFactory),
dashboardContentManagement: new PluginServiceProvider(dashboardContentManagementServiceFactory),
analytics: new PluginServiceProvider(analyticsServiceFactory),
application: new PluginServiceProvider(applicationServiceFactory),
chrome: new PluginServiceProvider(chromeServiceFactory),

View file

@ -38,12 +38,12 @@ import { urlForwardingServiceFactory } from './url_forwarding/url_forwarding_ser
import { visualizationsServiceFactory } from './visualizations/visualizations_service';
import { usageCollectionServiceFactory } from './usage_collection/usage_collection_service';
import { analyticsServiceFactory } from './analytics/analytics_service';
import { dashboardSavedObjectServiceFactory } from './dashboard_saved_object/dashboard_saved_object_service';
import { customBrandingServiceFactory } from './custom_branding/custom_branding_service';
import { savedObjectsManagementServiceFactory } from './saved_objects_management/saved_objects_management_service';
import { dashboardContentManagementServiceFactory } from './dashboard_content_management/dashboard_content_management_service';
const providers: PluginServiceProviders<DashboardServices, DashboardPluginServiceParams> = {
dashboardSavedObject: new PluginServiceProvider(dashboardSavedObjectServiceFactory, [
dashboardContentManagement: new PluginServiceProvider(dashboardContentManagementServiceFactory, [
'dashboardSessionStorage',
'savedObjectsTagging',
'initializerContext',

View file

@ -17,7 +17,7 @@ import { DashboardChromeService } from './chrome/types';
import { DashboardCoreContextService } from './core_context/types';
import { DashboardCustomBrandingService } from './custom_branding/types';
import { DashboardCapabilitiesService } from './dashboard_capabilities/types';
import { DashboardSavedObjectService } from './dashboard_saved_object/types';
import { DashboardContentManagementService } from './dashboard_content_management/types';
import { DashboardSessionStorageServiceType } from './dashboard_session_storage/types';
import { DashboardDataService } from './data/types';
import { DashboardDataViewEditorService } from './data_view_editor/types';
@ -41,8 +41,8 @@ export type DashboardPluginServiceParams = KibanaPluginServiceParams<DashboardSt
initContext: PluginInitializerContext; // need a custom type so that initContext is a required parameter for initializerContext
};
export interface DashboardServices {
dashboardSavedObject: DashboardSavedObjectService;
dashboardSessionStorage: DashboardSessionStorageServiceType;
dashboardContentManagement: DashboardContentManagementService;
analytics: DashboardAnalyticsService;
application: DashboardApplicationService;

View file

@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils';
import { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server';
import { CONTENT_ID } from '../../common/content_management';
import { cmServicesDefinition } from '../../common/content_management/cm_services';
import type { DashboardCrudTypes } from '../../common/content_management';
const searchArgsToSOFindOptions = (
args: DashboardCrudTypes['SearchIn']
): SavedObjectsFindOptions => {
const { query, contentTypeId, options } = args;
return {
type: contentTypeId,
searchFields: options?.onlyTitle ? ['title'] : ['title^3', 'description'],
fields: ['description', 'title', 'timeRestore'],
search: query.text,
perPage: query.limit,
page: query.cursor ? +query.cursor : undefined,
defaultSearchOperator: 'AND',
...tagsToFindOptions(query.tags),
};
};
export class DashboardStorage extends SOContentStorage<DashboardCrudTypes> {
constructor() {
super({
savedObjectType: CONTENT_ID,
cmServicesDefinition,
searchArgsToSOFindOptions,
enableMSearch: true,
allowedSavedObjectAttributes: [
'kibanaSavedObjectMeta',
'controlGroupInput',
'refreshInterval',
'description',
'timeRestore',
'optionsJSON',
'panelsJSON',
'timeFrom',
'timeTo',
'title',
],
});
}
}

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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { DashboardStorage } from './dashboard_storage';

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import { schema } from '@kbn/config-schema';
import { ANALYTICS_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
import { SavedObjectsType } from '@kbn/core/server';
import {
@ -69,5 +70,46 @@ export const createDashboardSavedObjectType = ({
version: { type: 'integer' },
},
},
schemas: {
'8.9.0': schema.object({
// General
title: schema.string(),
description: schema.string({ defaultValue: '' }),
// Search
kibanaSavedObjectMeta: schema.object({
searchSourceJSON: schema.maybe(schema.string()),
}),
// Time
timeRestore: schema.maybe(schema.boolean()),
timeFrom: schema.maybe(schema.string()),
timeTo: schema.maybe(schema.string()),
refreshInterval: schema.maybe(
schema.object({
pause: schema.boolean(),
value: schema.number(),
display: schema.maybe(schema.string()),
section: schema.maybe(schema.number()),
})
),
// Dashboard Content
controlGroupInput: schema.maybe(
schema.object({
panelsJSON: schema.maybe(schema.string()),
controlStyle: schema.maybe(schema.string()),
chainingSystem: schema.maybe(schema.string()),
ignoreParentSettingsJSON: schema.maybe(schema.string()),
})
),
panelsJSON: schema.string({ defaultValue: '[]' }),
optionsJSON: schema.string({ defaultValue: '{}' }),
// Legacy
hits: schema.maybe(schema.number()),
version: schema.maybe(schema.number()),
}),
},
migrations: () => createDashboardSavedObjectTypeMigrations(migrationDeps),
});

View file

@ -19,8 +19,8 @@ import { SavedObjectEmbeddableInput } from '@kbn/embeddable-plugin/common';
import {
convertPanelStateToSavedDashboardPanel,
convertSavedDashboardPanelToPanelState,
SavedDashboardPanel,
} from '../../../common';
import { SavedDashboardPanel } from '../../../common/content_management';
type ValueOrReferenceInput = SavedObjectEmbeddableInput & {
attributes?: Serializable;

View file

@ -6,9 +6,10 @@
* Side Public License, v 1.
*/
import { SavedObjectAttributes, SavedObjectMigrationFn } from '@kbn/core/server';
import { SavedObjectMigrationFn } from '@kbn/core/server';
import { DashboardAttributes, extractReferences, injectReferences } from '../../../common';
import { extractReferences, injectReferences } from '../../../common';
import { DashboardAttributes } from '../../../common/content_management';
import { DashboardSavedObjectTypeMigrationsDeps } from './dashboard_saved_object_migrations';
/**
@ -36,7 +37,7 @@ export function createExtractPanelReferencesMigration(
const injectedAttributes = injectReferences(
{
attributes: doc.attributes as unknown as SavedObjectAttributes,
attributes: doc.attributes,
references,
},
{ embeddablePersistableStateService: deps.embeddable }

View file

@ -12,8 +12,8 @@ import { EmbeddableInput } from '@kbn/embeddable-plugin/common';
import {
convertSavedDashboardPanelToPanelState,
convertPanelStateToSavedDashboardPanel,
SavedDashboardPanel,
} from '../../../common';
import { SavedDashboardPanel } from '../../../common/content_management';
/**
* Before 7.10, hidden panel titles were stored as a blank string on the title attribute. In 7.10, this was replaced

View file

@ -24,7 +24,7 @@ import {
RawSavedDashboardPanel640To720,
RawSavedDashboardPanel730ToLatest,
} from './types';
import { GridData } from '../../../../common';
import { GridData } from '../../../../common/content_management';
const PANEL_HEIGHT_SCALE_FACTOR = 5;
const PANEL_HEIGHT_SCALE_FACTOR_WITH_MARGINS = 4;

View file

@ -12,28 +12,28 @@ import { SavedObjectReference } from '@kbn/core/server';
import type {
GridData,
DashboardAttributes as CurrentDashboardAttributes, // Dashboard attributes from common are the source of truth for the current version.
} from '../../../../common';
} from '../../../../common/content_management';
interface SavedObjectAttributes {
interface KibanaAttributes {
kibanaSavedObjectMeta: {
searchSourceJSON: string;
};
}
interface Doc<Attributes extends SavedObjectAttributes = SavedObjectAttributes> {
interface Doc<Attributes extends KibanaAttributes = KibanaAttributes> {
references: SavedObjectReference[];
attributes: Attributes;
id: string;
type: string;
}
interface DocPre700<Attributes extends SavedObjectAttributes = SavedObjectAttributes> {
interface DocPre700<Attributes extends KibanaAttributes = KibanaAttributes> {
attributes: Attributes;
id: string;
type: string;
}
interface DashboardAttributesTo720 extends SavedObjectAttributes {
interface DashboardAttributesTo720 extends KibanaAttributes {
panelsJSON: string;
description: string;
uiStateJSON?: string;

View file

@ -25,4 +25,3 @@ export function plugin(initializerContext: PluginInitializerContext) {
}
export type { DashboardPluginSetup, DashboardPluginStart } from './types';
export { findByValueEmbeddables } from './usage/find_by_value_embeddables';

View file

@ -6,32 +6,34 @@
* Side Public License, v 1.
*/
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server';
import { EmbeddableSetup } from '@kbn/embeddable-plugin/server';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import {
TaskManagerSetupContract,
TaskManagerStartContract,
} from '@kbn/task-manager-plugin/server';
import { createDashboardSavedObjectType } from './dashboard_saved_object';
import { capabilitiesProvider } from './capabilities_provider';
import { DashboardPluginSetup, DashboardPluginStart } from './types';
import { registerDashboardUsageCollector } from './usage/register_collector';
import { dashboardPersistableStateServiceFactory } from './dashboard_container/dashboard_container_embeddable_factory';
import { getUISettings } from './ui_settings';
import { EmbeddableSetup } from '@kbn/embeddable-plugin/server';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import { ContentManagementServerSetup } from '@kbn/content-management-plugin/server';
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server';
import {
initializeDashboardTelemetryTask,
scheduleDashboardTelemetry,
TASK_ID,
} from './usage/dashboard_telemetry_collection_task';
import { getUISettings } from './ui_settings';
import { DashboardStorage } from './content_management';
import { capabilitiesProvider } from './capabilities_provider';
import { DashboardPluginSetup, DashboardPluginStart } from './types';
import { createDashboardSavedObjectType } from './dashboard_saved_object';
import { CONTENT_ID, LATEST_VERSION } from '../common/content_management';
import { registerDashboardUsageCollector } from './usage/register_collector';
import { dashboardPersistableStateServiceFactory } from './dashboard_container/dashboard_container_embeddable_factory';
interface SetupDeps {
embeddable: EmbeddableSetup;
usageCollection: UsageCollectionSetup;
taskManager: TaskManagerSetupContract;
contentManagement: ContentManagementServerSetup;
}
interface StartDeps {
@ -58,6 +60,14 @@ export class DashboardPlugin
})
);
plugins.contentManagement.register({
id: CONTENT_ID,
storage: new DashboardStorage(),
version: {
latest: LATEST_VERSION,
},
});
if (plugins.taskManager) {
initializeDashboardTelemetryTask(this.logger, core, plugins.taskManager, plugins.embeddable);
}

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { SavedDashboardPanel } from '../../common';
import { SavedDashboardPanel } from '../../common/content_management';
import { getEmptyDashboardData, collectPanelsByType } from './dashboard_telemetry';
import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
import { createEmbeddablePersistableStateServiceMock } from '@kbn/embeddable-plugin/common/mocks';

View file

@ -7,16 +7,13 @@
*/
import { isEmpty } from 'lodash';
import { SavedObjectAttributes } from '@kbn/core/server';
import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common';
import {
type ControlGroupTelemetry,
CONTROL_GROUP_TYPE,
RawControlGroupAttributes,
} from '@kbn/controls-plugin/common';
import { initializeControlGroupTelemetry } from '@kbn/controls-plugin/server';
import { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
import type { SavedDashboardPanel } from '../../common';
import { initializeControlGroupTelemetry } from '@kbn/controls-plugin/server';
import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common';
import { type ControlGroupTelemetry, CONTROL_GROUP_TYPE } from '@kbn/controls-plugin/common';
import { DashboardAttributes, SavedDashboardPanel } from '../../common/content_management';
import { TASK_ID, DashboardTelemetryTaskState } from './dashboard_telemetry_collection_task';
export interface DashboardCollectorData {
panels: {
@ -90,13 +87,11 @@ export const collectPanelsByType = (
export const controlsCollectorFactory =
(embeddableService: EmbeddablePersistableStateService) =>
(attributes: SavedObjectAttributes, collectorData: DashboardCollectorData) => {
const controlGroupAttributes: RawControlGroupAttributes | undefined =
attributes.controlGroupInput as unknown as RawControlGroupAttributes;
if (!isEmpty(controlGroupAttributes)) {
(attributes: DashboardAttributes, collectorData: DashboardCollectorData) => {
if (!isEmpty(attributes.controlGroupInput)) {
collectorData.controls = embeddableService.telemetry(
{
...controlGroupAttributes,
...attributes.controlGroupInput,
type: CONTROL_GROUP_TYPE,
id: `DASHBOARD_${CONTROL_GROUP_TYPE}`,
},

View file

@ -6,21 +6,25 @@
* Side Public License, v 1.
*/
import { CoreSetup, Logger, SavedObjectAttributes, SavedObjectReference } from '@kbn/core/server';
import moment from 'moment';
import {
RunContext,
TaskManagerSetupContract,
TaskManagerStartContract,
} from '@kbn/task-manager-plugin/server';
import { EmbeddableSetup } from '@kbn/embeddable-plugin/server';
import { CoreSetup, Logger, SavedObjectReference } from '@kbn/core/server';
import {
controlsCollectorFactory,
collectPanelsByType,
getEmptyDashboardData,
DashboardCollectorData,
} from './dashboard_telemetry';
import { injectReferences, SavedDashboardPanel } from '../../common';
import { injectReferences } from '../../common';
import { DashboardAttributesAndReferences } from '../../common/types';
import { DashboardAttributes, SavedDashboardPanel } from '../../common/content_management';
// This task is responsible for running daily and aggregating all the Dashboard telemerty data
// into a single document. This is an effort to make sure the load of fetching/parsing all of the
@ -28,11 +32,6 @@ import { injectReferences, SavedDashboardPanel } from '../../common';
const TELEMETRY_TASK_TYPE = 'dashboard_telemetry';
export const TASK_ID = `Dashboard-${TELEMETRY_TASK_TYPE}`;
interface SavedObjectAttributesAndReferences {
attributes: SavedObjectAttributes;
references: SavedObjectReference[];
}
export interface DashboardTelemetryTaskState {
runs: number;
telemetry: DashboardCollectorData;
@ -92,7 +91,7 @@ export function dashboardTaskRunner(logger: Logger, core: CoreSetup, embeddable:
async run() {
let dashboardData = getEmptyDashboardData();
const controlsCollector = controlsCollectorFactory(embeddable);
const processDashboards = (dashboards: SavedObjectAttributesAndReferences[]) => {
const processDashboards = (dashboards: DashboardAttributesAndReferences[]) => {
for (const dashboard of dashboards) {
const attributes = injectReferences(dashboard, {
embeddablePersistableStateService: embeddable,
@ -133,7 +132,7 @@ export function dashboardTaskRunner(logger: Logger, core: CoreSetup, embeddable:
const esClient = await getEsClient();
let result = await esClient.search<{
dashboard: SavedObjectAttributes;
dashboard: DashboardAttributes;
references: SavedObjectReference[];
}>(searchParams);
@ -148,8 +147,8 @@ export function dashboardTaskRunner(logger: Logger, core: CoreSetup, embeddable:
}
return undefined;
})
.filter<SavedObjectAttributesAndReferences>(
(s): s is SavedObjectAttributesAndReferences => s !== undefined
.filter<DashboardAttributesAndReferences>(
(s): s is DashboardAttributesAndReferences => s !== undefined
)
);
@ -167,8 +166,8 @@ export function dashboardTaskRunner(logger: Logger, core: CoreSetup, embeddable:
}
return undefined;
})
.filter<SavedObjectAttributesAndReferences>(
(s): s is SavedObjectAttributesAndReferences => s !== undefined
.filter<DashboardAttributesAndReferences>(
(s): s is DashboardAttributesAndReferences => s !== undefined
)
);
}

View file

@ -1,60 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { SavedDashboardPanel } from '../../common';
import { findByValueEmbeddables } from './find_by_value_embeddables';
const visualizationByValue = {
embeddableConfig: {
value: 'visualization-by-value',
},
type: 'visualization',
} as unknown as SavedDashboardPanel;
const mapByValue = {
embeddableConfig: {
value: 'map-by-value',
},
type: 'map',
} as unknown as SavedDashboardPanel;
const embeddableByRef = {
panelRefName: 'panel_ref_1',
} as unknown as SavedDashboardPanel;
describe('findByValueEmbeddables', () => {
it('finds the by value embeddables for the given type', async () => {
const savedObjectsResult = {
saved_objects: [
{
attributes: {
panelsJSON: JSON.stringify([visualizationByValue, mapByValue, embeddableByRef]),
},
},
{
attributes: {
panelsJSON: JSON.stringify([embeddableByRef, mapByValue, visualizationByValue]),
},
},
],
};
const savedObjectClient = { find: jest.fn().mockResolvedValue(savedObjectsResult) };
const maps = await findByValueEmbeddables(savedObjectClient, 'map');
expect(maps.length).toBe(2);
expect(maps[0]).toEqual(mapByValue.embeddableConfig);
expect(maps[1]).toEqual(mapByValue.embeddableConfig);
const visualizations = await findByValueEmbeddables(savedObjectClient, 'visualization');
expect(visualizations.length).toBe(2);
expect(visualizations[0]).toEqual(visualizationByValue.embeddableConfig);
expect(visualizations[1]).toEqual(visualizationByValue.embeddableConfig);
});
});

View file

@ -1,34 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ISavedObjectsRepository, SavedObjectAttributes } from '@kbn/core/server';
import type { SavedDashboardPanel } from '../../common';
export const findByValueEmbeddables = async (
savedObjectClient: Pick<ISavedObjectsRepository, 'find'>,
embeddableType: string
) => {
const dashboards = await savedObjectClient.find<SavedObjectAttributes>({
type: 'dashboard',
});
return dashboards.saved_objects
.map((dashboard) => {
try {
return JSON.parse(
dashboard.attributes.panelsJSON as string
) as unknown as SavedDashboardPanel[];
} catch (exception) {
return [];
}
})
.flat()
.filter((panel) => (panel as Record<string, any>).panelRefName === undefined)
.filter((panel) => panel.type === embeddableType)
.map((panel) => panel.embeddableConfig);
};

View file

@ -1,7 +1,7 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"outDir": "target/types"
},
"include": ["*.ts", ".storybook/**/*.ts", "common/**/*", "public/**/*", "server/**/*"],
"kbn_references": [
@ -33,6 +33,8 @@
"@kbn/unified-search-plugin",
"@kbn/shared-ux-page-analytics-no-data",
"@kbn/content-management-table-list",
"@kbn/content-management-plugin",
"@kbn/content-management-utils",
"@kbn/i18n-react",
"@kbn/expressions-plugin",
"@kbn/field-formats-plugin",
@ -48,7 +50,6 @@
"@kbn/core-overlays-browser-mocks",
"@kbn/core-theme-browser-mocks",
"@kbn/core-ui-settings-browser-mocks",
"@kbn/core-saved-objects-common",
"@kbn/task-manager-plugin",
"@kbn/core-execution-context-common",
"@kbn/core-custom-branding-browser",
@ -58,8 +59,8 @@
"@kbn/shared-ux-button-toolbar",
"@kbn/core-saved-objects-server",
"@kbn/core-saved-objects-utils-server",
"@kbn/object-versioning",
"@kbn/core-saved-objects-api-server"
],
"exclude": [
"target/**/*",
]
"exclude": ["target/**/*"]
}

View file

@ -80,6 +80,9 @@ export interface PanelState<E extends EmbeddableInput & { id: string } = { id: s
// Stores input for this embeddable that is specific to this embeddable. Other parts of embeddable input
// will be derived from the container's input. **State in here will override state derived from the container.**
explicitInput: Partial<E> & { id: string };
// allows individual embeddable panels to maintain versioning information separate from the main Kibana version
version?: string;
}
export type EmbeddableStateWithType = EmbeddableInput & { type: string };

View file

@ -12,7 +12,13 @@ export class AuditTrailTestPlugin implements Plugin {
const router = core.http.createRouter();
router.get({ path: '/audit_log', validate: false }, async (context, request, response) => {
const soClient = (await context.core).savedObjects.client;
await soClient.create('dashboard', {});
await soClient.create('dashboard', {
title: '',
optionsJSON: '',
description: '',
panelsJSON: '{}',
kibanaSavedObjectMeta: {},
});
await soClient.find({ type: 'dashboard' });
return response.noContent();
});