diff --git a/src/plugins/dashboard/server/api/register_routes.ts b/src/plugins/dashboard/server/api/register_routes.ts index 692942e1bd1bb..00d0c18186a43 100644 --- a/src/plugins/dashboard/server/api/register_routes.ts +++ b/src/plugins/dashboard/server/api/register_routes.ts @@ -12,6 +12,7 @@ import type { ContentManagementServerSetup } from '@kbn/content-management-plugi import type { HttpServiceSetup } from '@kbn/core/server'; import type { UsageCounter } from '@kbn/usage-collection-plugin/server'; import type { Logger } from '@kbn/logging'; +import type { EmbeddableStart } from '@kbn/embeddable-plugin/server'; import { CONTENT_ID } from '../../common/content_management'; import { @@ -20,10 +21,10 @@ import { PUBLIC_API_CONTENT_MANAGEMENT_VERSION, } from './constants'; import { - dashboardAttributesSchema, - dashboardGetResultSchema, - dashboardCreateResultSchema, - dashboardSearchResultsSchema, + getDashboardAttributesSchema, + getDashboardGetResultSchema, + getDashboardCreateResultSchema, + getDashboardSearchResultsSchema, referenceSchema, } from '../content_management/v3'; @@ -32,6 +33,7 @@ interface RegisterAPIRoutesArgs { contentManagement: ContentManagementServerSetup; restCounter?: UsageCounter; logger: Logger; + embeddable: EmbeddableStart; } const TECHNICAL_PREVIEW_WARNING = @@ -40,6 +42,7 @@ const TECHNICAL_PREVIEW_WARNING = export function registerAPIRoutes({ http, contentManagement, + embeddable, restCounter, logger, }: RegisterAPIRoutesArgs) { @@ -59,23 +62,23 @@ export function registerAPIRoutes({ createRoute.addVersion( { version: PUBLIC_API_VERSION, - validate: { + validate: () => ({ request: { params: schema.object({ id: schema.maybe(schema.string()), }), body: schema.object({ - attributes: dashboardAttributesSchema, + attributes: getDashboardAttributesSchema(embeddable), references: schema.maybe(schema.arrayOf(referenceSchema)), spaces: schema.maybe(schema.arrayOf(schema.string())), }), }, response: { 200: { - body: () => dashboardCreateResultSchema, + body: () => getDashboardCreateResultSchema(embeddable), }, }, - }, + }), }, async (ctx, req, res) => { const { id } = req.params; @@ -131,13 +134,13 @@ export function registerAPIRoutes({ id: schema.string(), }), body: schema.object({ - attributes: dashboardAttributesSchema, + attributes: getDashboardAttributesSchema(embeddable), references: schema.maybe(schema.arrayOf(referenceSchema)), }), }, response: { 200: { - body: () => dashboardCreateResultSchema, + body: () => getDashboardCreateResultSchema(embeddable), }, }, }, @@ -193,7 +196,7 @@ export function registerAPIRoutes({ 200: { body: () => schema.object({ - items: schema.arrayOf(dashboardSearchResultsSchema), + items: schema.arrayOf(getDashboardSearchResultsSchema(embeddable)), total: schema.number(), }), }, @@ -247,7 +250,7 @@ export function registerAPIRoutes({ }, response: { 200: { - body: () => dashboardGetResultSchema, + body: () => getDashboardGetResultSchema(embeddable), }, }, }, diff --git a/src/plugins/dashboard/server/content_management/cm_services.ts b/src/plugins/dashboard/server/content_management/cm_services.ts index 081d7ad8a39d4..cf2d20dade623 100644 --- a/src/plugins/dashboard/server/content_management/cm_services.ts +++ b/src/plugins/dashboard/server/content_management/cm_services.ts @@ -11,16 +11,19 @@ import type { ContentManagementServicesDefinition as ServicesDefinition, Version, } from '@kbn/object-versioning'; +import { EmbeddableStart } from '@kbn/embeddable-plugin/server'; // 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'; import { serviceDefinition as v2 } from './v2'; -import { serviceDefinition as v3 } from './v3'; +import { getServiceDefinition as v3 } from './v3'; -export const cmServicesDefinition: { [version: Version]: ServicesDefinition } = { +export const getCmServicesDefinition = ( + embeddable: EmbeddableStart +): { [version: Version]: ServicesDefinition } => ({ 1: v1, 2: v2, - 3: v3, -}; + 3: v3(embeddable), +}); diff --git a/src/plugins/dashboard/server/content_management/dashboard_storage.ts b/src/plugins/dashboard/server/content_management/dashboard_storage.ts index e65002802989f..5d0611261a641 100644 --- a/src/plugins/dashboard/server/content_management/dashboard_storage.ts +++ b/src/plugins/dashboard/server/content_management/dashboard_storage.ts @@ -9,6 +9,7 @@ import Boom from '@hapi/boom'; import { tagsToFindOptions } from '@kbn/content-management-utils'; +import type { CoreSetup } from '@kbn/core-lifecycle-browser'; import { SavedObjectsFindOptions, SavedObjectsFindResult, @@ -18,7 +19,7 @@ import type { Logger } from '@kbn/logging'; import { CreateResult, DeleteResult, SearchQuery } from '@kbn/content-management-plugin/common'; import { StorageContext } from '@kbn/content-management-plugin/server'; import { DASHBOARD_SAVED_OBJECT_TYPE } from '../dashboard_saved_object'; -import { cmServicesDefinition } from './cm_services'; +import { getCmServicesDefinition } from './cm_services'; import { DashboardSavedObjectAttributes } from '../dashboard_saved_object'; import { itemAttrsToSavedObjectAttrs, savedObjectToItem } from './latest'; import type { @@ -60,67 +61,72 @@ const savedObjectClientFromRequest = async (ctx: StorageContext) => { export class DashboardStorage { constructor({ + core, logger, throwOnResultValidationError, }: { + core: CoreSetup; logger: Logger; throwOnResultValidationError: boolean; }) { + this.core = core; this.logger = logger; this.throwOnResultValidationError = throwOnResultValidationError ?? false; - this.mSearch = { - savedObjectType: DASHBOARD_SAVED_OBJECT_TYPE, - additionalSearchFields: [], - toItemResult: (ctx: StorageContext, savedObject: SavedObjectsFindResult): DashboardItem => { - const transforms = ctx.utils.getTransforms(cmServicesDefinition); - - const { item, error: itemError } = savedObjectToItem( - savedObject as SavedObjectsFindResult, - false - ); - if (itemError) { - throw Boom.badRequest(`Invalid response. ${itemError.message}`); - } - - const validationError = transforms.mSearch.out.result.validate(item); - if (validationError) { - if (this.throwOnResultValidationError) { - throw Boom.badRequest(`Invalid response. ${validationError.message}`); - } else { - this.logger.warn(`Invalid response. ${validationError.message}`); - } - } - - // Validate DB response and DOWN transform to the request version - const { value, error: resultError } = transforms.mSearch.out.result.down< - DashboardItem, - DashboardItem - >( - item, - undefined, // do not override version - { validate: false } // validation is done above - ); - - if (resultError) { - throw Boom.badRequest(`Invalid response. ${resultError.message}`); - } - - return value; - }, - }; + // this.mSearch = { + // savedObjectType: DASHBOARD_SAVED_OBJECT_TYPE, + // additionalSearchFields: [], + // toItemResult: (ctx: StorageContext, savedObject: SavedObjectsFindResult): DashboardItem => { + // const transforms = ctx.utils.getTransforms(getCmServicesDefinition(this.embeddable)); + + // const { item, error: itemError } = savedObjectToItem( + // savedObject as SavedObjectsFindResult, + // false + // ); + // if (itemError) { + // throw Boom.badRequest(`Invalid response. ${itemError.message}`); + // } + + // const validationError = transforms.mSearch.out.result.validate(item); + // if (validationError) { + // if (this.throwOnResultValidationError) { + // throw Boom.badRequest(`Invalid response. ${validationError.message}`); + // } else { + // this.logger.warn(`Invalid response. ${validationError.message}`); + // } + // } + + // // Validate DB response and DOWN transform to the request version + // const { value, error: resultError } = transforms.mSearch.out.result.down< + // DashboardItem, + // DashboardItem + // >( + // item, + // undefined, // do not override version + // { validate: false } // validation is done above + // ); + + // if (resultError) { + // throw Boom.badRequest(`Invalid response. ${resultError.message}`); + // } + + // return value; + // }, + // }; } + private core: CoreSetup; private logger: Logger; private throwOnResultValidationError: boolean; - mSearch: { - savedObjectType: string; - toItemResult: (ctx: StorageContext, savedObject: SavedObjectsFindResult) => DashboardItem; - additionalSearchFields?: string[]; - }; + // mSearch: { + // savedObjectType: string; + // toItemResult: (ctx: StorageContext, savedObject: SavedObjectsFindResult) => DashboardItem; + // additionalSearchFields?: string[]; + // }; async get(ctx: StorageContext, id: string): Promise { - const transforms = ctx.utils.getTransforms(cmServicesDefinition); + const [_, { embeddable }] = await this.core.getStartServices(); + const transforms = ctx.utils.getTransforms(getCmServicesDefinition(embeddable)); const soClient = await savedObjectClientFromRequest(ctx); // Save data in DB @@ -174,7 +180,8 @@ export class DashboardStorage { data: DashboardAttributes, options: DashboardCreateOptions ): Promise { - const transforms = ctx.utils.getTransforms(cmServicesDefinition); + const [_, { embeddable }] = await this.core.getStartServices(); + const transforms = ctx.utils.getTransforms(getCmServicesDefinition(embeddable)); const soClient = await savedObjectClientFromRequest(ctx); // Validate input (data & options) & UP transform them to the latest version @@ -243,7 +250,8 @@ export class DashboardStorage { data: DashboardAttributes, options: DashboardUpdateOptions ): Promise { - const transforms = ctx.utils.getTransforms(cmServicesDefinition); + const [_, { embeddable }] = await this.core.getStartServices(); + const transforms = ctx.utils.getTransforms(getCmServicesDefinition(embeddable)); const soClient = await savedObjectClientFromRequest(ctx); // Validate input (data & options) & UP transform them to the latest version @@ -324,7 +332,8 @@ export class DashboardStorage { query: SearchQuery, options: DashboardSearchOptions ): Promise { - const transforms = ctx.utils.getTransforms(cmServicesDefinition); + const [_, { embeddable }] = await this.core.getStartServices(); + const transforms = ctx.utils.getTransforms(getCmServicesDefinition(embeddable)); const soClient = await savedObjectClientFromRequest(ctx); // Validate and UP transform the options diff --git a/src/plugins/dashboard/server/content_management/v3/cm_services.ts b/src/plugins/dashboard/server/content_management/v3/cm_services.ts index e086d1cc1460a..3a768389f9cc1 100644 --- a/src/plugins/dashboard/server/content_management/v3/cm_services.ts +++ b/src/plugins/dashboard/server/content_management/v3/cm_services.ts @@ -8,7 +8,7 @@ */ import { v4 as uuidv4 } from 'uuid'; -import { schema, Type } from '@kbn/config-schema'; +import { AnyType, ConditionalType, ObjectType, schema, Type } from '@kbn/config-schema'; import { createOptionsSchemas, updateOptionsSchema } from '@kbn/content-management-utils'; import type { ContentManagementServicesDefinition as ServicesDefinition } from '@kbn/object-versioning'; import { @@ -27,6 +27,8 @@ import { } from '@kbn/controls-plugin/common'; import { FilterStateStore } from '@kbn/es-query'; import { SortDirection } from '@kbn/data-plugin/common/search'; +import { ViewMode } from '@kbn/embeddable-plugin/common'; +import type { EmbeddableStart } from '@kbn/embeddable-plugin/server'; import { DASHBOARD_GRID_COLUMN_COUNT, DEFAULT_PANEL_HEIGHT, @@ -35,6 +37,21 @@ import { } from '../../../common/content_management'; import { getResultV3ToV2 } from './transform_utils'; +function buildConditionalSchema(conditions: Array<{ type: string; schema: Type }>) { + return conditions.reduce, Type> | AnyType>( + (acc, condition) => { + return schema.conditional( + // a panelRefName means the panel is by-reference, so don't validate the panelConfig + schema.siblingRef('.panelRefName'), + schema.string(), + schema.any(), + schema.conditional(schema.siblingRef('.type'), condition.type, condition.schema, acc) + ); + }, + schema.any() + ); +} + const apiError = schema.object({ error: schema.string(), message: schema.string(), @@ -249,57 +266,77 @@ export const gridDataSchema = schema.object({ }), }); -export const panelSchema = schema.object({ - panelConfig: schema.object( - { - version: schema.maybe( - schema.string({ - meta: { description: 'The version of the embeddable in the panel.' }, - }) - ), - title: schema.maybe(schema.string({ meta: { description: 'The title of the panel' } })), - description: schema.maybe( - schema.string({ meta: { description: 'The description of the panel' } }) - ), - savedObjectId: schema.maybe( - schema.string({ - meta: { description: 'The unique id of the library item to construct the embeddable.' }, - }) - ), - hidePanelTitles: schema.maybe( - schema.boolean({ - defaultValue: false, - meta: { description: 'Set to true to hide the panel title in its container.' }, - }) - ), - enhancements: schema.maybe(schema.recordOf(schema.string(), schema.any())), - }, - { - unknowns: 'allow', - } +const commonPanelConfigSchema = schema.object({ + version: schema.maybe( + schema.string({ + meta: { description: 'The version of the embeddable in the panel.' }, + }) ), - id: schema.maybe( - schema.string({ meta: { description: 'The saved object id for by reference panels' } }) + viewMode: schema.maybe( + schema.oneOf( + Object.values(ViewMode) + .filter((value): value is ViewMode => typeof value === 'string') + .map((value) => schema.literal(value)) as [Type] + ) ), - type: schema.string({ meta: { description: 'The embeddable type' } }), - panelRefName: schema.maybe(schema.string()), - gridData: gridDataSchema, - panelIndex: schema.string({ - meta: { description: 'The unique ID of the panel.' }, - defaultValue: schema.siblingRef('gridData.i'), - }), title: schema.maybe(schema.string({ meta: { description: 'The title of the panel' } })), - version: schema.maybe( + description: schema.maybe( + schema.string({ meta: { description: 'The description of the panel' } }) + ), + savedObjectId: schema.maybe( schema.string({ meta: { - description: - "The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).", - deprecated: true, + description: 'The unique id of the library item to construct the embeddable.', }, }) ), + hidePanelTitles: schema.maybe( + schema.boolean({ + defaultValue: false, + meta: { description: 'Set to true to hide the panel title in its container.' }, + }) + ), + enhancements: schema.maybe(schema.recordOf(schema.string(), schema.any())), + disabledActions: schema.maybe(schema.arrayOf(schema.string())), + disableTriggers: schema.maybe(schema.boolean()), + timeRange: schema.maybe(schema.object({ from: schema.string(), to: schema.string() })), }); +const generatePanelConfig = (getValidationSchema: () => ObjectType) => { + if (getValidationSchema) { + const validationSchema = getValidationSchema(); + return schema.intersection([commonPanelConfigSchema, validationSchema]); + } + return commonPanelConfigSchema.extendsDeep({ unknowns: 'allow' }); +}; + +const getPanelSchema = (embeddableId: string, embeddable: EmbeddableStart) => { + const panelConfigSchema = generatePanelConfig(embeddable.getValidationSchema(embeddableId)); + return schema.object({ + panelConfig: panelConfigSchema, + id: schema.maybe( + schema.string({ meta: { description: 'The saved object id for by reference panels' } }) + ), + type: schema.literal(embeddableId), + panelRefName: schema.maybe(schema.string()), + gridData: gridDataSchema, + panelIndex: schema.string({ + meta: { description: 'The unique ID of the panel.' }, + defaultValue: schema.siblingRef('gridData.i'), + }), + title: schema.maybe(schema.string({ meta: { description: 'The title of the panel' } })), + version: schema.maybe( + schema.string({ + meta: { + description: + "The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).", + deprecated: true, + }, + }) + ), + }); +}; + export const optionsSchema = schema.object({ hidePanelTitles: schema.boolean({ defaultValue: DEFAULT_DASHBOARD_OPTIONS.hidePanelTitles, @@ -333,72 +370,91 @@ export const searchResultsAttributesSchema = schema.object({ }), }); -export const dashboardAttributesSchema = searchResultsAttributesSchema.extends({ - // Search - kibanaSavedObjectMeta: schema.object( - { - searchSource: schema.maybe(searchSourceSchema), - }, - { - meta: { - description: 'A container for various metadata', +export const getDashboardAttributesSchema = (embeddable: EmbeddableStart) => { + const panelSchemas = embeddable + .getRegisteredEmbeddableFactories() + .filter( + (embeddableId: string) => + ![ + 'control_group', + 'dashboard', + 'optionsListControl', + 'rangeSliderControl', + 'timeSlider', + ].includes(embeddableId) + ) + .map((embeddableId: string) => ({ + type: embeddableId, + schema: getPanelSchema(embeddableId, embeddable), + })); + + return searchResultsAttributesSchema.extends({ + // Search + kibanaSavedObjectMeta: schema.object( + { + searchSource: schema.maybe(searchSourceSchema), }, - defaultValue: {}, - } - ), - // Time - timeFrom: schema.maybe( - schema.string({ meta: { description: 'An ISO string indicating when to restore time from' } }) - ), - timeTo: schema.maybe( - schema.string({ meta: { description: 'An ISO string indicating when to restore time from' } }) - ), - refreshInterval: schema.maybe( - schema.object( { - pause: schema.boolean({ - meta: { - description: - 'Whether the refresh interval is set to be paused while viewing the dashboard.', - }, - }), - value: schema.number({ - meta: { - description: 'A numeric value indicating refresh frequency in milliseconds.', - }, - }), - display: schema.maybe( - schema.string({ + meta: { + description: 'A container for various metadata', + }, + defaultValue: {}, + } + ), + // Time + timeFrom: schema.maybe( + schema.string({ meta: { description: 'An ISO string indicating when to restore time from' } }) + ), + timeTo: schema.maybe( + schema.string({ meta: { description: 'An ISO string indicating when to restore time from' } }) + ), + refreshInterval: schema.maybe( + schema.object( + { + pause: schema.boolean({ meta: { description: - 'A human-readable string indicating the refresh frequency. No longer used.', - deprecated: true, + 'Whether the refresh interval is set to be paused while viewing the dashboard.', }, - }) - ), - section: schema.maybe( - schema.number({ + }), + value: schema.number({ meta: { - description: 'No longer used.', // TODO what is this legacy property? - deprecated: true, + description: 'A numeric value indicating refresh frequency in milliseconds.', }, - }) - ), - }, - { - meta: { - description: 'A container for various refresh interval settings', + }), + display: schema.maybe( + schema.string({ + meta: { + description: + 'A human-readable string indicating the refresh frequency. No longer used.', + deprecated: true, + }, + }) + ), + section: schema.maybe( + schema.number({ + meta: { + description: 'No longer used.', // TODO what is this legacy property? + deprecated: true, + }, + }) + ), }, - } - ) - ), + { + meta: { + description: 'A container for various refresh interval settings', + }, + } + ) + ), - // Dashboard Content - controlGroupInput: schema.maybe(controlGroupInputSchema), - panels: schema.arrayOf(panelSchema, { defaultValue: [] }), - options: optionsSchema, - version: schema.maybe(schema.number({ meta: { deprecated: true } })), -}); + // Dashboard Content + controlGroupInput: schema.maybe(controlGroupInputSchema), + panels: schema.arrayOf(buildConditionalSchema(panelSchemas), { defaultValue: [] }), + options: optionsSchema, + version: schema.maybe(schema.number({ meta: { deprecated: true } })), + }); +}; export const referenceSchema = schema.object( { @@ -409,28 +465,30 @@ export const referenceSchema = schema.object( { unknowns: 'forbid' } ); -export const dashboardItemSchema = schema.object( - { - id: schema.string(), - type: schema.string(), - version: schema.maybe(schema.string()), - createdAt: schema.maybe(schema.string()), - updatedAt: schema.maybe(schema.string()), - createdBy: schema.maybe(schema.string()), - updatedBy: schema.maybe(schema.string()), - managed: schema.maybe(schema.boolean()), - error: schema.maybe(apiError), - attributes: dashboardAttributesSchema, - references: schema.arrayOf(referenceSchema), - namespaces: schema.maybe(schema.arrayOf(schema.string())), - originId: schema.maybe(schema.string()), - }, - { unknowns: 'allow' } -); +export const getDashboardItemSchema = (embeddable: EmbeddableStart) => + schema.object( + { + id: schema.string(), + type: schema.string(), + version: schema.maybe(schema.string()), + createdAt: schema.maybe(schema.string()), + updatedAt: schema.maybe(schema.string()), + createdBy: schema.maybe(schema.string()), + updatedBy: schema.maybe(schema.string()), + managed: schema.maybe(schema.boolean()), + error: schema.maybe(apiError), + attributes: getDashboardAttributesSchema(embeddable), + references: schema.arrayOf(referenceSchema), + namespaces: schema.maybe(schema.arrayOf(schema.string())), + originId: schema.maybe(schema.string()), + }, + { unknowns: 'allow' } + ); -export const dashboardSearchResultsSchema = dashboardItemSchema.extends({ - attributes: searchResultsAttributesSchema, -}); +export const getDashboardSearchResultsSchema = (embeddable: EmbeddableStart) => + getDashboardItemSchema(embeddable).extends({ + attributes: searchResultsAttributesSchema, + }); export const dashboardSearchOptionsSchema = schema.maybe( schema.object( @@ -457,42 +515,44 @@ export const dashboardUpdateOptionsSchema = schema.object({ mergeAttributes: schema.maybe(updateOptionsSchema.mergeAttributes), }); -export const dashboardGetResultSchema = schema.object( - { - item: dashboardItemSchema, - meta: schema.object( - { - outcome: schema.oneOf([ - schema.literal('exactMatch'), - schema.literal('aliasMatch'), - schema.literal('conflict'), - ]), - aliasTargetId: schema.maybe(schema.string()), - aliasPurpose: schema.maybe( - schema.oneOf([ - schema.literal('savedObjectConversion'), - schema.literal('savedObjectImport'), - ]) - ), - }, - { unknowns: 'forbid' } - ), - }, - { unknowns: 'forbid' } -); +export const getDashboardGetResultSchema = (embeddable: EmbeddableStart) => + schema.object( + { + item: getDashboardItemSchema(embeddable), + meta: schema.object( + { + outcome: schema.oneOf([ + schema.literal('exactMatch'), + schema.literal('aliasMatch'), + schema.literal('conflict'), + ]), + aliasTargetId: schema.maybe(schema.string()), + aliasPurpose: schema.maybe( + schema.oneOf([ + schema.literal('savedObjectConversion'), + schema.literal('savedObjectImport'), + ]) + ), + }, + { unknowns: 'forbid' } + ), + }, + { unknowns: 'forbid' } + ); -export const dashboardCreateResultSchema = schema.object( - { - item: dashboardItemSchema, - }, - { unknowns: 'forbid' } -); +export const getDashboardCreateResultSchema = (embeddable: EmbeddableStart) => + schema.object( + { + item: getDashboardItemSchema(embeddable), + }, + { unknowns: 'forbid' } + ); -export const serviceDefinition: ServicesDefinition = { +export const getServiceDefinition = (embeddable: EmbeddableStart): ServicesDefinition => ({ get: { out: { result: { - schema: dashboardGetResultSchema, + schema: getDashboardGetResultSchema(embeddable), down: getResultV3ToV2, }, }, @@ -503,12 +563,12 @@ export const serviceDefinition: ServicesDefinition = { schema: dashboardCreateOptionsSchema, }, data: { - schema: dashboardAttributesSchema, + schema: getDashboardAttributesSchema(embeddable), }, }, out: { result: { - schema: dashboardCreateResultSchema, + schema: getDashboardCreateResultSchema(embeddable), }, }, }, @@ -518,7 +578,7 @@ export const serviceDefinition: ServicesDefinition = { schema: dashboardUpdateOptionsSchema, }, data: { - schema: dashboardAttributesSchema, + schema: getDashboardAttributesSchema(embeddable), }, }, }, @@ -532,8 +592,8 @@ export const serviceDefinition: ServicesDefinition = { mSearch: { out: { result: { - schema: dashboardItemSchema, + schema: getDashboardItemSchema(embeddable), }, }, }, -}; +}); diff --git a/src/plugins/dashboard/server/content_management/v3/index.ts b/src/plugins/dashboard/server/content_management/v3/index.ts index 7be9313c3210e..de966210eac86 100644 --- a/src/plugins/dashboard/server/content_management/v3/index.ts +++ b/src/plugins/dashboard/server/content_management/v3/index.ts @@ -27,12 +27,12 @@ export type { DashboardOptions, } from './types'; export { - serviceDefinition, - dashboardAttributesSchema, - dashboardGetResultSchema, - dashboardCreateResultSchema, - dashboardItemSchema, - dashboardSearchResultsSchema, + getServiceDefinition, + getDashboardAttributesSchema, + getDashboardGetResultSchema, + getDashboardCreateResultSchema, + getDashboardItemSchema, + getDashboardSearchResultsSchema, referenceSchema, } from './cm_services'; export { diff --git a/src/plugins/dashboard/server/content_management/v3/types.ts b/src/plugins/dashboard/server/content_management/v3/types.ts index 36f277ff3b268..c15af0883e2f9 100644 --- a/src/plugins/dashboard/server/content_management/v3/types.ts +++ b/src/plugins/dashboard/server/content_management/v3/types.ts @@ -17,18 +17,18 @@ import { } from '@kbn/content-management-plugin/common'; import { SavedObjectReference } from '@kbn/core-saved-objects-api-server'; import { - dashboardItemSchema, controlGroupInputSchema, - gridDataSchema, - panelSchema, - dashboardAttributesSchema, + getDashboardAttributesSchema, dashboardCreateOptionsSchema, - dashboardCreateResultSchema, - dashboardGetResultSchema, + getDashboardCreateResultSchema, + getDashboardGetResultSchema, + getDashboardItemSchema, dashboardSearchOptionsSchema, - dashboardSearchResultsSchema, + getDashboardSearchResultsSchema, dashboardUpdateOptionsSchema, + gridDataSchema, optionsSchema, + getPanelSchema, } from './cm_services'; import { CONTENT_ID } from '../../../common/content_management'; import { DashboardSavedObjectAttributes } from '../../dashboard_saved_object'; @@ -38,14 +38,17 @@ export type DashboardOptions = TypeOf; // Panel config has some defined types but also allows for custom keys added by embeddables // The schema uses "unknowns: 'allow'" to permit any other keys, but the TypeOf helper does not // recognize this, so we need to manually extend the type here. -export type DashboardPanel = Omit, 'panelConfig'> & { - panelConfig: TypeOf['panelConfig'] & { [key: string]: any }; +export type DashboardPanel = Omit>, 'panelConfig'> & { + panelConfig: TypeOf>['panelConfig'] & { [key: string]: any }; }; -export type DashboardAttributes = Omit, 'panels'> & { +export type DashboardAttributes = Omit< + TypeOf>, + 'panels' +> & { panels: DashboardPanel[]; }; -export type DashboardItem = TypeOf; +export type DashboardItem = TypeOf>; export type PartialDashboardItem = Omit & { attributes: Partial; references: SavedObjectReference[] | undefined; @@ -55,19 +58,21 @@ export type ControlGroupAttributes = TypeOf; export type GridData = TypeOf; export type DashboardGetIn = GetIn; -export type DashboardGetOut = TypeOf; +export type DashboardGetOut = TypeOf>; export type DashboardCreateIn = CreateIn; -export type DashboardCreateOut = TypeOf; +export type DashboardCreateOut = TypeOf>; export type DashboardCreateOptions = TypeOf; export type DashboardUpdateIn = UpdateIn>; -export type DashboardUpdateOut = TypeOf; +export type DashboardUpdateOut = TypeOf>; export type DashboardUpdateOptions = TypeOf; export type DashboardSearchIn = SearchIn; export type DashboardSearchOptions = TypeOf; -export type DashboardSearchOut = SearchResult>; +export type DashboardSearchOut = SearchResult< + TypeOf> +>; export type SavedObjectToItemReturn = | { diff --git a/src/plugins/dashboard/server/plugin.ts b/src/plugins/dashboard/server/plugin.ts index e3d67ca10716b..5a937058efa08 100644 --- a/src/plugins/dashboard/server/plugin.ts +++ b/src/plugins/dashboard/server/plugin.ts @@ -11,7 +11,7 @@ import { TaskManagerSetupContract, TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; -import { EmbeddableSetup } from '@kbn/embeddable-plugin/server'; +import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/server'; import { UsageCollectionSetup, UsageCollectionStart } from '@kbn/usage-collection-plugin/server'; import { ContentManagementServerSetup } from '@kbn/content-management-plugin/server'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server'; @@ -42,6 +42,7 @@ interface SetupDeps { interface StartDeps { taskManager: TaskManagerStartContract; usageCollection?: UsageCollectionStart; + embeddable: EmbeddableStart; } export class DashboardPlugin @@ -63,10 +64,10 @@ export class DashboardPlugin }, }) ); - plugins.contentManagement.register({ id: CONTENT_ID, storage: new DashboardStorage({ + core, throwOnResultValidationError: this.initializerContext.env.mode.dev, logger: this.logger.get('storage'), }), @@ -112,10 +113,13 @@ export class DashboardPlugin core.uiSettings.register(getUISettings()); - registerAPIRoutes({ - http: core.http, - contentManagement: plugins.contentManagement, - logger: this.logger, + core.getStartServices().then(([_, { embeddable }]) => { + registerAPIRoutes({ + http: core.http, + contentManagement: plugins.contentManagement, + logger: this.logger, + embeddable, + }); }); return {}; diff --git a/src/plugins/embeddable/common/types.ts b/src/plugins/embeddable/common/types.ts index 951ecd9026ded..44c6cbee71fd5 100644 --- a/src/plugins/embeddable/common/types.ts +++ b/src/plugins/embeddable/common/types.ts @@ -7,6 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import type { Type } from '@kbn/config-schema'; import type { SerializableRecord } from '@kbn/utility-types'; import type { KibanaExecutionContext } from '@kbn/core/public'; import type { @@ -92,6 +93,7 @@ export interface EmbeddableRegistryDefinition< P extends EmbeddableStateWithType = EmbeddableStateWithType > extends PersistableStateDefinition

{ id: string; + getSchema: () => Type; } export type EmbeddablePersistableStateService = PersistableStateService; diff --git a/src/plugins/embeddable/server/plugin.ts b/src/plugins/embeddable/server/plugin.ts index 12424a15e1733..0b8c59fb84893 100644 --- a/src/plugins/embeddable/server/plugin.ts +++ b/src/plugins/embeddable/server/plugin.ts @@ -7,7 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { SerializableRecord } from '@kbn/utility-types'; +import { schema, type Type } from '@kbn/config-schema'; +import type { SerializableRecord, WithRequiredProperty } from '@kbn/utility-types'; import { CoreSetup, CoreStart, Plugin } from '@kbn/core/server'; import { identity } from 'lodash'; import { @@ -20,6 +21,7 @@ import { EnhancementsRegistry, EnhancementRegistryDefinition, EnhancementRegistryItem, + EmbeddableRegistryItem, } from './types'; import { getExtractFunction, @@ -38,6 +40,7 @@ export interface EmbeddableSetup extends PersistableStateService void; registerEnhancement: (enhancement: EnhancementRegistryDefinition) => void; getAllMigrations: () => MigrateFunctionsObject; + getValidationSchema: (embeddableId: string) => Type; } export type EmbeddableStart = PersistableStateService; @@ -87,6 +90,20 @@ export class EmbeddableServerPlugin implements Plugin { + const factory = this.embeddableFactories.get(embeddableId); + if (!factory || !factory.getSchema) { + return; + } + return factory.getSchema; + }, + getAllValidationSchemas: () => { + const schemas = Array.from(this.embeddableFactories.values()) + .filter((factory) => factory.getSchema) + .map((factory) => factory.getSchema); + return schemas; + }, + getRegisteredEmbeddableFactories: () => Array.from(this.embeddableFactories.keys()), }; } @@ -135,6 +152,7 @@ export class EmbeddableServerPlugin implements Plugin ({ state, references: [] })), migrations: factory.migrations || {}, + getSchema: factory.getSchema, }); }; diff --git a/src/plugins/embeddable/server/types.ts b/src/plugins/embeddable/server/types.ts index 213a6537cde6c..7813ed40e3f4d 100644 --- a/src/plugins/embeddable/server/types.ts +++ b/src/plugins/embeddable/server/types.ts @@ -7,6 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import type { Type } from '@kbn/config-schema'; import type { SerializableRecord } from '@kbn/utility-types'; import { PersistableState, PersistableStateDefinition } from '@kbn/kibana-utils-plugin/common'; import { EmbeddableStateWithType } from '../common/types'; @@ -27,4 +28,5 @@ export interface EnhancementRegistryItem

extends PersistableState

{ id: string; + getSchema?: () => Type; } diff --git a/src/plugins/kibana_utils/common/persistable_state/types.ts b/src/plugins/kibana_utils/common/persistable_state/types.ts index f9bbe1f8665a5..3761c14c3efa2 100644 --- a/src/plugins/kibana_utils/common/persistable_state/types.ts +++ b/src/plugins/kibana_utils/common/persistable_state/types.ts @@ -7,6 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import type { Type } from '@kbn/config-schema'; import type { SerializableRecord, Serializable } from '@kbn/utility-types'; import { SavedObjectReference } from '@kbn/core/types'; diff --git a/src/plugins/links/server/plugin.ts b/src/plugins/links/server/plugin.ts index c2d374f8d1e3c..2ff962d29e446 100644 --- a/src/plugins/links/server/plugin.ts +++ b/src/plugins/links/server/plugin.ts @@ -9,6 +9,8 @@ import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from '@kbn/core/server'; import type { ContentManagementServerSetup } from '@kbn/content-management-plugin/server'; +import { EmbeddableSetup } from '@kbn/embeddable-plugin/server'; +import { schema } from '@kbn/config-schema'; import { CONTENT_ID, LATEST_VERSION } from '../common'; import { LinksAttributes } from '../common/content_management'; import { LinksStorage } from './content_management'; @@ -25,6 +27,7 @@ export class LinksServerPlugin implements Plugin { core: CoreSetup, plugins: { contentManagement: ContentManagementServerSetup; + embeddable: EmbeddableSetup; } ) { plugins.contentManagement.register({ @@ -38,6 +41,30 @@ export class LinksServerPlugin implements Plugin { }, }); + plugins.embeddable.registerEmbeddableFactory({ + id: CONTENT_ID, + getSchema: () => + schema.object({ + attributes: schema.object({ + layout: schema.maybe( + schema.oneOf([schema.literal('horizontal'), schema.literal('vertical')]) + ), + links: schema.arrayOf( + schema.object({ + type: schema.oneOf([ + schema.literal('dashboardLink'), + schema.literal('externalLink'), + ]), + id: schema.string(), + order: schema.number(), + destinationRefName: schema.maybe(schema.string()), + destination: schema.maybe(schema.string()), + }) + ), + }), + }), + }); + core.savedObjects.registerType(linksSavedObjectType); return {}; diff --git a/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts index 9262b6054af09..c475c10cc8c69 100644 --- a/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts +++ b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts @@ -52,6 +52,7 @@ import { XYVisState850, } from '../migrations/types'; import { extract, inject } from '../../common/embeddable_factory'; +import { getSchema } from './schema'; export const makeLensEmbeddableFactory = ( @@ -186,5 +187,6 @@ export const makeLensEmbeddableFactory = ), extract, inject, + getSchema, }; }; diff --git a/x-pack/plugins/lens/server/embeddable/schema/index.ts b/x-pack/plugins/lens/server/embeddable/schema/index.ts new file mode 100644 index 0000000000000..2001b5988cd27 --- /dev/null +++ b/x-pack/plugins/lens/server/embeddable/schema/index.ts @@ -0,0 +1,8 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getSchema } from './latest'; diff --git a/x-pack/plugins/lens/server/embeddable/schema/latest.ts b/x-pack/plugins/lens/server/embeddable/schema/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/lens/server/embeddable/schema/latest.ts @@ -0,0 +1,8 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './v1'; diff --git a/x-pack/plugins/lens/server/embeddable/schema/v1/common.ts b/x-pack/plugins/lens/server/embeddable/schema/v1/common.ts new file mode 100644 index 0000000000000..be985c98028e8 --- /dev/null +++ b/x-pack/plugins/lens/server/embeddable/schema/v1/common.ts @@ -0,0 +1,209 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ObjectType, schema } from '@kbn/config-schema'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; + +import { FilterStateStore } from '@kbn/es-query'; +import { formBasedLayerSchema } from './data_sources'; + +const referenceSchema = schema.object( + { + name: schema.string(), + type: schema.string(), + id: schema.string(), + }, + { unknowns: 'forbid' } +); + +const lensGenericAttributesStateSchema = schema.object({ + datasourceStates: schema.recordOf(schema.string(), schema.any()), + visualization: schema.any(), + query: schema.object({ + query: schema.oneOf([ + schema.string({ + meta: { + description: + 'A text-based query such as Kibana Query Language (KQL) or Lucene query language.', + }, + }), + schema.recordOf(schema.string(), schema.any()), + ]), + language: schema.string({ + meta: { description: 'The query language such as KQL or Lucene.' }, + }), + }), + globalPalette: schema.maybe( + schema.object({ + activePaletteId: schema.string(), + state: schema.maybe(schema.any()), + }) + ), + filters: schema.arrayOf( + schema.object( + { + meta: schema.object( + { + alias: schema.maybe(schema.nullable(schema.string())), + disabled: schema.maybe(schema.boolean()), + negate: schema.maybe(schema.boolean()), + controlledBy: schema.maybe(schema.string()), + group: schema.maybe(schema.string()), + index: schema.maybe(schema.string()), + isMultiIndex: schema.maybe(schema.boolean()), + type: schema.maybe(schema.string()), + key: schema.maybe(schema.string()), + params: schema.maybe(schema.any()), + value: schema.maybe(schema.string()), + field: schema.maybe(schema.string()), + }, + { unknowns: 'allow' } + ), + query: schema.maybe(schema.recordOf(schema.string(), schema.any())), + $state: schema.maybe( + schema.object({ + store: schema.oneOf( + [ + schema.literal(FilterStateStore.APP_STATE), + schema.literal(FilterStateStore.GLOBAL_STATE), + ], + { + meta: { + description: + "Denote whether a filter is specific to an application's context (e.g. 'appState') or whether it should be applied globally (e.g. 'globalState').", + }, + } + ), + }) + ), + }, + { meta: { description: 'A filter for the search source.' } } + ) + ), + adHocDataViews: schema.maybe(schema.recordOf(schema.string(), schema.any())), + internalReferences: schema.maybe(schema.arrayOf(referenceSchema)), +}); + +const metaType = (Object.keys(KBN_FIELD_TYPES) as Array).map((key) => + schema.literal(KBN_FIELD_TYPES[key]) +); + +const datatableColumnSchema = schema.object({ + id: schema.string(), + name: schema.string(), + meta: schema.object({ + type: schema.oneOf(metaType as [(typeof metaType)[number]]), + esType: schema.maybe(schema.string()), + field: schema.maybe(schema.string()), + index: schema.maybe(schema.string()), + dimensionName: schema.maybe(schema.string()), + params: schema.maybe(schema.any()), + source: schema.maybe(schema.string()), + sourceParams: schema.maybe( + schema.object({ + id: schema.maybe(schema.string()), + params: schema.maybe(schema.recordOf(schema.string(), schema.any())), + }) + ), + }), + isNull: schema.maybe(schema.boolean()), +}); + +const paletteSchema = schema.object({ + type: schema.oneOf([schema.literal('palette'), schema.literal('system_palette')]), + name: schema.string(), + params: schema.maybe(schema.any()), +}); + +export const columnStateSchema = schema.object({ + columnId: schema.string(), + width: schema.maybe(schema.number()), + hidden: schema.maybe(schema.boolean()), + oneClickFilter: schema.maybe(schema.boolean()), + isTransposed: schema.maybe(schema.boolean()), + transposable: schema.maybe(schema.boolean()), + originalColumnId: schema.maybe(schema.string()), + originalName: schema.maybe(schema.string()), + bucketValues: schema.maybe( + schema.arrayOf( + schema.object({ + originalBucketColumn: datatableColumnSchema, + value: schema.any(), + }) + ) + ), + alignment: schema.maybe( + schema.oneOf([schema.literal('left'), schema.literal('right'), schema.literal('center')]) + ), + palette: schema.maybe(paletteSchema), + colorMapping: schema.maybe(schema.any()), + colorMode: schema.maybe( + schema.oneOf([schema.literal('none'), schema.literal('cell'), schema.literal('text')]) + ), + summaryRow: schema.maybe( + schema.oneOf([ + schema.literal('none'), + schema.literal('sum'), + schema.literal('avg'), + schema.literal('count'), + schema.literal('min'), + schema.literal('max'), + ]) + ), + summaryLabel: schema.maybe(schema.string()), + collapseFn: schema.maybe( + schema.oneOf([ + schema.literal('sum'), + schema.literal('avg'), + schema.literal('min'), + schema.literal('max'), + ]) + ), + isMetric: schema.maybe(schema.boolean()), +}); + +export const rowHeightModeSchema = schema.oneOf([ + schema.literal('auto'), + schema.literal('single'), + schema.literal('custom'), +]); + +export const sortingStateSchema = schema.object({ + columnId: schema.maybe(schema.string()), + direction: schema.oneOf([schema.literal('asc'), schema.literal('desc'), schema.literal('none')]), +}); + +export const lensGenericAttributesSchema = schema.object({ + title: schema.maybe(schema.string()), + type: schema.maybe(schema.string()), + description: schema.maybe(schema.string()), + references: schema.arrayOf(referenceSchema), +}); + +export const getLensAttributesSchema = (visType: string, visState: ObjectType) => + lensGenericAttributesSchema.extends({ + visualizationType: schema.oneOf([schema.literal(visType)], { + meta: { description: 'The type of visualization.' }, + }), + state: lensGenericAttributesStateSchema.extends({ + datasourceStates: schema.object({ + formBased: schema.maybe( + schema.object({ + layers: schema.recordOf(schema.string(), formBasedLayerSchema), + }) + ), + textBased: schema.maybe( + schema.object({ + // TODO add schema for text based datasource to ./text_based.ts + layers: schema.recordOf(schema.string(), schema.any()), + initialContext: schema.maybe(schema.any()), + }) + ), + }), + visualization: visState, + }), + }); diff --git a/x-pack/plugins/lens/server/embeddable/schema/v1/data_sources/form_based.ts b/x-pack/plugins/lens/server/embeddable/schema/v1/data_sources/form_based.ts new file mode 100644 index 0000000000000..f97d15ac95d45 --- /dev/null +++ b/x-pack/plugins/lens/server/embeddable/schema/v1/data_sources/form_based.ts @@ -0,0 +1,180 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +const fieldOnlyDataTypeSchema = schema.oneOf([ + schema.literal('document'), + schema.literal('ip'), + schema.literal('histogram'), + schema.literal('geo_point'), + schema.literal('geo_shape'), + schema.literal('counter'), + schema.literal('gauge'), + schema.literal('murmur3'), +]); + +const dataTypeSchema = schema.oneOf([ + schema.literal('string'), + schema.literal('number'), + schema.literal('date'), + schema.literal('boolean'), + fieldOnlyDataTypeSchema, +]); + +const operationMetadataSchema = schema.object({ + interval: schema.maybe(schema.string()), + dataType: dataTypeSchema, + isBucketed: schema.boolean(), + scale: schema.maybe( + schema.oneOf([schema.literal('ordinal'), schema.literal('interval'), schema.literal('ratio')]) + ), + isStaticValue: schema.maybe(schema.boolean()), +}); + +const operationSchema = operationMetadataSchema.extends({ + label: schema.maybe(schema.string()), + sortingHint: schema.maybe(schema.string()), +}); + +const baseIndexPatternColumnSchema = operationSchema.extends({ + operationType: schema.string(), + customLabel: schema.maybe(schema.string()), + timeScale: schema.maybe(schema.any()), + filter: schema.maybe(schema.any()), + reducedTimeRange: schema.maybe(schema.string()), + timeShift: schema.maybe(schema.string()), +}); + +const valueFormatConfigSchema = schema.object({ + id: schema.string(), + params: schema.maybe( + schema.object({ + decimals: schema.number(), + suffix: schema.maybe(schema.string()), + compact: schema.maybe(schema.boolean()), + pattern: schema.maybe(schema.string()), + fromUnit: schema.maybe(schema.string()), + toUnit: schema.maybe(schema.string()), + }) + ), +}); + +const fieldBasedIndexPatternColumnSchema = baseIndexPatternColumnSchema.extends({ + sourceField: schema.string(), +}); + +const countIndexPatternColumnSchema = fieldBasedIndexPatternColumnSchema.extends({ + operationType: schema.literal('count'), + params: schema.object({ + emptyAsNull: schema.maybe(schema.boolean()), + format: schema.maybe(valueFormatConfigSchema), + }), +}); + +const getMetricColumnSchema = (operationType: string) => + fieldBasedIndexPatternColumnSchema.extends({ + operationType: schema.literal(operationType), + params: schema.maybe( + schema.object({ + emptyAsNull: schema.maybe(schema.boolean()), + format: schema.maybe(valueFormatConfigSchema), + }) + ), + }); + +const sumIndexPatternColumnSchema = getMetricColumnSchema('sum'); +const avgIndexPatternColumnSchema = getMetricColumnSchema('average'); +const minIndexPatternColumnSchema = getMetricColumnSchema('min'); +const maxIndexPatternColumnSchema = getMetricColumnSchema('max'); +const medianIndexPatternColumnSchema = getMetricColumnSchema('median'); +const standardDeviationIndexPatternColumnSchema = getMetricColumnSchema('standard_deviation'); + +const formattedIndexPatternColumnSchema = baseIndexPatternColumnSchema.extends({ + params: schema.maybe( + schema.object({ + format: schema.maybe(valueFormatConfigSchema), + }) + ), +}); + +const termsIndexPatternColumnSchema = fieldBasedIndexPatternColumnSchema.extends({ + operationType: schema.literal('terms'), + params: schema.object({ + size: schema.number(), + accuracyMode: schema.maybe(schema.boolean()), + include: schema.maybe(schema.arrayOf(schema.oneOf([schema.string(), schema.number()]))), + exclude: schema.maybe(schema.arrayOf(schema.oneOf([schema.string(), schema.number()]))), + includeIsRegex: schema.maybe(schema.boolean()), + excludeIsRegex: schema.maybe(schema.boolean()), + orderBy: schema.oneOf([ + schema.object({ + type: schema.literal('alphabetical'), + fallback: schema.maybe(schema.boolean()), + }), + schema.object({ + type: schema.literal('rare'), + maxDocCount: schema.number(), + }), + schema.literal('significant'), + schema.object({ + type: schema.literal('column'), + columnId: schema.string(), + }), + schema.literal('custom'), + ]), + orderAgg: schema.maybe(fieldBasedIndexPatternColumnSchema), + orderDirection: schema.maybe(schema.oneOf([schema.literal('asc'), schema.literal('desc')])), + otherBucket: schema.maybe(schema.boolean()), + missingBucket: schema.maybe(schema.boolean()), + secondaryFields: schema.maybe(schema.arrayOf(schema.string())), + format: schema.maybe(valueFormatConfigSchema), + parentFormat: schema.maybe(schema.object({ id: schema.string() })), + }), +}); + +const referenceBasedIndexPatternColumnSchema = formattedIndexPatternColumnSchema.extends({ + references: schema.arrayOf(schema.string()), +}); + +const genericIndexPatternColumnSchema = schema.oneOf([ + baseIndexPatternColumnSchema, + fieldBasedIndexPatternColumnSchema, + referenceBasedIndexPatternColumnSchema, +]); + +const incompleteColumnSchema = schema.object({ + operationType: schema.maybe(schema.string()), + sourceField: schema.maybe(schema.string()), +}); + +export const formBasedLayerSchema = schema.object({ + columnOrder: schema.arrayOf(schema.string()), + columns: schema.recordOf( + schema.string(), + schema.oneOf([ + termsIndexPatternColumnSchema, + countIndexPatternColumnSchema, + sumIndexPatternColumnSchema, + avgIndexPatternColumnSchema, + minIndexPatternColumnSchema, + maxIndexPatternColumnSchema, + medianIndexPatternColumnSchema, + standardDeviationIndexPatternColumnSchema, + genericIndexPatternColumnSchema, + ]) + ), + // TODO indexPatternId should be required, but we make it optional since it might be + // specified in the state.references array via the HTTP endpoint. + indexPatternId: schema.maybe(schema.string()), + linkToLayers: schema.maybe(schema.arrayOf(schema.string())), + incompleteColumns: schema.maybe( + schema.recordOf(schema.string(), schema.maybe(incompleteColumnSchema)) + ), + sampling: schema.maybe(schema.number()), + ignoreGlobalFilters: schema.maybe(schema.boolean()), +}); diff --git a/x-pack/plugins/lens/server/embeddable/schema/v1/data_sources/index.ts b/x-pack/plugins/lens/server/embeddable/schema/v1/data_sources/index.ts new file mode 100644 index 0000000000000..369cd14e390d5 --- /dev/null +++ b/x-pack/plugins/lens/server/embeddable/schema/v1/data_sources/index.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { formBasedLayerSchema } from './form_based'; +export { indexPatternSchema } from './index_pattern'; diff --git a/x-pack/plugins/lens/server/embeddable/schema/v1/data_sources/index_pattern.ts b/x-pack/plugins/lens/server/embeddable/schema/v1/data_sources/index_pattern.ts new file mode 100644 index 0000000000000..7965c5353597c --- /dev/null +++ b/x-pack/plugins/lens/server/embeddable/schema/v1/data_sources/index_pattern.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { formBasedLayerSchema } from './form_based'; + +export const indexPatternSchema = formBasedLayerSchema.extends({ + indexPatternId: undefined, +}); diff --git a/x-pack/plugins/lens/server/embeddable/schema/v1/data_sources/text_based.ts b/x-pack/plugins/lens/server/embeddable/schema/v1/data_sources/text_based.ts new file mode 100644 index 0000000000000..53c3add276bed --- /dev/null +++ b/x-pack/plugins/lens/server/embeddable/schema/v1/data_sources/text_based.ts @@ -0,0 +1,8 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// TODO diff --git a/x-pack/plugins/lens/server/embeddable/schema/v1/index.ts b/x-pack/plugins/lens/server/embeddable/schema/v1/index.ts new file mode 100644 index 0000000000000..d24d03351db3f --- /dev/null +++ b/x-pack/plugins/lens/server/embeddable/schema/v1/index.ts @@ -0,0 +1,36 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AnyType, ConditionalType, Type, schema } from '@kbn/config-schema'; +import { datatableVisualizationStateSchema } from './visualization_state/data_table'; +import { getLensAttributesSchema } from './common'; + +function buildConditionalSchema(conditions: Array<{ type: string; schema: Type }>) { + return conditions.reduce, Type> | AnyType>( + (acc, condition) => { + return schema.conditional( + schema.siblingRef('.visualizationType'), + condition.type, + condition.schema, + acc + ); + }, + // Fall back to no validation. Change to schema.never() to enforce validation. + schema.any() + ); +} + +export const getSchema = () => + schema.object({ + attributes: buildConditionalSchema([ + { + type: 'lnsDatatable', + schema: getLensAttributesSchema('lnsDatatable', datatableVisualizationStateSchema), + }, + // TODO create schemas for other visualization types + ]), + }); diff --git a/x-pack/plugins/lens/server/embeddable/schema/v1/visualization_state/data_table.ts b/x-pack/plugins/lens/server/embeddable/schema/v1/visualization_state/data_table.ts new file mode 100644 index 0000000000000..6119db25528c7 --- /dev/null +++ b/x-pack/plugins/lens/server/embeddable/schema/v1/visualization_state/data_table.ts @@ -0,0 +1,30 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Type, schema } from '@kbn/config-schema'; +import { columnStateSchema, rowHeightModeSchema, sortingStateSchema } from '../common'; +import { LayerType } from '../../../../../common/types'; +import { layerTypes } from '../../../../../common/layer_types'; + +const pagingStateSchema = schema.object({ + size: schema.number(), + enabled: schema.boolean(), +}); + +export const datatableVisualizationStateSchema = schema.object({ + columns: schema.arrayOf(columnStateSchema), + layerId: schema.string(), + layerType: schema.oneOf( + Object.values(layerTypes).map((value) => schema.literal(value)) as [Type] + ), + sorting: schema.maybe(sortingStateSchema), + rowHeight: schema.maybe(rowHeightModeSchema), + headerRowHeight: schema.maybe(rowHeightModeSchema), + rowHeightLines: schema.maybe(schema.number()), + headerRowHeightLines: schema.maybe(schema.number()), + paging: schema.maybe(pagingStateSchema), +});