[embeddable] remove embeddable factory methods from setup and start API (#204797)

Part of embeddable rebuild cleanup. PR removes legacy embeddable factory
registration APIs.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2024-12-19 11:19:48 -07:00 committed by GitHub
parent b80980694a
commit 3b61e7bea7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 173 additions and 1083 deletions

View file

@ -10,8 +10,7 @@
import { useCallback, useMemo, useRef } from 'react';
import { AsyncSubject, defer, from, lastValueFrom, map, type Subscription } from 'rxjs';
import type { IconType } from '@elastic/eui';
import { COMMON_EMBEDDABLE_GROUPING, EmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { COMMON_EMBEDDABLE_GROUPING } from '@kbn/embeddable-plugin/public';
import { PresentationContainer } from '@kbn/presentation-containers';
import { ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public';
import { VisGroups, type BaseVisType, type VisTypeAlias } from '@kbn/visualizations-plugin/public';
@ -28,14 +27,6 @@ interface UseGetDashboardPanelsArgs {
createNewVisType: (visType: BaseVisType | VisTypeAlias) => () => void;
}
export interface FactoryGroup {
id: string;
appName: string;
icon?: IconType;
factories: EmbeddableFactory[];
order: number;
}
const sortGroupPanelsByOrder = <T extends { order: number }>(panelGroups: T[]): T[] => {
return panelGroups.sort(
// larger number sorted to the top

View file

@ -13,13 +13,8 @@ import { buildMockDashboardApi } from '../../mocks';
import { EditorMenu } from './editor_menu';
import { DashboardContext } from '../../dashboard_api/use_dashboard_api';
import {
embeddableService,
uiActionsService,
visualizationsService,
} from '../../services/kibana_services';
import { uiActionsService, visualizationsService } from '../../services/kibana_services';
jest.spyOn(embeddableService, 'getEmbeddableFactories').mockReturnValue(new Map().values());
jest.spyOn(uiActionsService, 'getTriggerCompatibleActions').mockResolvedValue([]);
jest.spyOn(visualizationsService, 'getByGroup').mockReturnValue([]);
jest.spyOn(visualizationsService, 'getAliases').mockReturnValue([]);

View file

@ -13,7 +13,7 @@ import type { ContentManagementPublicStart } from '@kbn/content-management-plugi
import type { CoreStart } from '@kbn/core/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { EmbeddableStart } from '@kbn/embeddable-plugin/public/plugin';
import type { EmbeddableStart } from '@kbn/embeddable-plugin/public';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public/plugin';
import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public';

View file

@ -14,7 +14,7 @@ import { extractBaseEmbeddableInput } from './migrate_base_input';
export const getExtractFunction = (embeddables: CommonEmbeddableStartContract) => {
return (state: EmbeddableStateWithType) => {
const enhancements = state.enhancements || {};
const factory = embeddables.getEmbeddableFactory(state.type);
const factory = embeddables.getEmbeddableFactory?.(state.type);
const baseResponse = extractBaseEmbeddableInput(state);
let updatedInput = baseResponse.state;

View file

@ -15,7 +15,7 @@ import { injectBaseEmbeddableInput } from './migrate_base_input';
export const getInjectFunction = (embeddables: CommonEmbeddableStartContract) => {
return (state: EmbeddableStateWithType, references: SavedObjectReference[]) => {
const enhancements = state.enhancements || {};
const factory = embeddables.getEmbeddableFactory(state.type);
const factory = embeddables.getEmbeddableFactory?.(state.type);
let updatedInput = injectBaseEmbeddableInput(state, references);

View file

@ -16,7 +16,7 @@ export type MigrateFunction = (state: SerializableRecord, version: string) => Se
export const getMigrateFunction = (embeddables: CommonEmbeddableStartContract) => {
const migrateFn: MigrateFunction = (state: SerializableRecord, version: string) => {
const enhancements = (state.enhancements as SerializableRecord) || {};
const factory = embeddables.getEmbeddableFactory(state.type as string);
const factory = embeddables.getEmbeddableFactory?.(state.type as string);
let updatedInput = baseEmbeddableMigrations[version]
? baseEmbeddableMigrations[version](state)

View file

@ -17,7 +17,7 @@ export const getTelemetryFunction = (embeddables: CommonEmbeddableStartContract)
telemetryData: Record<string, string | number | boolean> = {}
) => {
const enhancements = state.enhancements || {};
const factory = embeddables.getEmbeddableFactory(state.type);
const factory = embeddables.getEmbeddableFactory?.(state.type);
let outputTelemetryData = telemetryBaseEmbeddableInput(state, telemetryData);
if (factory) {

View file

@ -97,7 +97,7 @@ export interface EmbeddableRegistryDefinition<
export type EmbeddablePersistableStateService = PersistableStateService<EmbeddableStateWithType>;
export interface CommonEmbeddableStartContract {
getEmbeddableFactory: (
getEmbeddableFactory?: (
embeddableFactoryId: string
) => PersistableState & { isContainerType: boolean };
getEnhancement: (enhancementId: string) => PersistableState;

View file

@ -1,8 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`embeddable factory migrateToLatest returns list of all migrations 1`] = `
Object {
"7.11.0": [Function],
"7.12.0": [Function],
}
`;

View file

@ -27,7 +27,6 @@ import {
contentManagement,
usageCollection,
} from '../kibana_services';
import { EmbeddableFactoryNotFoundError } from '../lib';
import { getAddFromLibraryType, useAddFromLibraryTypes } from './registry';
const runAddTelemetry = (
@ -61,7 +60,12 @@ export const AddFromLibraryFlyout = ({
) => {
const libraryType = getAddFromLibraryType(type);
if (!libraryType) {
core.notifications.toasts.addWarning(new EmbeddableFactoryNotFoundError(type).message);
core.notifications.toasts.addWarning(
i18n.translate('embeddableApi.addPanel.typeNotFound', {
defaultMessage: 'Unable to load type: {type}',
values: { type },
})
);
return;
}

View file

@ -0,0 +1,51 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { identity } from 'lodash';
import { SerializableRecord } from '@kbn/utility-types';
import { EnhancementRegistryDefinition, EnhancementRegistryItem } from './types';
export class EnhancementsRegistry {
private registry: Map<string, EnhancementRegistryItem> = new Map();
public registerEnhancement = (enhancement: EnhancementRegistryDefinition) => {
if (this.registry.has(enhancement.id)) {
throw new Error(`enhancement with id ${enhancement.id} already exists in the registry`);
}
this.registry.set(enhancement.id, {
id: enhancement.id,
telemetry: enhancement.telemetry || ((state, stats) => stats),
inject: enhancement.inject || identity,
extract:
enhancement.extract ||
((state: SerializableRecord) => {
return { state, references: [] };
}),
migrations: enhancement.migrations || {},
});
};
public getEnhancements = (): EnhancementRegistryItem[] => {
return Array.from(this.registry.values());
};
public getEnhancement = (id: string): EnhancementRegistryItem => {
return (
this.registry.get(id) || {
id: 'unknown',
telemetry: (state, stats) => stats,
inject: identity,
extract: (state: SerializableRecord) => {
return { state, references: [] };
},
migrations: {},
}
);
};
}

View file

@ -0,0 +1,21 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { SerializableRecord } from '@kbn/utility-types';
import { PersistableState, PersistableStateDefinition } from '@kbn/kibana-utils-plugin/common';
export interface EnhancementRegistryDefinition<P extends SerializableRecord = SerializableRecord>
extends PersistableStateDefinition<P> {
id: string;
}
export interface EnhancementRegistryItem<P extends SerializableRecord = SerializableRecord>
extends PersistableState<P> {
id: string;
}

View file

@ -17,13 +17,10 @@ export {
CELL_VALUE_TRIGGER,
contextMenuTrigger,
CONTEXT_MENU_TRIGGER,
defaultEmbeddableFactoryProvider,
Embeddable,
EmbeddableFactoryNotFoundError,
EmbeddableStateTransfer,
ErrorEmbeddable,
isContextMenuTriggerContext,
isExplicitInputWithAttributes,
isMultiValueClickTriggerContext,
isRangeSelectTriggerContext,
isRowClickTriggerContext,
@ -37,7 +34,6 @@ export {
PANEL_BADGE_TRIGGER,
PANEL_HOVER_TRIGGER,
PANEL_NOTIFICATION_TRIGGER,
runEmbeddableFactoryMigrations,
SELECT_RANGE_TRIGGER,
VALUE_CLICK_TRIGGER,
ViewMode,
@ -47,26 +43,17 @@ export type {
ChartActionContext,
EmbeddableContext,
EmbeddableEditorState,
EmbeddableFactory,
EmbeddableFactoryDefinition,
EmbeddableInput,
EmbeddableInstanceConfiguration,
EmbeddableOutput,
EmbeddablePackageState,
IEmbeddable,
MultiValueClickContext,
OutputSpec,
PropertySpec,
RangeSelectContext,
ValueClickContext,
} from './lib';
export type {
EmbeddableSetup,
EmbeddableSetupDependencies,
EmbeddableStart,
EmbeddableStartDependencies,
} from './plugin';
export type { EnhancementRegistryDefinition } from './types';
export type { EmbeddableSetup, EmbeddableStart } from './types';
export type { EnhancementRegistryDefinition } from './enhancements/types';
export {
ReactEmbeddableRenderer,

View file

@ -11,7 +11,7 @@ import { BehaviorSubject } from 'rxjs';
import { CoreStart } from '@kbn/core/public';
import { EmbeddableStart, EmbeddableStartDependencies } from '.';
import { EmbeddableStart, EmbeddableStartDependencies } from './types';
export let core: CoreStart;
export let embeddableStart: EmbeddableStart;

View file

@ -1,65 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { SavedObjectAttributes } from '@kbn/core/public';
import type { FinderAttributes } from '@kbn/saved-objects-finder-plugin/common';
import { EmbeddableFactory } from './embeddable_factory';
import { EmbeddableStateWithType } from '../../../common/types';
import { EmbeddableFactoryDefinition } from './embeddable_factory_definition';
import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';
import { runEmbeddableFactoryMigrations } from '../factory_migrations/run_factory_migrations';
export const defaultEmbeddableFactoryProvider = <
I extends EmbeddableInput = EmbeddableInput,
O extends EmbeddableOutput = EmbeddableOutput,
E extends IEmbeddable<I, O> = IEmbeddable<I, O>,
T extends FinderAttributes = SavedObjectAttributes
>(
def: EmbeddableFactoryDefinition<I, O, E, T>
): EmbeddableFactory<I, O, E, T> => {
if (def.migrations && !def.latestVersion) {
throw new Error(
'To run clientside Embeddable migrations a latest version key is required on the factory'
);
}
const factory: EmbeddableFactory<I, O, E, T> = {
...def,
latestVersion: def.latestVersion,
isContainerType: def.isContainerType ?? false,
canCreateNew: def.canCreateNew ? def.canCreateNew.bind(def) : () => true,
getDefaultInput: def.getDefaultInput ? def.getDefaultInput.bind(def) : () => ({}),
getExplicitInput: def.getExplicitInput
? def.getExplicitInput.bind(def)
: () => Promise.resolve({}),
createFromSavedObject: def.createFromSavedObject
? def.createFromSavedObject.bind(def)
: (savedObjectId: string, input: Partial<I>, parent?: unknown) => {
throw new Error(`Creation from saved object not supported by type ${def.type}`);
},
create: (...args) => {
const [initialInput, ...otherArgs] = args;
const { input } = runEmbeddableFactoryMigrations(initialInput, def);
const createdEmbeddable = def.create.bind(def)(input as I, ...otherArgs);
return createdEmbeddable;
},
type: def.type,
isEditable: def.isEditable.bind(def),
getDisplayName: def.getDisplayName.bind(def),
getDescription: def.getDescription ? def.getDescription.bind(def) : () => '',
getIconType: def.getIconType ? def.getIconType.bind(def) : () => 'empty',
savedObjectMetaData: def.savedObjectMetaData,
telemetry: def.telemetry || ((state, stats) => stats),
inject: def.inject || ((state: EmbeddableStateWithType) => state),
extract: def.extract || ((state: EmbeddableStateWithType) => ({ state, references: [] })),
migrations: def.migrations || {},
grouping: def.grouping,
};
return factory;
};

View file

@ -1,153 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { SavedObjectMetaData } from '@kbn/saved-objects-finder-plugin/public';
import { PersistableState } from '@kbn/kibana-utils-plugin/common';
import type { FinderAttributes } from '@kbn/saved-objects-finder-plugin/common';
import { UiActionsPresentableGrouping } from '@kbn/ui-actions-plugin/public';
import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';
import { ErrorEmbeddable } from './error_embeddable';
import { PropertySpec } from '../types';
import { EmbeddableStateWithType } from '../../../common/types';
export interface EmbeddableInstanceConfiguration {
id: string;
savedObjectId?: string;
}
export interface OutputSpec {
[key: string]: PropertySpec;
}
export interface ExplicitInputWithAttributes {
newInput: Partial<EmbeddableInput>;
attributes?: unknown;
}
export const isExplicitInputWithAttributes = (
value: ExplicitInputWithAttributes | Partial<EmbeddableInput>
): value is ExplicitInputWithAttributes => {
return Boolean((value as ExplicitInputWithAttributes).newInput);
};
/**
* EmbeddableFactories create and initialize an embeddable instance
*/
export interface EmbeddableFactory<
TEmbeddableInput extends EmbeddableInput = EmbeddableInput,
TEmbeddableOutput extends EmbeddableOutput = EmbeddableOutput,
TEmbeddable extends IEmbeddable<TEmbeddableInput, TEmbeddableOutput> = IEmbeddable<
TEmbeddableInput,
TEmbeddableOutput
>,
TSavedObjectAttributes extends FinderAttributes = FinderAttributes
> extends PersistableState<EmbeddableStateWithType> {
/**
* The version of this Embeddable factory. This will be used in the client side migration system
* to ensure that input from any source is compatible with the latest version of this embeddable.
* If the latest version is not defined, all clientside migrations will be skipped. If migrations
* are added to this factory but a latestVersion is not set, an error will be thrown on server start
*/
readonly latestVersion?: string;
// A unique identified for this factory, which will be used to map an embeddable spec to
// a factory that can generate an instance of it.
readonly type: string;
/**
* Returns whether the current user should be allowed to edit this type of
* embeddable. Most of the time this should be based off the capabilities service, hence it's async.
*/
readonly isEditable: () => Promise<boolean>;
readonly savedObjectMetaData?: SavedObjectMetaData<TSavedObjectAttributes>;
/**
* Indicates the grouping this factory should appear in a sub-menu. Example, this is used for grouping
* options in the editors menu in Dashboard for creating new embeddables
*/
readonly grouping?: UiActionsPresentableGrouping;
/**
* True if is this factory create embeddables that are Containers. Used in the add panel to
* conditionally show whether these can be added to another container. It's just not
* supported right now, but once nested containers are officially supported we can probably get
* rid of this interface.
*/
readonly isContainerType: boolean;
/**
* Returns a display name for this type of embeddable. Used in "Create new... " options
* in the add panel for containers.
*/
getDisplayName(): string;
/**
* Returns an EUI Icon type to be displayed in a menu.
*/
getIconType(): string;
/**
* Returns a description about the embeddable.
*/
getDescription(): string;
/**
* If false, this type of embeddable can't be created with the "createNew" functionality. Instead,
* use createFromSavedObject, where an existing saved object must first exist.
*/
canCreateNew(): boolean;
/**
* Can be used to get the default input, to be passed in to during the creation process. Default
* input will not be stored in a parent container, so all inherited input from a container will trump
* default input parameters.
* @param partial
*/
getDefaultInput(partial: Partial<TEmbeddableInput>): Partial<TEmbeddableInput>;
/**
* Can be used to request explicit input from the user, to be passed in to `EmbeddableFactory:create`.
* Explicit input is stored on the parent container for this embeddable. It overrides all inherited
* input passed down from the parent container.
*
* Can be used to edit an embeddable by re-requesting explicit input. Initial input can be provided to allow the editor to show the current state.
*
* If saved object information is needed for creation use-cases, getExplicitInput can also return an unknown typed attributes object which will be passed
* into the container's addNewEmbeddable function.
*/
getExplicitInput(
initialInput?: Partial<TEmbeddableInput>,
parent?: unknown
): Promise<Partial<TEmbeddableInput> | ExplicitInputWithAttributes>;
/**
* Creates a new embeddable instance based off the saved object id.
* @param savedObjectId
* @param input - some input may come from a parent, or user, if it's not stored with the saved object. For example, the time
* range of the parent container.
* @param parent
*/
createFromSavedObject(
savedObjectId: string,
input: Partial<TEmbeddableInput>,
parent?: unknown
): Promise<TEmbeddable | ErrorEmbeddable>;
/**
* Creates an Embeddable instance, running the inital input through all registered migrations. Resolves to undefined if a new Embeddable
* cannot be directly created and the user will instead be redirected elsewhere.
*/
create(
initialInput: TEmbeddableInput,
parent?: unknown
): Promise<TEmbeddable | ErrorEmbeddable | undefined>;
order?: number;
}

View file

@ -1,44 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { FinderAttributes } from '@kbn/saved-objects-finder-plugin/common';
import { IEmbeddable } from './i_embeddable';
import { EmbeddableFactory } from './embeddable_factory';
import { EmbeddableInput, EmbeddableOutput } from '..';
export type EmbeddableFactoryDefinition<
I extends EmbeddableInput = EmbeddableInput,
O extends EmbeddableOutput = EmbeddableOutput,
E extends IEmbeddable<I, O> = IEmbeddable<I, O>,
T extends FinderAttributes = FinderAttributes
> =
// Required parameters
Pick<
EmbeddableFactory<I, O, E, T>,
'create' | 'type' | 'latestVersion' | 'isEditable' | 'getDisplayName'
> &
// Optional parameters
Partial<
Pick<
EmbeddableFactory<I, O, E, T>,
| 'createFromSavedObject'
| 'isContainerType'
| 'getExplicitInput'
| 'savedObjectMetaData'
| 'canCreateNew'
| 'getDefaultInput'
| 'telemetry'
| 'extract'
| 'inject'
| 'migrations'
| 'grouping'
| 'getIconType'
| 'getDescription'
>
>;

View file

@ -8,10 +8,7 @@
*/
export * from '../../../common/lib/saved_object_embeddable';
export * from './default_embeddable_factory_provider';
export { Embeddable } from './embeddable';
export { EmbeddableErrorHandler } from './embeddable_error_handler';
export * from './embeddable_factory';
export * from './embeddable_factory_definition';
export { ErrorEmbeddable } from './error_embeddable';
export type { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';

View file

@ -8,7 +8,7 @@
*/
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { PanelNotFoundError, EmbeddableFactoryNotFoundError } from './errors';
import { PanelNotFoundError } from './errors';
describe('IncompatibleActionError', () => {
test('is instance of error', () => {
@ -33,15 +33,3 @@ describe('PanelNotFoundError', () => {
expect(error.code).toBe('PANEL_NOT_FOUND');
});
});
describe('EmbeddableFactoryNotFoundError', () => {
test('is instance of error', () => {
const error = new EmbeddableFactoryNotFoundError('type1');
expect(error).toBeInstanceOf(Error);
});
test('has EMBEDDABLE_FACTORY_NOT_FOUND code', () => {
const error = new EmbeddableFactoryNotFoundError('type1');
expect(error.code).toBe('EMBEDDABLE_FACTORY_NOT_FOUND');
});
});

View file

@ -33,18 +33,3 @@ export class PanelIncompatibleError extends Error {
);
}
}
export class EmbeddableFactoryNotFoundError extends Error {
code = 'EMBEDDABLE_FACTORY_NOT_FOUND';
constructor(type: string) {
super(
i18n.translate('embeddableApi.errors.embeddableFactoryNotFound', {
defaultMessage: `{type} can't be loaded. Please upgrade to the default distribution of Elasticsearch and Kibana with the appropriate license.`,
values: {
type,
},
})
);
}
}

View file

@ -1,77 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { EmbeddableInput } from '../embeddables';
import { runEmbeddableFactoryMigrations } from './run_factory_migrations';
describe('Run embeddable factory migrations', () => {
interface TestInputTypeVersion009 extends EmbeddableInput {
version: '0.0.9';
keyThatAlwaysExists: string;
keyThatGetsRemoved: string;
}
interface TestInputTypeVersion100 extends EmbeddableInput {
version: '1.0.0';
id: string;
keyThatAlwaysExists: string;
keyThatGetsAdded: string;
}
const migrations = {
'1.0.0': (input: TestInputTypeVersion009): TestInputTypeVersion100 => {
const newInput: TestInputTypeVersion100 = {
id: input.id,
version: '1.0.0',
keyThatAlwaysExists: input.keyThatAlwaysExists,
keyThatGetsAdded: 'I just got born',
};
return newInput;
},
};
it('should return the initial input and migrationRun=false if the current version is the latest', () => {
const initialInput: TestInputTypeVersion100 = {
id: 'superId',
version: '1.0.0',
keyThatAlwaysExists: 'Inside Problems',
keyThatGetsAdded: 'Oh my - I just got born',
};
const factory = {
latestVersion: '1.0.0',
migrations,
};
const result = runEmbeddableFactoryMigrations<TestInputTypeVersion100>(initialInput, factory);
expect(result.input).toBe(initialInput);
expect(result.migrationRun).toBe(false);
});
it('should return migrated input and migrationRun=true if version does not match latestVersion', () => {
const initialInput: TestInputTypeVersion009 = {
id: 'superId',
version: '0.0.9',
keyThatAlwaysExists: 'Inside Problems',
keyThatGetsRemoved: 'juvenile plumage',
};
const factory = {
latestVersion: '1.0.0',
migrations,
};
const result = runEmbeddableFactoryMigrations<TestInputTypeVersion100>(initialInput, factory);
expect(result.migrationRun).toBe(true);
expect(result.input.version).toBe('1.0.0');
expect((result.input as unknown as TestInputTypeVersion009).keyThatGetsRemoved).toBeUndefined();
expect(result.input.keyThatGetsAdded).toEqual('I just got born');
});
});

View file

@ -1,47 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { cloneDeep } from 'lodash';
import compare from 'semver/functions/compare';
import { migrateToLatest } from '@kbn/kibana-utils-plugin/common';
import { EmbeddableFactory, EmbeddableInput } from '../embeddables';
/**
* A helper function that migrates an Embeddable Input to its latest version. Note that this function
* only runs the embeddable factory's migrations.
*/
export const runEmbeddableFactoryMigrations = <ToType extends EmbeddableInput>(
initialInput: { version?: string },
factory: { migrations?: EmbeddableFactory['migrations']; latestVersion?: string }
): { input: ToType; migrationRun: boolean } => {
if (!factory.latestVersion) {
return { input: initialInput as unknown as ToType, migrationRun: false };
}
// any embeddable with no version set is considered to require all clientside migrations so we default to 0.0.0
const inputVersion = initialInput.version ?? '0.0.0';
const migrationRun = compare(inputVersion, factory.latestVersion, true) !== 0;
// return early to avoid extra operations when there are no migrations to run.
if (!migrationRun) return { input: initialInput as unknown as ToType, migrationRun };
const factoryMigrations =
typeof factory?.migrations === 'function' ? factory?.migrations() : factory?.migrations || {};
const migratedInput = migrateToLatest(
factoryMigrations ?? {},
{
state: cloneDeep(initialInput),
version: inputVersion,
},
true
);
migratedInput.version = factory.latestVersion;
return { input: migratedInput as ToType, migrationRun };
};

View file

@ -12,4 +12,3 @@ export * from './embeddables';
export * from './types';
export * from './triggers';
export * from './state_transfer';
export * from './factory_migrations/run_factory_migrations';

View file

@ -19,17 +19,17 @@ import { savedObjectsManagementPluginMock } from '@kbn/saved-objects-management-
import { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
import { EmbeddableStateTransfer } from '.';
import { setKibanaServices } from './kibana_services';
import { EmbeddablePublicPlugin } from './plugin';
import { registerReactEmbeddableFactory } from './react_embeddable_system';
import { registerAddFromLibraryType } from './add_from_library/registry';
import {
EmbeddableSetup,
EmbeddableSetupDependencies,
EmbeddableStart,
EmbeddableStartDependencies,
EmbeddableStateTransfer,
} from '.';
import { setKibanaServices } from './kibana_services';
import { EmbeddablePublicPlugin } from './plugin';
import { registerReactEmbeddableFactory } from './react_embeddable_system';
import { registerAddFromLibraryType } from './add_from_library/registry';
} from './types';
export type Setup = jest.Mocked<EmbeddableSetup>;
export type Start = jest.Mocked<EmbeddableStart>;
@ -48,7 +48,6 @@ const createSetupContract = (): Setup => {
const setupContract: Setup = {
registerAddFromLibraryType: jest.fn().mockImplementation(registerAddFromLibraryType),
registerReactEmbeddableFactory: jest.fn().mockImplementation(registerReactEmbeddableFactory),
registerEmbeddableFactory: jest.fn(),
registerEnhancement: jest.fn(),
};
return setupContract;
@ -56,8 +55,6 @@ const createSetupContract = (): Setup => {
const createStartContract = (): Start => {
const startContract: Start = {
getEmbeddableFactories: jest.fn(),
getEmbeddableFactory: jest.fn(),
telemetry: jest.fn(),
extract: jest.fn(),
inject: jest.fn(),

View file

@ -10,104 +10,6 @@
import { coreMock } from '@kbn/core/public/mocks';
import { testPlugin } from './tests/test_plugin';
describe('embeddable factory', () => {
const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();
const { setup, doStart } = testPlugin(coreSetup, coreStart);
const start = doStart();
const embeddableFactoryId = 'ID';
const embeddableFactory = {
type: embeddableFactoryId,
create: jest.fn(),
getDisplayName: () => 'Test',
isEditable: () => Promise.resolve(true),
extract: jest.fn().mockImplementation((state) => ({ state, references: [] })),
inject: jest.fn().mockImplementation((state) => state),
telemetry: jest.fn().mockResolvedValue({}),
latestVersion: '7.11.0',
migrations: { '7.11.0': jest.fn().mockImplementation((state) => state) },
} as any;
const embeddableState = {
id: embeddableFactoryId,
type: embeddableFactoryId,
my: 'state',
} as any;
const containerEmbeddableFactoryId = 'CONTAINER';
const containerEmbeddableFactory = {
type: containerEmbeddableFactoryId,
latestVersion: '1.0.0',
create: jest.fn(),
getDisplayName: () => 'Container',
isContainer: true,
isEditable: () => Promise.resolve(true),
extract: jest.fn().mockImplementation((state) => ({ state, references: [] })),
inject: jest.fn().mockImplementation((state) => state),
telemetry: jest.fn().mockResolvedValue({}),
migrations: { '7.12.0': jest.fn().mockImplementation((state) => state) },
};
const containerState = {
id: containerEmbeddableFactoryId,
type: containerEmbeddableFactoryId,
some: 'state',
panels: [
{
...embeddableState,
},
],
} as any;
setup.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory);
setup.registerEmbeddableFactory(containerEmbeddableFactoryId, containerEmbeddableFactory);
test('cannot register embeddable factory with the same ID', async () => {
expect(() =>
setup.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory)
).toThrowError(
'Embeddable factory [embeddableFactoryId = ID] already registered in Embeddables API.'
);
});
test('embeddableFactory extract function gets called when calling embeddable extract', () => {
start.extract(embeddableState);
expect(embeddableFactory.extract).toBeCalledWith(embeddableState);
});
test('embeddableFactory inject function gets called when calling embeddable inject', () => {
start.inject(embeddableState, []);
expect(embeddableFactory.extract).toBeCalledWith(embeddableState);
});
test('embeddableFactory telemetry function gets called when calling embeddable telemetry', () => {
start.telemetry(embeddableState, {});
expect(embeddableFactory.telemetry).toBeCalledWith(embeddableState, {});
});
test('embeddableFactory migrate function gets called when calling embeddable migrate', () => {
start.getAllMigrations!()['7.11.0']!(embeddableState);
expect(embeddableFactory.migrations['7.11.0']).toBeCalledWith(embeddableState);
});
test('panels inside container get automatically migrated when migrating conta1iner', () => {
start.getAllMigrations!()['7.11.0']!(containerState);
expect(embeddableFactory.migrations['7.11.0']).toBeCalledWith(embeddableState);
});
test('migrateToLatest returns list of all migrations', () => {
const migrations = start.getAllMigrations();
expect(migrations).toMatchSnapshot();
});
test('migrateToLatest calls correct migrate functions', () => {
start.migrateToLatest!({
state: embeddableState,
version: '7.11.0',
});
expect(embeddableFactory.migrations['7.11.0']).toBeCalledWith(embeddableState);
});
});
describe('embeddable enhancements', () => {
const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();

View file

@ -8,10 +8,6 @@
*/
import { Subscription } from 'rxjs';
import { identity } from 'lodash';
import type { SerializableRecord } from '@kbn/utility-types';
import { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { Start as InspectorStart } from '@kbn/inspector-plugin/public';
import {
PluginInitializerContext,
CoreSetup,
@ -20,26 +16,8 @@ import {
PublicAppInfo,
} from '@kbn/core/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
import { migrateToLatest, PersistableStateService } from '@kbn/kibana-utils-plugin/common';
import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public';
import {
EmbeddableFactoryRegistry,
EnhancementsRegistry,
EnhancementRegistryDefinition,
EnhancementRegistryItem,
} from './types';
import { migrateToLatest } from '@kbn/kibana-utils-plugin/common';
import { bootstrap } from './bootstrap';
import {
EmbeddableFactory,
EmbeddableInput,
EmbeddableOutput,
defaultEmbeddableFactoryProvider,
IEmbeddable,
} from './lib';
import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition';
import { EmbeddableStateTransfer } from './lib/state_transfer';
import { EmbeddableStateWithType, CommonEmbeddableStartContract } from '../common/types';
import {
@ -52,90 +30,19 @@ import { getAllMigrations } from '../common/lib/get_all_migrations';
import { setKibanaServices } from './kibana_services';
import { registerReactEmbeddableFactory } from './react_embeddable_system';
import { registerAddFromLibraryType } from './add_from_library/registry';
import { EnhancementsRegistry } from './enhancements/registry';
import {
EmbeddableSetup,
EmbeddableSetupDependencies,
EmbeddableStart,
EmbeddableStartDependencies,
} from './types';
export interface EmbeddableSetupDependencies {
uiActions: UiActionsSetup;
}
export interface EmbeddableStartDependencies {
uiActions: UiActionsStart;
inspector: InspectorStart;
usageCollection: UsageCollectionStart;
contentManagement: ContentManagementPublicStart;
savedObjectsManagement: SavedObjectsManagementPluginStart;
savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart;
}
export interface EmbeddableSetup {
/**
* Register a saved object type with the "Add from library" flyout.
*
* @example
* registerAddFromLibraryType({
* onAdd: (container, savedObject) => {
* container.addNewPanel({
* panelType: CONTENT_ID,
* initialState: savedObject.attributes,
* });
* },
* savedObjectType: MAP_SAVED_OBJECT_TYPE,
* savedObjectName: i18n.translate('xpack.maps.mapSavedObjectLabel', {
* defaultMessage: 'Map',
* }),
* getIconForSavedObject: () => APP_ICON,
* });
*/
registerAddFromLibraryType: typeof registerAddFromLibraryType;
/**
* Registers an async {@link ReactEmbeddableFactory} getter.
*/
registerReactEmbeddableFactory: typeof registerReactEmbeddableFactory;
/**
* @deprecated use {@link registerReactEmbeddableFactory} instead.
*/
registerEmbeddableFactory: <
I extends EmbeddableInput,
O extends EmbeddableOutput,
E extends IEmbeddable<I, O> = IEmbeddable<I, O>
>(
id: string,
factory: EmbeddableFactoryDefinition<I, O, E>
) => () => EmbeddableFactory<I, O, E>;
/**
* @deprecated
*/
registerEnhancement: (enhancement: EnhancementRegistryDefinition) => void;
}
export interface EmbeddableStart extends PersistableStateService<EmbeddableStateWithType> {
/**
* @deprecated use {@link registerReactEmbeddableFactory} instead.
*/
getEmbeddableFactory: <
I extends EmbeddableInput = EmbeddableInput,
O extends EmbeddableOutput = EmbeddableOutput,
E extends IEmbeddable<I, O> = IEmbeddable<I, O>
>(
embeddableFactoryId: string
) => EmbeddableFactory<I, O, E> | undefined;
/**
* @deprecated
*/
getEmbeddableFactories: () => IterableIterator<EmbeddableFactory>;
getStateTransfer: (storage?: Storage) => EmbeddableStateTransfer;
}
export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, EmbeddableStart> {
private readonly embeddableFactoryDefinitions: Map<string, EmbeddableFactoryDefinition> =
new Map();
private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map();
private readonly enhancements: EnhancementsRegistry = new Map();
private stateTransferService: EmbeddableStateTransfer = {} as EmbeddableStateTransfer;
private isRegistryReady = false;
private appList?: ReadonlyMap<string, PublicAppInfo>;
private appListSubscription?: Subscription;
private enhancementsRegistry = new EnhancementsRegistry();
constructor(initializerContext: PluginInitializerContext) {}
@ -145,17 +52,11 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
return {
registerReactEmbeddableFactory,
registerAddFromLibraryType,
registerEmbeddableFactory: this.registerEmbeddableFactory,
registerEnhancement: this.registerEnhancement,
registerEnhancement: this.enhancementsRegistry.registerEnhancement,
};
}
public start(core: CoreStart, deps: EmbeddableStartDependencies): EmbeddableStart {
this.embeddableFactoryDefinitions.forEach((def) => {
this.embeddableFactories.set(def.type, defaultEmbeddableFactoryProvider(def));
});
this.appListSubscription = core.application.applications$.subscribe((appList) => {
this.appList = appList;
});
@ -165,24 +66,19 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
core.application.currentAppId$,
this.appList
);
this.isRegistryReady = true;
const commonContract: CommonEmbeddableStartContract = {
getEmbeddableFactory: this
.getEmbeddableFactory as unknown as CommonEmbeddableStartContract['getEmbeddableFactory'],
getEnhancement: this.getEnhancement,
getEnhancement: this.enhancementsRegistry.getEnhancement,
};
const getAllMigrationsFn = () =>
getAllMigrations(
Array.from(this.embeddableFactories.values()),
Array.from(this.enhancements.values()),
[],
this.enhancementsRegistry.getEnhancements(),
getMigrateFunction(commonContract)
);
const embeddableStart: EmbeddableStart = {
getEmbeddableFactory: this.getEmbeddableFactory,
getEmbeddableFactories: this.getEmbeddableFactories,
getStateTransfer: (storage?: Storage) =>
storage
? new EmbeddableStateTransfer(
@ -210,89 +106,4 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
this.appListSubscription.unsubscribe();
}
}
private registerEnhancement = (enhancement: EnhancementRegistryDefinition) => {
if (this.enhancements.has(enhancement.id)) {
throw new Error(`enhancement with id ${enhancement.id} already exists in the registry`);
}
this.enhancements.set(enhancement.id, {
id: enhancement.id,
telemetry: enhancement.telemetry || ((state, stats) => stats),
inject: enhancement.inject || identity,
extract:
enhancement.extract ||
((state: SerializableRecord) => {
return { state, references: [] };
}),
migrations: enhancement.migrations || {},
});
};
private getEnhancement = (id: string): EnhancementRegistryItem => {
return (
this.enhancements.get(id) || {
id: 'unknown',
telemetry: (state, stats) => stats,
inject: identity,
extract: (state: SerializableRecord) => {
return { state, references: [] };
},
migrations: {},
}
);
};
private getEmbeddableFactories = () => {
this.ensureFactoriesExist();
return this.embeddableFactories.values();
};
private registerEmbeddableFactory = <
I extends EmbeddableInput = EmbeddableInput,
O extends EmbeddableOutput = EmbeddableOutput,
E extends IEmbeddable<I, O> = IEmbeddable<I, O>
>(
embeddableFactoryId: string,
factory: EmbeddableFactoryDefinition<I, O, E>
): (() => EmbeddableFactory<I, O, E>) => {
if (this.embeddableFactoryDefinitions.has(embeddableFactoryId)) {
throw new Error(
`Embeddable factory [embeddableFactoryId = ${embeddableFactoryId}] already registered in Embeddables API.`
);
}
this.embeddableFactoryDefinitions.set(embeddableFactoryId, factory);
return () => {
return this.getEmbeddableFactory(embeddableFactoryId);
};
};
private getEmbeddableFactory = <
I extends EmbeddableInput = EmbeddableInput,
O extends EmbeddableOutput = EmbeddableOutput,
E extends IEmbeddable<I, O> = IEmbeddable<I, O>
>(
embeddableFactoryId: string
): EmbeddableFactory<I, O, E> => {
if (!this.isRegistryReady) {
throw new Error('Embeddable factories can only be retrieved after setup lifecycle.');
}
this.ensureFactoryExists(embeddableFactoryId);
const factory = this.embeddableFactories.get(embeddableFactoryId);
return factory as EmbeddableFactory<I, O, E>;
};
// These two functions are only to support legacy plugins registering factories after the start lifecycle.
private ensureFactoriesExist = () => {
this.embeddableFactoryDefinitions.forEach((def) => this.ensureFactoryExists(def.type));
};
private ensureFactoryExists = (type: string) => {
if (!this.embeddableFactories.get(type)) {
const def = this.embeddableFactoryDefinitions.get(type);
if (!def) return;
this.embeddableFactories.set(type, defaultEmbeddableFactoryProvider(def));
}
};
}

View file

@ -19,7 +19,8 @@ import {
import { Query } from '@kbn/es-query';
import { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
import { contentManagementMock } from '@kbn/content-management-plugin/public/mocks';
import { EmbeddablePublicPlugin, EmbeddableSetup, EmbeddableStart } from '../plugin';
import { EmbeddablePublicPlugin } from '../plugin';
import type { EmbeddableSetup, EmbeddableStart } from '../types';
export interface TestPluginReturn {
plugin: EmbeddablePublicPlugin;
coreSetup: CoreSetup;

View file

@ -7,36 +7,65 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { SerializableRecord } from '@kbn/utility-types';
import { SavedObjectAttributes } from '@kbn/core/public';
import type { FinderAttributes } from '@kbn/saved-objects-finder-plugin/common';
import { PersistableState, PersistableStateDefinition } from '@kbn/kibana-utils-plugin/common';
import {
EmbeddableFactory,
EmbeddableInput,
EmbeddableOutput,
IEmbeddable,
EmbeddableFactoryDefinition,
} from './lib/embeddables';
import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { Start as InspectorStart } from '@kbn/inspector-plugin/public';
import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public';
import type { Storage } from '@kbn/kibana-utils-plugin/public';
import type { PersistableStateService } from '@kbn/kibana-utils-plugin/common';
import type { registerAddFromLibraryType } from './add_from_library/registry';
import type { registerReactEmbeddableFactory } from './react_embeddable_system';
import type { EmbeddableStateTransfer } from './lib';
import type { EmbeddableStateWithType } from '../common';
import { EnhancementRegistryDefinition } from './enhancements/types';
export type EmbeddableFactoryRegistry = Map<string, EmbeddableFactory>;
export type EnhancementsRegistry = Map<string, EnhancementRegistryItem>;
export interface EnhancementRegistryDefinition<P extends SerializableRecord = SerializableRecord>
extends PersistableStateDefinition<P> {
id: string;
export interface EmbeddableSetupDependencies {
uiActions: UiActionsSetup;
}
export interface EnhancementRegistryItem<P extends SerializableRecord = SerializableRecord>
extends PersistableState<P> {
id: string;
export interface EmbeddableStartDependencies {
uiActions: UiActionsStart;
inspector: InspectorStart;
usageCollection: UsageCollectionStart;
contentManagement: ContentManagementPublicStart;
savedObjectsManagement: SavedObjectsManagementPluginStart;
savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart;
}
export type EmbeddableFactoryProvider = <
I extends EmbeddableInput = EmbeddableInput,
O extends EmbeddableOutput = EmbeddableOutput,
E extends IEmbeddable<I, O> = IEmbeddable<I, O>,
T extends FinderAttributes = SavedObjectAttributes
>(
def: EmbeddableFactoryDefinition<I, O, E, T>
) => EmbeddableFactory<I, O, E, T>;
export interface EmbeddableSetup {
/**
* Register a saved object type with the "Add from library" flyout.
*
* @example
* registerAddFromLibraryType({
* onAdd: (container, savedObject) => {
* container.addNewPanel({
* panelType: CONTENT_ID,
* initialState: savedObject.attributes,
* });
* },
* savedObjectType: MAP_SAVED_OBJECT_TYPE,
* savedObjectName: i18n.translate('xpack.maps.mapSavedObjectLabel', {
* defaultMessage: 'Map',
* }),
* getIconForSavedObject: () => APP_ICON,
* });
*/
registerAddFromLibraryType: typeof registerAddFromLibraryType;
/**
* Registers an async {@link ReactEmbeddableFactory} getter.
*/
registerReactEmbeddableFactory: typeof registerReactEmbeddableFactory;
/**
* @deprecated
*/
registerEnhancement: (enhancement: EnhancementRegistryDefinition) => void;
}
export interface EmbeddableStart extends PersistableStateService<EmbeddableStateWithType> {
getStateTransfer: (storage?: Storage) => EmbeddableStateTransfer;
}

View file

@ -2774,7 +2774,6 @@
"embeddableApi.common.constants.grouping.other": "Autre",
"embeddableApi.contextMenuTrigger.description": "Une nouvelle action sera ajoutée au menu contextuel du panneau",
"embeddableApi.contextMenuTrigger.title": "Menu contextuel",
"embeddableApi.errors.embeddableFactoryNotFound": "Impossible de charger {type}. Veuillez effectuer une mise à niveau vers la distribution par défaut d'Elasticsearch et de Kibana avec la licence appropriée.",
"embeddableApi.errors.paneldoesNotExist": "Panneau introuvable",
"embeddableApi.errors.panelIncompatibleError": "L'API du panneau n'est pas compatible",
"embeddableApi.multiValueClickTrigger.description": "Sélection de plusieurs valeurs d'une même dimension dans la visualisation",

View file

@ -2769,7 +2769,6 @@
"embeddableApi.common.constants.grouping.other": "Other",
"embeddableApi.contextMenuTrigger.description": "新しいアクションがパネルのコンテキストメニューに追加されます",
"embeddableApi.contextMenuTrigger.title": "コンテキストメニュー",
"embeddableApi.errors.embeddableFactoryNotFound": "{type} を読み込めません。Elasticsearch と Kibanaのデフォルトのディストリビューションを適切なライセンスでアップグレードしてください。",
"embeddableApi.errors.paneldoesNotExist": "パネルが見つかりません",
"embeddableApi.errors.panelIncompatibleError": "パネルAPIに互換性がありません",
"embeddableApi.multiValueClickTrigger.description": "ビジュアライゼーションの1つのディメンションの複数値を選択しています",

View file

@ -2761,7 +2761,6 @@
"embeddableApi.common.constants.grouping.other": "其他",
"embeddableApi.contextMenuTrigger.description": "会将一个新操作添加到该面板的上下文菜单",
"embeddableApi.contextMenuTrigger.title": "上下文菜单",
"embeddableApi.errors.embeddableFactoryNotFound": "{type} 无法加载。请升级到具有适当许可的默认 Elasticsearch 和 Kibana 分发。",
"embeddableApi.errors.paneldoesNotExist": "未找到面板",
"embeddableApi.errors.panelIncompatibleError": "面板 API 不兼容",
"embeddableApi.multiValueClickTrigger.description": "在可视化上选择多个单一维度的值",

View file

@ -5,11 +5,6 @@
* 2.0.
*/
import { type ExplicitInputWithAttributes } from '@kbn/embeddable-plugin/public/lib';
import { EmbeddableInput } from '../../types';
export const encode = (
input: ExplicitInputWithAttributes | Partial<EmbeddableInput> | Readonly<EmbeddableInput>
) => Buffer.from(JSON.stringify(input)).toString('base64');
export const encode = (input: object) => Buffer.from(JSON.stringify(input)).toString('base64');
export const decode = (serializedInput: string) =>
JSON.parse(Buffer.from(serializedInput, 'base64').toString());

View file

@ -8,54 +8,9 @@
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import React from 'react';
import { EmbeddableFactoryDefinition, IEmbeddable } from '@kbn/embeddable-plugin/public';
import { BaseVisType, VisTypeAlias } from '@kbn/visualizations-plugin/public';
import { EditorMenu } from '../editor_menu.component';
const testFactories: EmbeddableFactoryDefinition[] = [
{
type: 'ml_anomaly_swimlane',
getDisplayName: () => 'Anomaly swimlane',
getIconType: () => '',
getDescription: () => 'Description for anomaly swimlane',
isEditable: () => Promise.resolve(true),
latestVersion: '1.0.0',
create: () => Promise.resolve({ id: 'swimlane_embeddable' } as IEmbeddable),
grouping: [
{
id: 'ml',
getDisplayName: () => 'machine learning',
getIconType: () => 'machineLearningApp',
},
],
},
{
type: 'ml_anomaly_chart',
getDisplayName: () => 'Anomaly chart',
getIconType: () => '',
getDescription: () => 'Description for anomaly chart',
isEditable: () => Promise.resolve(true),
create: () => Promise.resolve({ id: 'anomaly_chart_embeddable' } as IEmbeddable),
latestVersion: '1.0.0',
grouping: [
{
id: 'ml',
getDisplayName: () => 'machine learning',
getIconType: () => 'machineLearningApp',
},
],
},
{
type: 'log_stream',
getDisplayName: () => 'Log stream',
getIconType: () => '',
getDescription: () => 'Description for log stream',
latestVersion: '1.0.0',
isEditable: () => Promise.resolve(true),
create: () => Promise.resolve({ id: 'anomaly_chart_embeddable' } as IEmbeddable),
},
];
const testVisTypes: BaseVisType[] = [
{ title: 'TSVB', icon: '', description: 'Description of TSVB', name: 'tsvb' } as BaseVisType,
{
@ -95,11 +50,9 @@ const testVisTypeAliases: VisTypeAlias[] = [
storiesOf('components/WorkpadHeader/EditorMenu', module).add('default', () => (
<EditorMenu
addPanelActions={[]}
factories={testFactories}
promotedVisTypes={testVisTypes}
visTypeAliases={testVisTypeAliases}
createNewVisType={() => action('createNewVisType')}
createNewEmbeddableFromFactory={() => action('createNewEmbeddableFromFactory')}
createNewEmbeddableFromAction={() => action('createNewEmbeddableFromAction')}
/>
));

View file

@ -7,12 +7,7 @@
import React, { FC, useCallback } from 'react';
import {
EuiContextMenu,
EuiContextMenuItemIcon,
EuiContextMenuPanelItemDescriptor,
} from '@elastic/eui';
import { EmbeddableFactoryDefinition } from '@kbn/embeddable-plugin/public';
import { EuiContextMenu, EuiContextMenuPanelItemDescriptor } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ToolbarPopover } from '@kbn/shared-ux-button-toolbar';
import { Action, ActionExecutionContext } from '@kbn/ui-actions-plugin/public/actions';
@ -28,21 +23,11 @@ const strings = {
}),
};
interface FactoryGroup {
id: string;
appName: string;
icon: EuiContextMenuItemIcon;
panelId: number;
factories: EmbeddableFactoryDefinition[];
}
interface Props {
factories: EmbeddableFactoryDefinition[];
addPanelActions: Action[];
promotedVisTypes: BaseVisType[];
visTypeAliases: VisTypeAlias[];
createNewVisType: (visType?: BaseVisType | VisTypeAlias) => () => void;
createNewEmbeddableFromFactory: (factory: EmbeddableFactoryDefinition) => () => void;
createNewEmbeddableFromAction: (
action: Action,
context: ActionExecutionContext<object>,
@ -51,46 +36,14 @@ interface Props {
}
export const EditorMenu: FC<Props> = ({
factories,
addPanelActions,
promotedVisTypes,
visTypeAliases,
createNewVisType,
createNewEmbeddableFromAction,
createNewEmbeddableFromFactory,
}: Props) => {
const factoryGroupMap: Record<string, FactoryGroup> = {};
const ungroupedFactories: EmbeddableFactoryDefinition[] = [];
const canvasApi = useCanvasApi();
let panelCount = 1;
// Maps factories with a group to create nested context menus for each group type
// and pushes ungrouped factories into a separate array
factories.forEach((factory: EmbeddableFactoryDefinition, index) => {
const { grouping } = factory;
if (grouping) {
grouping.forEach((group) => {
if (factoryGroupMap[group.id]) {
factoryGroupMap[group.id].factories.push(factory);
} else {
factoryGroupMap[group.id] = {
id: group.id,
appName: group.getDisplayName ? group.getDisplayName({}) : group.id,
icon: (group.getIconType ? group.getIconType({}) : 'empty') as EuiContextMenuItemIcon,
factories: [factory],
panelId: panelCount,
};
panelCount++;
}
});
} else {
ungroupedFactories.push(factory);
}
});
const getVisTypeMenuItem = (visType: BaseVisType): EuiContextMenuPanelItemDescriptor => {
const { name, title, titleInWizard, description, icon = 'empty' } = visType;
return {
@ -116,22 +69,6 @@ export const EditorMenu: FC<Props> = ({
};
};
const getEmbeddableFactoryMenuItem = (
factory: EmbeddableFactoryDefinition
): EuiContextMenuPanelItemDescriptor => {
const icon = factory?.getIconType ? factory.getIconType() : 'empty';
const toolTipContent = factory?.getDescription ? factory.getDescription() : undefined;
return {
name: factory.getDisplayName(),
icon,
toolTipContent,
onClick: createNewEmbeddableFromFactory(factory),
'data-test-subj': `createNew-${factory.type}`,
};
};
const getAddPanelActionMenuItems = useCallback(
(closePopover: () => void) => {
return addPanelActions.map((item) => {
@ -158,23 +95,9 @@ export const EditorMenu: FC<Props> = ({
items: [
...visTypeAliases.map(getVisTypeAliasMenuItem),
...getAddPanelActionMenuItems(closePopover),
...ungroupedFactories.map(getEmbeddableFactoryMenuItem),
...promotedVisTypes.map(getVisTypeMenuItem),
...Object.values(factoryGroupMap).map(({ id, appName, icon, panelId }) => ({
name: appName,
icon,
panel: panelId,
'data-test-subj': `canvasEditorMenu-${id}Group`,
})),
],
},
...Object.values(factoryGroupMap).map(
({ appName, panelId, factories: groupFactories }: FactoryGroup) => ({
id: panelId,
title: appName,
items: groupFactories.map(getEmbeddableFactoryMenuItem),
})
),
];
return (

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import React, { FC, useCallback, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import {
VisGroups,
@ -13,19 +13,12 @@ import {
type VisTypeAlias,
type VisParams,
} from '@kbn/visualizations-plugin/public';
import {
EmbeddableFactory,
EmbeddableFactoryDefinition,
EmbeddableInput,
} from '@kbn/embeddable-plugin/public';
import { Action, ActionExecutionContext } from '@kbn/ui-actions-plugin/public/actions';
import { trackCanvasUiMetric, METRIC_TYPE } from '../../../lib/ui_metric';
import { CANVAS_APP } from '../../../../common/lib';
import { ElementSpec } from '../../../../types';
import { EditorMenu as Component } from './editor_menu.component';
import { embeddableInputToExpression } from '../../../../canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression';
import { EmbeddableInput as CanvasEmbeddableInput } from '../../../../canvas_plugin_src/expression_types';
import { useCanvasApi } from '../../hooks/use_canvas_api';
import { ADD_CANVAS_ELEMENT_TRIGGER } from '../../../state/triggers/add_canvas_element_trigger';
import {
@ -41,11 +34,6 @@ interface Props {
addElement: (element: Partial<ElementSpec>) => void;
}
interface UnwrappedEmbeddableFactory {
factory: EmbeddableFactory;
isEditable: boolean;
}
export const EditorMenu: FC<Props> = ({ addElement }) => {
const { pathname, search, hash } = useLocation();
const stateTransferService = embeddableService.getStateTransfer();
@ -53,26 +41,6 @@ export const EditorMenu: FC<Props> = ({ addElement }) => {
const [addPanelActions, setAddPanelActions] = useState<Array<Action<object>>>([]);
const embeddableFactories = useMemo(
() => (embeddableService ? Array.from(embeddableService.getEmbeddableFactories()) : []),
[]
);
const [unwrappedEmbeddableFactories, setUnwrappedEmbeddableFactories] = useState<
UnwrappedEmbeddableFactory[]
>([]);
useEffect(() => {
Promise.all(
embeddableFactories.map<Promise<UnwrappedEmbeddableFactory>>(async (factory) => ({
factory,
isEditable: await factory.isEditable(),
}))
).then((factories) => {
setUnwrappedEmbeddableFactories(factories);
});
}, [embeddableFactories]);
useEffect(() => {
let mounted = true;
async function loadPanelActions() {
@ -123,33 +91,6 @@ export const EditorMenu: FC<Props> = ({ addElement }) => {
[stateTransferService, pathname, search, hash]
);
const createNewEmbeddableFromFactory = useCallback(
(factory: EmbeddableFactoryDefinition) => async () => {
if (trackCanvasUiMetric) {
trackCanvasUiMetric(METRIC_TYPE.CLICK, factory.type);
}
let embeddableInput;
if (factory.getExplicitInput) {
embeddableInput = await factory.getExplicitInput();
} else {
const newEmbeddable = await factory.create({} as EmbeddableInput);
embeddableInput = newEmbeddable?.getInput();
}
if (embeddableInput) {
const expression = embeddableInputToExpression(
embeddableInput as CanvasEmbeddableInput,
factory.type,
undefined,
true
);
addElement({ expression });
}
},
[addElement]
);
const createNewEmbeddableFromAction = useCallback(
(action: Action, context: ActionExecutionContext<object>, closePopover: () => void) =>
(event: React.MouseEvent) => {
@ -190,31 +131,17 @@ export const EditorMenu: FC<Props> = ({ addElement }) => {
)
.filter(({ disableCreate }: VisTypeAlias) => !disableCreate);
const factories = unwrappedEmbeddableFactories
.filter(
({ isEditable, factory: { type, canCreateNew, isContainerType } }) =>
isEditable &&
!isContainerType &&
canCreateNew() &&
!['visualization', 'ml', 'links'].some((factoryType) => {
return type.includes(factoryType);
})
)
.map(({ factory }) => factory);
const promotedVisTypes = getVisTypesByGroup(VisGroups.PROMOTED);
const legacyVisTypes = getVisTypesByGroup(VisGroups.LEGACY);
return (
<Component
createNewVisType={createNewVisType}
createNewEmbeddableFromFactory={createNewEmbeddableFromFactory}
createNewEmbeddableFromAction={createNewEmbeddableFromAction}
promotedVisTypes={([] as Array<BaseVisType<VisParams>>).concat(
promotedVisTypes,
legacyVisTypes
)}
factories={factories}
addPanelActions={addPanelActions}
visTypeAliases={visTypeAliases}
/>

View file

@ -11,7 +11,7 @@ import type { ContentManagementPublicStart } from '@kbn/content-management-plugi
import type { CoreStart, PluginInitializerContext } from '@kbn/core/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { EmbeddableStart } from '@kbn/embeddable-plugin/public/plugin';
import type { EmbeddableStart } from '@kbn/embeddable-plugin/public';
import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
import type { ReportingStart } from '@kbn/reporting-plugin/public';

View file

@ -1,59 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { css } from '@emotion/react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import React, { useEffect, useRef, useState } from 'react';
import { ObservabilitySharedStart } from '../../../plugin';
export function ProfilingEmbeddable<T>({
embeddableFactoryId,
height,
...props
}: T & { embeddableFactoryId: string; height?: string }) {
const { embeddable: embeddablePlugin } = useKibana<ObservabilitySharedStart>().services;
const [embeddable, setEmbeddable] = useState<any>();
const embeddableRoot: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
useEffect(() => {
async function createEmbeddable() {
const factory = embeddablePlugin?.getEmbeddableFactory(embeddableFactoryId);
const input = { ...props, id: 'embeddable_profiling' };
const embeddableObject = await factory?.create(input);
setEmbeddable(embeddableObject);
}
createEmbeddable();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (embeddableRoot.current && embeddable) {
embeddable.render(embeddableRoot.current);
}
}, [embeddable, embeddableRoot]);
useEffect(() => {
if (embeddable) {
embeddable.updateInput(props);
embeddable.reload();
}
}, [embeddable, props]);
return (
<div
css={css`
width: 100%;
height: ${height};
display: flex;
flex: 1 1 100%;
z-index: 1;
min-height: 0;
`}
ref={embeddableRoot}
/>
);
}

View file

@ -51,18 +51,8 @@ const mockPlugin = {
observabilityAIAssistant: mockAIAssistantPlugin,
};
const mockEmbeddable = embeddablePluginMock.createStartContract();
mockEmbeddable.getEmbeddableFactory = jest.fn().mockImplementation(() => ({
create: () => ({
reload: jest.fn(),
setRenderTooltipContent: jest.fn(),
setLayerList: jest.fn(),
}),
}));
const mockCorePlugins = {
embeddable: mockEmbeddable,
embeddable: embeddablePluginMock.createStartContract(),
inspector: {},
maps: {},
observabilityShared: {