function getProps(
props?: Partial
): { props: DashboardViewportProps; options: DashboardContainerOptions } {
- const embeddableFactories = new Map();
- embeddableFactories.set(
+ const { setup, doStart } = embeddablePluginMock.createInstance();
+ setup.registerEmbeddableFactory(
CONTACT_CARD_EMBEDDABLE,
- new ContactCardEmbeddableFactory({}, (() => null) as any, {} as any)
+ new ContactCardEmbeddableFactory((() => null) as any, {} as any)
);
+ const start = doStart();
const options: DashboardContainerOptions = {
application: {} as any,
embeddable: {
getTriggerCompatibleActions: (() => []) as any,
- getEmbeddableFactories: (() => []) as any,
- getEmbeddableFactory: (id: string) => embeddableFactories.get(id),
+ getEmbeddableFactories: start.getEmbeddableFactories,
+ getEmbeddableFactory: start.getEmbeddableFactory,
} as any,
notifications: {} as any,
overlays: {} as any,
diff --git a/src/plugins/dashboard/public/tests/dashboard_container.test.tsx b/src/plugins/dashboard/public/tests/dashboard_container.test.tsx
index a81d80b440e04..1c72ad34e5446 100644
--- a/src/plugins/dashboard/public/tests/dashboard_container.test.tsx
+++ b/src/plugins/dashboard/public/tests/dashboard_container.test.tsx
@@ -52,7 +52,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => {
uiActionsSetup.attachAction(CONTEXT_MENU_TRIGGER, editModeAction);
setup.registerEmbeddableFactory(
CONTACT_CARD_EMBEDDABLE,
- new ContactCardEmbeddableFactory({} as any, (() => null) as any, {} as any)
+ new ContactCardEmbeddableFactory((() => null) as any, {} as any)
);
const start = doStart();
diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts
index eca74af4ec253..23275fbe8e8f0 100644
--- a/src/plugins/embeddable/public/index.ts
+++ b/src/plugins/embeddable/public/index.ts
@@ -38,6 +38,7 @@ export {
EmbeddableChildPanel,
EmbeddableChildPanelProps,
EmbeddableContext,
+ EmbeddableFactoryDefinition,
EmbeddableFactory,
EmbeddableFactoryNotFoundError,
EmbeddableFactoryRenderer,
diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx
index 9aeaf34f3311b..ce733bba6dda5 100644
--- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx
+++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx
@@ -18,14 +18,14 @@
*/
import { EditPanelAction } from './edit_panel_action';
-import { EmbeddableFactory, Embeddable, EmbeddableInput } from '../embeddables';
+import { Embeddable, EmbeddableInput } from '../embeddables';
import { ViewMode } from '../types';
import { ContactCardEmbeddable } from '../test_samples';
-import { EmbeddableStart } from '../../plugin';
+import { embeddablePluginMock } from '../../mocks';
-const embeddableFactories = new Map();
-const getFactory = ((id: string) =>
- embeddableFactories.get(id)) as EmbeddableStart['getEmbeddableFactory'];
+const { doStart } = embeddablePluginMock.createInstance();
+const start = doStart();
+const getFactory = start.getEmbeddableFactory;
class EditableEmbeddable extends Embeddable {
public readonly type = 'EDITABLE_EMBEDDABLE';
@@ -83,9 +83,7 @@ test('is not compatible when edit url is not available', async () => {
});
test('is not visible when edit url is available but in view mode', async () => {
- embeddableFactories.clear();
- const action = new EditPanelAction((type =>
- embeddableFactories.get(type)) as EmbeddableStart['getEmbeddableFactory']);
+ const action = new EditPanelAction(getFactory);
expect(
await action.isCompatible({
embeddable: new EditableEmbeddable(
@@ -100,9 +98,7 @@ test('is not visible when edit url is available but in view mode', async () => {
});
test('is not compatible when edit url is available, in edit mode, but not editable', async () => {
- embeddableFactories.clear();
- const action = new EditPanelAction((type =>
- embeddableFactories.get(type)) as EmbeddableStart['getEmbeddableFactory']);
+ const action = new EditPanelAction(getFactory);
expect(
await action.isCompatible({
embeddable: new EditableEmbeddable(
diff --git a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx
index 07915ce59e6ca..9e47da5cea032 100644
--- a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx
+++ b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx
@@ -20,7 +20,6 @@
import React from 'react';
import { nextTick } from 'test_utils/enzyme_helpers';
import { EmbeddableChildPanel } from './embeddable_child_panel';
-import { EmbeddableFactory } from '../embeddables';
import { CONTACT_CARD_EMBEDDABLE } from '../test_samples/embeddables/contact_card/contact_card_embeddable_factory';
import { SlowContactCardEmbeddableFactory } from '../test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory';
import { HelloWorldContainer } from '../test_samples/embeddables/hello_world_container';
@@ -32,16 +31,17 @@ import {
// eslint-disable-next-line
import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks';
import { mount } from 'enzyme';
+import { embeddablePluginMock } from '../../mocks';
test('EmbeddableChildPanel renders an embeddable when it is done loading', async () => {
const inspector = inspectorPluginMock.createStartContract();
-
- const embeddableFactories = new Map();
- embeddableFactories.set(
+ const { setup, doStart } = embeddablePluginMock.createInstance();
+ setup.registerEmbeddableFactory(
CONTACT_CARD_EMBEDDABLE,
new SlowContactCardEmbeddableFactory({ execAction: (() => null) as any })
);
- const getEmbeddableFactory = (id: string) => embeddableFactories.get(id);
+ const start = doStart();
+ const getEmbeddableFactory = start.getEmbeddableFactory;
const container = new HelloWorldContainer({ id: 'hello', panels: {} }, {
getEmbeddableFactory,
@@ -63,8 +63,8 @@ test('EmbeddableChildPanel renders an embeddable when it is done loading', async
container={container}
embeddableId={newEmbeddable.id}
getActions={() => Promise.resolve([])}
- getAllEmbeddableFactories={(() => []) as any}
- getEmbeddableFactory={(() => undefined) as any}
+ getAllEmbeddableFactories={start.getEmbeddableFactories}
+ getEmbeddableFactory={getEmbeddableFactory}
notifications={{} as any}
overlays={{} as any}
inspector={inspector}
diff --git a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts
new file mode 100644
index 0000000000000..570a78fc41ea9
--- /dev/null
+++ b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts
@@ -0,0 +1,52 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { SavedObjectAttributes } from 'kibana/public';
+import { EmbeddableFactoryDefinition } from './embeddable_factory_definition';
+import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';
+import { EmbeddableFactory } from './embeddable_factory';
+import { IContainer } from '..';
+
+export const defaultEmbeddableFactoryProvider = <
+ I extends EmbeddableInput = EmbeddableInput,
+ O extends EmbeddableOutput = EmbeddableOutput,
+ E extends IEmbeddable = IEmbeddable,
+ T extends SavedObjectAttributes = SavedObjectAttributes
+>(
+ def: EmbeddableFactoryDefinition
+): EmbeddableFactory => {
+ const factory: EmbeddableFactory = {
+ 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 ??
+ ((savedObjectId: string, input: Partial, parent?: IContainer) => {
+ throw new Error(`Creation from saved object not supported by type ${def.type}`);
+ }),
+ create: def.create.bind(def),
+ type: def.type,
+ isEditable: def.isEditable.bind(def),
+ getDisplayName: def.getDisplayName.bind(def),
+ savedObjectMetaData: def.savedObjectMetaData,
+ };
+ return factory;
+};
diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
index eb10c16806640..a135484ff61be 100644
--- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
+++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
@@ -158,6 +158,8 @@ export abstract class Embeddable<
*/
public destroy(): void {
this.destoyed = true;
+ this.input$.complete();
+ this.output$.complete();
if (this.parentSubscription) {
this.parentSubscription.unsubscribe();
}
diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts
index 81f7f35c900c9..7949b6fb8ba27 100644
--- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts
+++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts
@@ -22,32 +22,21 @@ import { SavedObjectMetaData } from '../../../../saved_objects/public';
import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';
import { ErrorEmbeddable } from './error_embeddable';
import { IContainer } from '../containers/i_container';
+import { PropertySpec } from '../types';
export interface EmbeddableInstanceConfiguration {
id: string;
savedObjectId?: string;
}
-export interface PropertySpec {
- displayName: string;
- accessPath: string;
- id: string;
- description: string;
- value?: string;
-}
-
export interface OutputSpec {
[key: string]: PropertySpec;
}
-export interface EmbeddableFactoryOptions {
- savedObjectMetaData?: SavedObjectMetaData;
-}
-
/**
- * The EmbeddableFactory creates and initializes an embeddable instance
+ * EmbeddableFactories create and initialize an embeddable instance
*/
-export abstract class EmbeddableFactory<
+export interface EmbeddableFactory<
TEmbeddableInput extends EmbeddableInput = EmbeddableInput,
TEmbeddableOutput extends EmbeddableOutput = EmbeddableOutput,
TEmbeddable extends IEmbeddable = IEmbeddable<
@@ -58,9 +47,15 @@ export abstract class EmbeddableFactory<
> {
// 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.
- public abstract readonly type: string;
+ 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;
- public readonly savedObjectMetaData?: SavedObjectMetaData;
+ readonly savedObjectMetaData?: SavedObjectMetaData;
/**
* True if is this factory create embeddables that are Containers. Used in the add panel to
@@ -68,31 +63,19 @@ export abstract class EmbeddableFactory<
* supported right now, but once nested containers are officially supported we can probably get
* rid of this interface.
*/
- public readonly isContainerType: boolean = false;
-
- constructor({ savedObjectMetaData }: EmbeddableFactoryOptions = {}) {
- this.savedObjectMetaData = savedObjectMetaData;
- }
-
- /**
- * 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.
- */
- public abstract async isEditable(): Promise;
+ readonly isContainerType: boolean;
/**
* Returns a display name for this type of embeddable. Used in "Create new... " options
* in the add panel for containers.
*/
- public abstract getDisplayName(): string;
+ getDisplayName(): 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.
*/
- public canCreateNew() {
- return true;
- }
+ canCreateNew(): boolean;
/**
* Can be used to get any default input, to be passed in to during the creation process. Default
@@ -100,18 +83,14 @@ export abstract class EmbeddableFactory<
* default input parameters.
* @param partial
*/
- public getDefaultInput(partial: Partial): Partial {
- return {};
- }
+ getDefaultInput(partial: Partial): Partial;
/**
* 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 any inherited
* input passed down from the parent container.
*/
- public async getExplicitInput(): Promise> {
- return {};
- }
+ getExplicitInput(): Promise>;
/**
* Creates a new embeddable instance based off the saved object id.
@@ -120,13 +99,11 @@ export abstract class EmbeddableFactory<
* range of the parent container.
* @param parent
*/
- public createFromSavedObject(
+ createFromSavedObject(
savedObjectId: string,
input: Partial,
parent?: IContainer
- ): Promise {
- throw new Error(`Creation from saved object not supported by type ${this.type}`);
- }
+ ): Promise;
/**
* Resolves to undefined if a new Embeddable cannot be directly created and the user will instead be redirected
@@ -134,7 +111,7 @@ export abstract class EmbeddableFactory<
*
* This will likely change in future iterations when we improve in place editing capabilities.
*/
- public abstract create(
+ create(
initialInput: TEmbeddableInput,
parent?: IContainer
): Promise;
diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts
new file mode 100644
index 0000000000000..b8985f7311ea9
--- /dev/null
+++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts
@@ -0,0 +1,44 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { SavedObjectAttributes } from 'kibana/server';
+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 = IEmbeddable,
+ T extends SavedObjectAttributes = SavedObjectAttributes
+> =
+ // Required parameters
+ Pick, 'create' | 'type' | 'isEditable' | 'getDisplayName'> &
+ // Optional parameters
+ Partial<
+ Pick<
+ EmbeddableFactory,
+ | 'createFromSavedObject'
+ | 'isContainerType'
+ | 'getExplicitInput'
+ | 'savedObjectMetaData'
+ | 'canCreateNew'
+ | 'getDefaultInput'
+ >
+ >;
diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx
index 51b83ea0ecaa3..e27045495af5b 100644
--- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx
+++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx
@@ -21,22 +21,22 @@ import {
HELLO_WORLD_EMBEDDABLE,
HelloWorldEmbeddableFactory,
} from '../../../../../../examples/embeddable_examples/public';
-import { EmbeddableFactory } from './embeddable_factory';
import { EmbeddableFactoryRenderer } from './embeddable_factory_renderer';
import { mount } from 'enzyme';
import { nextTick } from 'test_utils/enzyme_helpers';
// @ts-ignore
import { findTestSubject } from '@elastic/eui/lib/test';
-import { EmbeddableStart } from '../../plugin';
+import { embeddablePluginMock } from '../../mocks';
test('EmbeddableFactoryRenderer renders an embeddable', async () => {
- const embeddableFactories = new Map();
- embeddableFactories.set(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory());
- const getEmbeddableFactory = (id: string) => embeddableFactories.get(id);
+ const { setup, doStart } = embeddablePluginMock.createInstance();
+ setup.registerEmbeddableFactory(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory());
+
+ const getEmbeddableFactory = doStart().getEmbeddableFactory;
const component = mount(
diff --git a/src/plugins/embeddable/public/lib/embeddables/index.ts b/src/plugins/embeddable/public/lib/embeddables/index.ts
index 2175c3a59aa58..4d6ab37a50c05 100644
--- a/src/plugins/embeddable/public/lib/embeddables/index.ts
+++ b/src/plugins/embeddable/public/lib/embeddables/index.ts
@@ -18,11 +18,9 @@
*/
export { EmbeddableOutput, EmbeddableInput, IEmbeddable } from './i_embeddable';
export { Embeddable } from './embeddable';
-export {
- EmbeddableInstanceConfiguration,
- EmbeddableFactory,
- OutputSpec,
-} from './embeddable_factory';
+export * from './embeddable_factory';
+export * from './embeddable_factory_definition';
+export * from './default_embeddable_factory_provider';
export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable';
export { withEmbeddableSubscription } from './with_subscription';
export { EmbeddableFactoryRenderer } from './embeddable_factory_renderer';
diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx
index 757d4e6bfddef..649677dc67c7d 100644
--- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx
+++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx
@@ -27,7 +27,7 @@ import { I18nProvider } from '@kbn/i18n/react';
import { CONTEXT_MENU_TRIGGER } from '../triggers';
import { Action, UiActionsStart, ActionType } from 'src/plugins/ui_actions/public';
import { Trigger, ViewMode } from '../types';
-import { EmbeddableFactory, isErrorEmbeddable } from '../embeddables';
+import { isErrorEmbeddable } from '../embeddables';
import { EmbeddablePanel } from './embeddable_panel';
import { createEditModeAction } from '../test_samples/actions';
import {
@@ -43,26 +43,25 @@ import {
// eslint-disable-next-line
import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks';
import { EuiBadge } from '@elastic/eui';
+import { embeddablePluginMock } from '../../mocks';
const actionRegistry = new Map>();
const triggerRegistry = new Map();
-const embeddableFactories = new Map();
-const getEmbeddableFactory = (id: string) => embeddableFactories.get(id);
+
+const { setup, doStart } = embeddablePluginMock.createInstance();
const editModeAction = createEditModeAction();
const trigger: Trigger = {
id: CONTEXT_MENU_TRIGGER,
};
-const embeddableFactory = new ContactCardEmbeddableFactory(
- {} as any,
- (() => null) as any,
- {} as any
-);
+const embeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
actionRegistry.set(editModeAction.id, editModeAction);
triggerRegistry.set(trigger.id, trigger);
-embeddableFactories.set(embeddableFactory.type, embeddableFactory);
+setup.registerEmbeddableFactory(embeddableFactory.type, embeddableFactory);
+const start = doStart();
+const getEmbeddableFactory = start.getEmbeddableFactory;
test('HelloWorldContainer initializes embeddables', async done => {
const container = new HelloWorldContainer(
{
@@ -157,8 +156,8 @@ test('HelloWorldContainer in view mode hides edit mode actions', async () => {
Promise.resolve([])}
- getAllEmbeddableFactories={(() => []) as any}
- getEmbeddableFactory={(() => undefined) as any}
+ getAllEmbeddableFactories={start.getEmbeddableFactories}
+ getEmbeddableFactory={start.getEmbeddableFactory}
notifications={{} as any}
overlays={{} as any}
inspector={inspector}
@@ -195,8 +194,8 @@ const renderInEditModeAndOpenContextMenu = async (
[]) as any}
- getEmbeddableFactory={(() => undefined) as any}
+ getAllEmbeddableFactories={start.getEmbeddableFactories}
+ getEmbeddableFactory={start.getEmbeddableFactory}
notifications={{} as any}
overlays={{} as any}
inspector={inspector}
@@ -293,8 +292,8 @@ test('HelloWorldContainer in edit mode shows edit mode actions', async () => {
Promise.resolve([])}
- getAllEmbeddableFactories={(() => []) as any}
- getEmbeddableFactory={(() => undefined) as any}
+ getAllEmbeddableFactories={start.getEmbeddableFactories}
+ getEmbeddableFactory={start.getEmbeddableFactory}
notifications={{} as any}
overlays={{} as any}
inspector={inspector}
@@ -355,8 +354,8 @@ test('Updates when hidePanelTitles is toggled', async () => {
Promise.resolve([])}
- getAllEmbeddableFactories={(() => []) as any}
- getEmbeddableFactory={(() => undefined) as any}
+ getAllEmbeddableFactories={start.getEmbeddableFactories}
+ getEmbeddableFactory={start.getEmbeddableFactory}
notifications={{} as any}
overlays={{} as any}
inspector={inspector}
@@ -407,8 +406,8 @@ test('Check when hide header option is false', async () => {
Promise.resolve([])}
- getAllEmbeddableFactories={(() => []) as any}
- getEmbeddableFactory={(() => undefined) as any}
+ getAllEmbeddableFactories={start.getEmbeddableFactories}
+ getEmbeddableFactory={start.getEmbeddableFactory}
notifications={{} as any}
overlays={{} as any}
inspector={inspector}
@@ -444,8 +443,8 @@ test('Check when hide header option is true', async () => {
Promise.resolve([])}
- getAllEmbeddableFactories={(() => []) as any}
- getEmbeddableFactory={(() => undefined) as any}
+ getAllEmbeddableFactories={start.getEmbeddableFactories}
+ getEmbeddableFactory={start.getEmbeddableFactory}
notifications={{} as any}
overlays={{} as any}
inspector={inspector}
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx
index 8ee8c8dad9df3..74b08535bf27a 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx
+++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx
@@ -19,7 +19,6 @@
import { ViewMode, EmbeddableOutput, isErrorEmbeddable } from '../../../../';
import { AddPanelAction } from './add_panel_action';
-import { EmbeddableFactory } from '../../../../embeddables';
import {
FILTERABLE_EMBEDDABLE,
FilterableEmbeddable,
@@ -31,11 +30,12 @@ import { FilterableContainer } from '../../../../test_samples/embeddables/filter
import { coreMock } from '../../../../../../../../core/public/mocks';
import { ContactCardEmbeddable } from '../../../../test_samples';
import { esFilters, Filter } from '../../../../../../../../plugins/data/public';
-import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin';
+import { EmbeddableStart } from '../../../../../plugin';
+import { embeddablePluginMock } from '../../../../../mocks';
-const embeddableFactories = new Map();
-embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
-const getFactory = (id: string) => embeddableFactories.get(id);
+const { setup, doStart } = embeddablePluginMock.createInstance();
+setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
+const getFactory = doStart().getEmbeddableFactory;
let container: FilterableContainer;
let embeddable: FilterableEmbeddable;
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx
index 2fa21e40ca0f0..282b0f05891e0 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx
+++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx
@@ -31,7 +31,7 @@ import { ReactWrapper } from 'enzyme';
import { coreMock } from '../../../../../../../../core/public/mocks';
// @ts-ignore
import { findTestSubject } from '@elastic/eui/lib/test';
-import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin';
+import { embeddablePluginMock } from '../../../../../mocks';
function DummySavedObjectFinder(props: { children: React.ReactNode }) {
return (
@@ -43,10 +43,10 @@ function DummySavedObjectFinder(props: { children: React.ReactNode }) {
}
test('createNewEmbeddable() add embeddable to container', async () => {
+ const { setup, doStart } = embeddablePluginMock.createInstance();
const core = coreMock.createStart();
const { overlays } = core;
const contactCardEmbeddableFactory = new ContactCardEmbeddableFactory(
- {},
(() => null) as any,
overlays
);
@@ -55,7 +55,9 @@ test('createNewEmbeddable() add embeddable to container', async () => {
firstName: 'foo',
lastName: 'bar',
} as any);
- const getEmbeddableFactory = (id: string) => contactCardEmbeddableFactory;
+ setup.registerEmbeddableFactory(CONTACT_CARD_EMBEDDABLE, contactCardEmbeddableFactory);
+ const start = doStart();
+ const getEmbeddableFactory = start.getEmbeddableFactory;
const input: ContainerInput<{ firstName: string; lastName: string }> = {
id: '1',
panels: {},
@@ -66,8 +68,8 @@ test('createNewEmbeddable() add embeddable to container', async () => {
new Set([contactCardEmbeddableFactory]).values()}
+ getFactory={getEmbeddableFactory}
+ getAllFactories={start.getEmbeddableFactories}
notifications={core.notifications}
SavedObjectFinder={() => null}
/>
@@ -88,10 +90,10 @@ test('createNewEmbeddable() add embeddable to container', async () => {
});
test('selecting embeddable in "Create new ..." list calls createNewEmbeddable()', async () => {
+ const { setup, doStart } = embeddablePluginMock.createInstance();
const core = coreMock.createStart();
const { overlays } = core;
const contactCardEmbeddableFactory = new ContactCardEmbeddableFactory(
- {},
(() => null) as any,
overlays
);
@@ -100,8 +102,10 @@ test('selecting embeddable in "Create new ..." list calls createNewEmbeddable()'
firstName: 'foo',
lastName: 'bar',
} as any);
- const getEmbeddableFactory = ((id: string) =>
- contactCardEmbeddableFactory) as EmbeddableStart['getEmbeddableFactory'];
+
+ setup.registerEmbeddableFactory(CONTACT_CARD_EMBEDDABLE, contactCardEmbeddableFactory);
+ const start = doStart();
+ const getEmbeddableFactory = start.getEmbeddableFactory;
const input: ContainerInput<{ firstName: string; lastName: string }> = {
id: '1',
panels: {},
@@ -113,7 +117,7 @@ test('selecting embeddable in "Create new ..." list calls createNewEmbeddable()'
container={container}
onClose={onClose}
getFactory={getEmbeddableFactory}
- getAllFactories={() => new Set([contactCardEmbeddableFactory]).values()}
+ getAllFactories={start.getEmbeddableFactories}
notifications={core.notifications}
SavedObjectFinder={props => }
/>
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx
index 95eeb63710c32..06c47bd1bcad8 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx
+++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx
@@ -121,15 +121,16 @@ export class AddPanelFlyout extends React.Component {
public render() {
const SavedObjectFinder = this.props.SavedObjectFinder;
+ const metaData = [...this.props.getAllFactories()]
+ .filter(
+ embeddableFactory =>
+ Boolean(embeddableFactory.savedObjectMetaData) && !embeddableFactory.isContainerType
+ )
+ .map(({ savedObjectMetaData }) => savedObjectMetaData as any);
const savedObjectsFinder = (
- Boolean(embeddableFactory.savedObjectMetaData) && !embeddableFactory.isContainerType
- )
- .map(({ savedObjectMetaData }) => savedObjectMetaData as any)}
+ savedObjectMetaData={metaData}
showFilter={true}
noItemsMessage={i18n.translate('embeddableApi.addPanel.noMatchingObjectsMessage', {
defaultMessage: 'No matching objects found.',
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts
index 3f7c917cd1617..2f66d8eb0d619 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts
+++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts
@@ -32,18 +32,19 @@ import {
ContactCardEmbeddableFactory,
} from '../../../../test_samples/embeddables/contact_card/contact_card_embeddable_factory';
import { HelloWorldContainer } from '../../../../test_samples/embeddables/hello_world_container';
-import { EmbeddableFactory } from '../../../../embeddables';
+import { embeddablePluginMock } from '../../../../../mocks';
let container: Container;
let embeddable: ContactCardEmbeddable;
function createHelloWorldContainer(input = { id: '123', panels: {} }) {
- const embeddableFactories = new Map();
- const getEmbeddableFactory = (id: string) => embeddableFactories.get(id);
- embeddableFactories.set(
+ const { setup, doStart } = embeddablePluginMock.createInstance();
+ setup.registerEmbeddableFactory(
CONTACT_CARD_EMBEDDABLE,
- new ContactCardEmbeddableFactory({}, (() => {}) as any, {} as any)
+ new ContactCardEmbeddableFactory((() => {}) as any, {} as any)
);
+ const getEmbeddableFactory = doStart().getEmbeddableFactory;
+
return new HelloWorldContainer(input, { getEmbeddableFactory } as any);
}
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx
index e19acda8419da..ee31127cb5a40 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx
+++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx
@@ -28,20 +28,16 @@ import {
} from '../../../test_samples';
// eslint-disable-next-line
import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks';
-import {
- EmbeddableFactory,
- EmbeddableOutput,
- isErrorEmbeddable,
- ErrorEmbeddable,
-} from '../../../embeddables';
+import { EmbeddableOutput, isErrorEmbeddable, ErrorEmbeddable } from '../../../embeddables';
import { of } from '../../../../tests/helpers';
import { esFilters } from '../../../../../../../plugins/data/public';
-import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin';
+import { embeddablePluginMock } from '../../../../mocks';
+import { EmbeddableStart } from '../../../../plugin';
-const setup = async () => {
- const embeddableFactories = new Map();
- embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
- const getFactory = (id: string) => embeddableFactories.get(id);
+const setupTests = async () => {
+ const { setup, doStart } = embeddablePluginMock.createInstance();
+ setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
+ const getFactory = doStart().getEmbeddableFactory;
const container = new FilterableContainer(
{
id: 'hello',
@@ -79,7 +75,7 @@ test('Is compatible when inspector adapters are available', async () => {
const inspector = inspectorPluginMock.createStartContract();
inspector.isAvailable.mockImplementation(() => true);
- const { embeddable } = await setup();
+ const { embeddable } = await setupTests();
const inspectAction = new InspectPanelAction(inspector);
expect(await inspectAction.isCompatible({ embeddable })).toBe(true);
@@ -114,7 +110,7 @@ test('Executes when inspector adapters are available', async () => {
const inspector = inspectorPluginMock.createStartContract();
inspector.isAvailable.mockImplementation(() => true);
- const { embeddable } = await setup();
+ const { embeddable } = await setupTests();
const inspectAction = new InspectPanelAction(inspector);
expect(inspector.open).toHaveBeenCalledTimes(0);
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx
index f4d5aa148373b..dea4a88bda082 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx
+++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx
@@ -19,7 +19,6 @@
import { EmbeddableOutput, isErrorEmbeddable } from '../../../';
import { RemovePanelAction } from './remove_panel_action';
-import { EmbeddableFactory } from '../../../embeddables';
import { EmbeddableStart } from '../../../../plugin';
import {
FILTERABLE_EMBEDDABLE,
@@ -31,11 +30,11 @@ import { FilterableContainer } from '../../../test_samples/embeddables/filterabl
import { ViewMode } from '../../../types';
import { ContactCardEmbeddable } from '../../../test_samples/embeddables/contact_card/contact_card_embeddable';
import { esFilters, Filter } from '../../../../../../../plugins/data/public';
+import { embeddablePluginMock } from '../../../../mocks';
-const embeddableFactories = new Map();
-embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
-const getFactory = (id: string) => embeddableFactories.get(id);
-
+const { setup, doStart } = embeddablePluginMock.createInstance();
+setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
+const getFactory = doStart().getEmbeddableFactory;
let container: FilterableContainer;
let embeddable: FilterableEmbeddable;
diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx
index 20a5a8112f4d3..f977329562b9b 100644
--- a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx
+++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx
@@ -23,24 +23,21 @@ import { UiActionsStart } from 'src/plugins/ui_actions/public';
import { CoreStart } from 'src/core/public';
import { toMountPoint } from '../../../../../../kibana_react/public';
-import { EmbeddableFactory } from '../../../embeddables';
+import { EmbeddableFactoryDefinition } from '../../../embeddables';
import { Container } from '../../../containers';
import { ContactCardEmbeddable, ContactCardEmbeddableInput } from './contact_card_embeddable';
import { ContactCardInitializer } from './contact_card_initializer';
-import { EmbeddableFactoryOptions } from '../../../embeddables/embeddable_factory';
export const CONTACT_CARD_EMBEDDABLE = 'CONTACT_CARD_EMBEDDABLE';
-export class ContactCardEmbeddableFactory extends EmbeddableFactory {
+export class ContactCardEmbeddableFactory
+ implements EmbeddableFactoryDefinition {
public readonly type = CONTACT_CARD_EMBEDDABLE;
constructor(
- options: EmbeddableFactoryOptions,
private readonly execTrigger: UiActionsStart['executeTriggerActions'],
private readonly overlays: CoreStart['overlays']
- ) {
- super(options);
- }
+ ) {}
public async isEditable() {
return true;
@@ -52,7 +49,7 @@ export class ContactCardEmbeddableFactory extends EmbeddableFactory> {
+ public getExplicitInput = (): Promise> => {
return new Promise(resolve => {
const modalSession = this.overlays.openModal(
toMountPoint(
@@ -72,9 +69,9 @@ export class ContactCardEmbeddableFactory extends EmbeddableFactory {
return new ContactCardEmbeddable(
initialInput,
{
@@ -82,5 +79,5 @@ export class ContactCardEmbeddableFactory extends EmbeddableFactory {
+export class SlowContactCardEmbeddableFactory
+ implements EmbeddableFactoryDefinition {
private loadTickCount = 0;
public readonly type = CONTACT_CARD_EMBEDDABLE;
constructor(private readonly options: SlowContactCardEmbeddableFactoryOptions) {
- super();
if (options.loadTickCount) {
this.loadTickCount = options.loadTickCount;
}
@@ -48,10 +46,10 @@ export class SlowContactCardEmbeddableFactory extends EmbeddableFactory<
return 'slow to load contact card';
}
- public async create(initialInput: ContactCardEmbeddableInput, parent?: Container) {
+ public create = async (initialInput: ContactCardEmbeddableInput, parent?: Container) => {
for (let i = 0; i < this.loadTickCount; i++) {
await Promise.resolve();
}
return new ContactCardEmbeddable(initialInput, { execAction: this.options.execAction }, parent);
- }
+ };
}
diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_container_factory.ts b/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_container_factory.ts
index 3488f6a2e038d..f27c7e8b011fd 100644
--- a/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_container_factory.ts
+++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_container_factory.ts
@@ -18,24 +18,21 @@
*/
import { i18n } from '@kbn/i18n';
-import { Container, EmbeddableFactory } from '../..';
+import { Container, EmbeddableFactoryDefinition } from '../..';
import {
FilterableContainer,
FilterableContainerInput,
FILTERABLE_CONTAINER,
} from './filterable_container';
-import { EmbeddableFactoryOptions } from '../../embeddables/embeddable_factory';
import { EmbeddableStart } from '../../../plugin';
-export class FilterableContainerFactory extends EmbeddableFactory {
+export class FilterableContainerFactory
+ implements EmbeddableFactoryDefinition {
public readonly type = FILTERABLE_CONTAINER;
constructor(
- private readonly getFactory: EmbeddableStart['getEmbeddableFactory'],
- options: EmbeddableFactoryOptions = {}
- ) {
- super(options);
- }
+ private readonly getFactory: () => Promise
+ ) {}
public getDisplayName() {
return i18n.translate('embeddableApi.samples.filterableContainer.displayName', {
@@ -47,7 +44,8 @@ export class FilterableContainerFactory extends EmbeddableFactory {
+ const getEmbeddableFactory = await this.getFactory();
+ return new FilterableContainer(initialInput, getEmbeddableFactory, parent);
+ };
}
diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_embeddable_factory.ts b/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_embeddable_factory.ts
index f37a16ea86c43..4c941ee22abfa 100644
--- a/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_embeddable_factory.ts
+++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_embeddable_factory.ts
@@ -23,10 +23,11 @@ import {
FilterableEmbeddableInput,
FILTERABLE_EMBEDDABLE,
} from './filterable_embeddable';
-import { EmbeddableFactory } from '../../embeddables';
+import { EmbeddableFactoryDefinition } from '../../embeddables';
import { IContainer } from '../../containers';
-export class FilterableEmbeddableFactory extends EmbeddableFactory {
+export class FilterableEmbeddableFactory
+ implements EmbeddableFactoryDefinition {
public readonly type = FILTERABLE_EMBEDDABLE;
public async isEditable() {
diff --git a/src/plugins/embeddable/public/mocks.ts b/src/plugins/embeddable/public/mocks.ts
index ba2f78e42e10e..2ee05d8316ace 100644
--- a/src/plugins/embeddable/public/mocks.ts
+++ b/src/plugins/embeddable/public/mocks.ts
@@ -30,6 +30,7 @@ export type Start = jest.Mocked;
const createSetupContract = (): Setup => {
const setupContract: Setup = {
registerEmbeddableFactory: jest.fn(),
+ setCustomEmbeddableFactoryProvider: jest.fn(),
};
return setupContract;
};
diff --git a/src/plugins/embeddable/public/plugin.test.ts b/src/plugins/embeddable/public/plugin.test.ts
index c334411004e2c..804f3e2e8a7b4 100644
--- a/src/plugins/embeddable/public/plugin.test.ts
+++ b/src/plugins/embeddable/public/plugin.test.ts
@@ -18,6 +18,9 @@
*/
import { coreMock } from '../../../core/public/mocks';
import { testPlugin } from './tests/test_plugin';
+import { EmbeddableFactoryProvider } from './types';
+import { defaultEmbeddableFactoryProvider } from './lib';
+import { HelloWorldEmbeddable } from '../../../../examples/embeddable_examples/public';
test('cannot register embeddable factory with the same ID', async () => {
const coreSetup = coreMock.createSetup();
@@ -33,3 +36,75 @@ test('cannot register embeddable factory with the same ID', async () => {
'Embeddable factory [embeddableFactoryId = ID] already registered in Embeddables API.'
);
});
+
+test('can set custom embeddable factory provider', async () => {
+ const coreSetup = coreMock.createSetup();
+ const coreStart = coreMock.createStart();
+ const { setup, doStart } = testPlugin(coreSetup, coreStart);
+
+ const customProvider: EmbeddableFactoryProvider = def => ({
+ ...defaultEmbeddableFactoryProvider(def),
+ getDisplayName: () => 'Intercepted!',
+ });
+
+ setup.setCustomEmbeddableFactoryProvider(customProvider);
+ setup.registerEmbeddableFactory('test', {
+ type: 'test',
+ create: () => Promise.resolve(undefined),
+ getDisplayName: () => 'Test',
+ isEditable: () => Promise.resolve(true),
+ });
+
+ const start = doStart();
+ const factory = start.getEmbeddableFactory('test');
+ expect(factory!.getDisplayName()).toEqual('Intercepted!');
+});
+
+test('custom embeddable factory provider test for intercepting embeddable creation and destruction', async () => {
+ const coreSetup = coreMock.createSetup();
+ const coreStart = coreMock.createStart();
+ const { setup, doStart } = testPlugin(coreSetup, coreStart);
+
+ let updateCount = 0;
+ const customProvider: EmbeddableFactoryProvider = def => {
+ return {
+ ...defaultEmbeddableFactoryProvider(def),
+ create: async (input, parent) => {
+ const embeddable = await defaultEmbeddableFactoryProvider(def).create(input, parent);
+ if (embeddable) {
+ const subscription = embeddable.getInput$().subscribe(
+ () => {
+ updateCount++;
+ },
+ () => {},
+ () => {
+ subscription.unsubscribe();
+ updateCount = 0;
+ }
+ );
+ }
+ return embeddable;
+ },
+ };
+ };
+
+ setup.setCustomEmbeddableFactoryProvider(customProvider);
+ setup.registerEmbeddableFactory('test', {
+ type: 'test',
+ create: (input, parent) => Promise.resolve(new HelloWorldEmbeddable(input, parent)),
+ getDisplayName: () => 'Test',
+ isEditable: () => Promise.resolve(true),
+ });
+
+ const start = doStart();
+ const factory = start.getEmbeddableFactory('test');
+
+ const embeddable = await factory?.create({ id: '123' });
+ embeddable!.updateInput({ title: 'boo' });
+ // initial subscription, plus the second update.
+ expect(updateCount).toEqual(2);
+
+ embeddable!.destroy();
+ await new Promise(resolve => process.nextTick(resolve));
+ expect(updateCount).toEqual(0);
+});
diff --git a/src/plugins/embeddable/public/plugin.ts b/src/plugins/embeddable/public/plugin.ts
index 381665c359ffd..a483f90f76dde 100644
--- a/src/plugins/embeddable/public/plugin.ts
+++ b/src/plugins/embeddable/public/plugin.ts
@@ -18,9 +18,16 @@
*/
import { UiActionsSetup } from 'src/plugins/ui_actions/public';
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public';
-import { EmbeddableFactoryRegistry } from './types';
+import { EmbeddableFactoryRegistry, EmbeddableFactoryProvider } from './types';
import { bootstrap } from './bootstrap';
-import { EmbeddableFactory, EmbeddableInput, EmbeddableOutput } from './lib';
+import {
+ EmbeddableFactory,
+ EmbeddableInput,
+ EmbeddableOutput,
+ defaultEmbeddableFactoryProvider,
+ IEmbeddable,
+} from './lib';
+import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition';
export interface EmbeddableSetupDependencies {
uiActions: UiActionsSetup;
@@ -29,21 +36,29 @@ export interface EmbeddableSetupDependencies {
export interface EmbeddableSetup {
registerEmbeddableFactory: (
id: string,
- factory: EmbeddableFactory
+ factory: EmbeddableFactoryDefinition
) => void;
+ setCustomEmbeddableFactoryProvider: (customProvider: EmbeddableFactoryProvider) => void;
}
+
export interface EmbeddableStart {
getEmbeddableFactory: <
I extends EmbeddableInput = EmbeddableInput,
- O extends EmbeddableOutput = EmbeddableOutput
+ O extends EmbeddableOutput = EmbeddableOutput,
+ E extends IEmbeddable = IEmbeddable
>(
embeddableFactoryId: string
- ) => EmbeddableFactory | undefined;
+ ) => EmbeddableFactory | undefined;
getEmbeddableFactories: () => IterableIterator;
}
export class EmbeddablePublicPlugin implements Plugin {
+ private readonly embeddableFactoryDefinitions: Map<
+ string,
+ EmbeddableFactoryDefinition
+ > = new Map();
private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map();
+ private customEmbeddableFactoryProvider?: EmbeddableFactoryProvider;
constructor(initializerContext: PluginInitializerContext) {}
@@ -52,34 +67,57 @@ export class EmbeddablePublicPlugin implements Plugin {
+ if (this.customEmbeddableFactoryProvider) {
+ throw new Error(
+ 'Custom embeddable factory provider is already set, and can only be set once'
+ );
+ }
+ this.customEmbeddableFactoryProvider = provider;
+ },
};
}
- public start(core: CoreStart) {
+ public start(core: CoreStart): EmbeddableStart {
+ this.embeddableFactoryDefinitions.forEach(def => {
+ this.embeddableFactories.set(
+ def.type,
+ this.customEmbeddableFactoryProvider
+ ? this.customEmbeddableFactoryProvider(def)
+ : defaultEmbeddableFactoryProvider(def)
+ );
+ });
return {
getEmbeddableFactory: this.getEmbeddableFactory,
- getEmbeddableFactories: () => this.embeddableFactories.values(),
+ getEmbeddableFactories: () => {
+ this.ensureFactoriesExist();
+ return this.embeddableFactories.values();
+ },
};
}
public stop() {}
- private registerEmbeddableFactory = (embeddableFactoryId: string, factory: EmbeddableFactory) => {
- if (this.embeddableFactories.has(embeddableFactoryId)) {
+ private registerEmbeddableFactory = (
+ embeddableFactoryId: string,
+ factory: EmbeddableFactoryDefinition
+ ) => {
+ if (this.embeddableFactoryDefinitions.has(embeddableFactoryId)) {
throw new Error(
`Embeddable factory [embeddableFactoryId = ${embeddableFactoryId}] already registered in Embeddables API.`
);
}
-
- this.embeddableFactories.set(embeddableFactoryId, factory);
+ this.embeddableFactoryDefinitions.set(embeddableFactoryId, factory);
};
private getEmbeddableFactory = <
I extends EmbeddableInput = EmbeddableInput,
- O extends EmbeddableOutput = EmbeddableOutput
+ O extends EmbeddableOutput = EmbeddableOutput,
+ E extends IEmbeddable = IEmbeddable
>(
embeddableFactoryId: string
- ) => {
+ ): EmbeddableFactory => {
+ this.ensureFactoryExists(embeddableFactoryId);
const factory = this.embeddableFactories.get(embeddableFactoryId);
if (!factory) {
@@ -88,6 +126,24 @@ export class EmbeddablePublicPlugin implements Plugin;
+ return factory as EmbeddableFactory;
};
+
+ // 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,
+ this.customEmbeddableFactoryProvider
+ ? this.customEmbeddableFactoryProvider(def)
+ : defaultEmbeddableFactoryProvider(def)
+ );
+ }
+ }
}
diff --git a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts
index 6beef35bbe136..54f3ac2887f6c 100644
--- a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts
+++ b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts
@@ -36,13 +36,13 @@ import { esFilters } from '../../../../plugins/data/public';
test('ApplyFilterAction applies the filter to the root of the container tree', async () => {
const { doStart, setup } = testPlugin();
- const api = doStart();
- const factory1 = new FilterableContainerFactory(api.getEmbeddableFactory);
const factory2 = new FilterableEmbeddableFactory();
-
- setup.registerEmbeddableFactory(factory1.type, factory1);
+ const factory1 = new FilterableContainerFactory(async () => await api.getEmbeddableFactory);
setup.registerEmbeddableFactory(factory2.type, factory2);
+ setup.registerEmbeddableFactory(factory1.type, factory1);
+
+ const api = doStart();
const applyFilterAction = createFilterAction();
@@ -63,7 +63,9 @@ test('ApplyFilterAction applies the filter to the root of the container tree', a
FilterableContainer
>(FILTERABLE_CONTAINER, { panels: {}, id: 'Node2' });
- if (isErrorEmbeddable(node1) || isErrorEmbeddable(node2)) throw new Error();
+ if (isErrorEmbeddable(node1) || isErrorEmbeddable(node2)) {
+ throw new Error();
+ }
const embeddable = await node2.addNewEmbeddable<
FilterableEmbeddableInput,
@@ -94,9 +96,11 @@ test('ApplyFilterAction applies the filter to the root of the container tree', a
test('ApplyFilterAction is incompatible if the root container does not accept a filter as input', async () => {
const { doStart, coreStart, setup } = testPlugin();
- const api = doStart();
const inspector = inspectorPluginMock.createStartContract();
+ const factory = new FilterableEmbeddableFactory();
+ setup.registerEmbeddableFactory(factory.type, factory);
+ const api = doStart();
const applyFilterAction = createFilterAction();
const parent = new HelloWorldContainer(
{ id: 'root', panels: {} },
@@ -110,10 +114,6 @@ test('ApplyFilterAction is incompatible if the root container does not accept a
SavedObjectFinder: () => null,
}
);
-
- const factory = new FilterableEmbeddableFactory();
- setup.registerEmbeddableFactory(factory.type, factory);
-
const embeddable = await parent.addNewEmbeddable<
FilterableContainerInput,
EmbeddableOutput,
@@ -130,12 +130,12 @@ test('ApplyFilterAction is incompatible if the root container does not accept a
test('trying to execute on incompatible context throws an error ', async () => {
const { doStart, coreStart, setup } = testPlugin();
- const api = doStart();
const inspector = inspectorPluginMock.createStartContract();
const factory = new FilterableEmbeddableFactory();
setup.registerEmbeddableFactory(factory.type, factory);
+ const api = doStart();
const applyFilterAction = createFilterAction();
const parent = new HelloWorldContainer(
{ id: 'root', panels: {} },
diff --git a/src/plugins/embeddable/public/tests/container.test.ts b/src/plugins/embeddable/public/tests/container.test.ts
index 1ee52f4749135..87076399465d3 100644
--- a/src/plugins/embeddable/public/tests/container.test.ts
+++ b/src/plugins/embeddable/public/tests/container.test.ts
@@ -56,8 +56,6 @@ async function creatHelloWorldContainerAndEmbeddable(
const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();
const { setup, doStart, uiActions } = testPlugin(coreSetup, coreStart);
- const start = doStart();
-
const filterableFactory = new FilterableEmbeddableFactory();
const slowContactCardFactory = new SlowContactCardEmbeddableFactory({
execAction: uiActions.executeTriggerActions,
@@ -68,6 +66,8 @@ async function creatHelloWorldContainerAndEmbeddable(
setup.registerEmbeddableFactory(slowContactCardFactory.type, slowContactCardFactory);
setup.registerEmbeddableFactory(helloWorldFactory.type, helloWorldFactory);
+ const start = doStart();
+
const container = new HelloWorldContainer(containerInput, {
getActions: uiActions.getTriggerCompatibleActions,
getEmbeddableFactory: start.getEmbeddableFactory,
@@ -563,6 +563,13 @@ test('Container changes made directly after adding a new embeddable are propagat
const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();
const { setup, doStart, uiActions } = testPlugin(coreSetup, coreStart);
+
+ const factory = new SlowContactCardEmbeddableFactory({
+ loadTickCount: 3,
+ execAction: uiActions.executeTriggerActions,
+ });
+ setup.registerEmbeddableFactory(factory.type, factory);
+
const start = doStart();
const container = new HelloWorldContainer(
@@ -582,12 +589,6 @@ test('Container changes made directly after adding a new embeddable are propagat
}
);
- const factory = new SlowContactCardEmbeddableFactory({
- loadTickCount: 3,
- execAction: uiActions.executeTriggerActions,
- });
- setup.registerEmbeddableFactory(factory.type, factory);
-
const subscription = Rx.merge(container.getOutput$(), container.getInput$())
.pipe(skip(2))
.subscribe(() => {
@@ -759,12 +760,13 @@ test('untilEmbeddableLoaded resolves with undefined if child is subsequently rem
coreMock.createSetup(),
coreMock.createStart()
);
- const start = doStart();
const factory = new SlowContactCardEmbeddableFactory({
loadTickCount: 3,
execAction: uiActions.executeTriggerActions,
});
setup.registerEmbeddableFactory(factory.type, factory);
+
+ const start = doStart();
const container = new HelloWorldContainer(
{
id: 'hello',
@@ -799,12 +801,12 @@ test('adding a panel then subsequently removing it before its loaded removes the
coreMock.createSetup(),
coreMock.createStart()
);
- const start = doStart();
const factory = new SlowContactCardEmbeddableFactory({
loadTickCount: 1,
execAction: uiActions.executeTriggerActions,
});
setup.registerEmbeddableFactory(factory.type, factory);
+ const start = doStart();
const container = new HelloWorldContainer(
{
id: 'hello',
diff --git a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx b/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx
index 99d5a7c747d15..19e461b8bde7e 100644
--- a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx
+++ b/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx
@@ -47,15 +47,14 @@ beforeEach(async () => {
coreMock.createSetup(),
coreMock.createStart()
);
- api = doStart();
const contactCardFactory = new ContactCardEmbeddableFactory(
- {},
uiActions.executeTriggerActions,
{} as any
);
setup.registerEmbeddableFactory(contactCardFactory.type, contactCardFactory);
+ api = doStart();
container = new HelloWorldContainer(
{ id: '123', panels: {} },
{
diff --git a/src/plugins/embeddable/public/tests/explicit_input.test.ts b/src/plugins/embeddable/public/tests/explicit_input.test.ts
index f0a7219531b59..0e03db3ec8358 100644
--- a/src/plugins/embeddable/public/tests/explicit_input.test.ts
+++ b/src/plugins/embeddable/public/tests/explicit_input.test.ts
@@ -41,7 +41,6 @@ const { setup, doStart, coreStart, uiActions } = testPlugin(
coreMock.createSetup(),
coreMock.createStart()
);
-const start = doStart();
setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
const factory = new SlowContactCardEmbeddableFactory({
@@ -51,6 +50,8 @@ const factory = new SlowContactCardEmbeddableFactory({
setup.registerEmbeddableFactory(CONTACT_CARD_EMBEDDABLE, factory);
setup.registerEmbeddableFactory(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory());
+const start = doStart();
+
test('Explicit embeddable input mapped to undefined will default to inherited', async () => {
const derivedFilter: Filter = {
$state: { store: esFilters.FilterStateStore.APP_STATE },
diff --git a/src/plugins/embeddable/public/tests/get_embeddable_factories.test.ts b/src/plugins/embeddable/public/tests/get_embeddable_factories.test.ts
index 1a222cd548de7..1989d6356cbd1 100644
--- a/src/plugins/embeddable/public/tests/get_embeddable_factories.test.ts
+++ b/src/plugins/embeddable/public/tests/get_embeddable_factories.test.ts
@@ -35,16 +35,16 @@ test('returns empty list if there are no embeddable factories', () => {
test('returns existing embeddable factories', () => {
const { setup, doStart } = testPlugin();
- const start = doStart();
- const { length } = [...start.getEmbeddableFactories()];
- const factory1 = new FilterableContainerFactory(start.getEmbeddableFactory);
- const factory2 = new ContactCardEmbeddableFactory({} as any, (() => null) as any, {} as any);
+ const factory1 = new FilterableContainerFactory(async () => await start.getEmbeddableFactory);
+ const factory2 = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
setup.registerEmbeddableFactory(factory1.type, factory1);
setup.registerEmbeddableFactory(factory2.type, factory2);
+ const start = doStart();
+
const list = [...start.getEmbeddableFactories()];
- expect(list.length - length).toBe(2);
+ expect(list.length).toBe(2);
expect(!!list.find(({ type }) => factory1.type === type)).toBe(true);
expect(!!list.find(({ type }) => factory2.type === type)).toBe(true);
});
diff --git a/src/plugins/embeddable/public/types.ts b/src/plugins/embeddable/public/types.ts
index 7a879c389f3c4..2d112b2359818 100644
--- a/src/plugins/embeddable/public/types.ts
+++ b/src/plugins/embeddable/public/types.ts
@@ -17,6 +17,22 @@
* under the License.
*/
-import { EmbeddableFactory } from './lib/embeddables';
+import { SavedObjectAttributes } from 'kibana/public';
+import {
+ EmbeddableFactory,
+ EmbeddableInput,
+ EmbeddableOutput,
+ IEmbeddable,
+ EmbeddableFactoryDefinition,
+} from './lib/embeddables';
export type EmbeddableFactoryRegistry = Map;
+
+export type EmbeddableFactoryProvider = <
+ I extends EmbeddableInput = EmbeddableInput,
+ O extends EmbeddableOutput = EmbeddableOutput,
+ E extends IEmbeddable = IEmbeddable,
+ T extends SavedObjectAttributes = SavedObjectAttributes
+>(
+ def: EmbeddableFactoryDefinition
+) => EmbeddableFactory;
diff --git a/src/plugins/es_ui_shared/public/request/np_ready_request.ts b/src/plugins/es_ui_shared/public/request/np_ready_request.ts
index 6771abd64df7e..06af698f2ce02 100644
--- a/src/plugins/es_ui_shared/public/request/np_ready_request.ts
+++ b/src/plugins/es_ui_shared/public/request/np_ready_request.ts
@@ -43,7 +43,7 @@ export interface UseRequestResponse {
isInitialRequest: boolean;
isLoading: boolean;
error: E | null;
- data: D | null;
+ data?: D | null;
sendRequest: (...args: any[]) => Promise>;
}
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts
index a8d24984cec7c..0509b8081c35b 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts
@@ -28,9 +28,9 @@ interface Props {
}
export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) => {
- const [formData, setFormData] = useState({});
- const previousRawData = useRef({});
const form = useFormContext();
+ const previousRawData = useRef(form.__formData$.current.value);
+ const [formData, setFormData] = useState(previousRawData.current);
useEffect(() => {
const subscription = form.subscribe(({ data: { raw } }) => {
@@ -41,6 +41,7 @@ export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) =
const valuesToWatchArray = Array.isArray(pathsToWatch)
? (pathsToWatch as string[])
: ([pathsToWatch] as string[]);
+
if (valuesToWatchArray.some(value => previousRawData.current[value] !== raw[value])) {
previousRawData.current = raw;
setFormData(raw);
diff --git a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts
new file mode 100644
index 0000000000000..bf2d174f594b2
--- /dev/null
+++ b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts
@@ -0,0 +1,69 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Vis } from '../types';
+import { VisualizeInput, VisualizeEmbeddable } from './visualize_embeddable';
+import { IContainer, ErrorEmbeddable } from '../../../../plugins/embeddable/public';
+import { DisabledLabEmbeddable } from './disabled_lab_embeddable';
+import {
+ getSavedVisualizationsLoader,
+ getUISettings,
+ getHttp,
+ getTimeFilter,
+ getCapabilities,
+} from '../services';
+
+export const createVisEmbeddableFromObject = async (
+ vis: Vis,
+ input: Partial & { id: string },
+ parent?: IContainer
+): Promise => {
+ const savedVisualizations = getSavedVisualizationsLoader();
+
+ try {
+ const visId = vis.id as string;
+
+ const editUrl = visId
+ ? getHttp().basePath.prepend(`/app/kibana${savedVisualizations.urlFor(visId)}`)
+ : '';
+ const isLabsEnabled = getUISettings().get('visualize:enableLabs');
+
+ if (!isLabsEnabled && vis.type.stage === 'experimental') {
+ return new DisabledLabEmbeddable(vis.title, input);
+ }
+
+ const indexPattern = vis.data.indexPattern;
+ const indexPatterns = indexPattern ? [indexPattern] : [];
+ const editable = getCapabilities().visualize.save as boolean;
+ return new VisualizeEmbeddable(
+ getTimeFilter(),
+ {
+ vis,
+ indexPatterns,
+ editUrl,
+ editable,
+ },
+ input,
+ parent
+ );
+ } catch (e) {
+ console.error(e); // eslint-disable-line no-console
+ return new ErrorEmbeddable(e, input, parent);
+ }
+};
diff --git a/src/plugins/visualizations/public/embeddable/index.ts b/src/plugins/visualizations/public/embeddable/index.ts
index 78f9827ffde3e..3753c4dbbb9ed 100644
--- a/src/plugins/visualizations/public/embeddable/index.ts
+++ b/src/plugins/visualizations/public/embeddable/index.ts
@@ -21,3 +21,4 @@ export { VisualizeEmbeddable, VisualizeInput } from './visualize_embeddable';
export { VisualizeEmbeddableFactory } from './visualize_embeddable_factory';
export { VISUALIZE_EMBEDDABLE_TYPE } from './constants';
export { VIS_EVENT_TO_TRIGGER } from './events';
+export { createVisEmbeddableFromObject } from './create_vis_embeddable_from_object';
diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
index 0c7e732f0b185..e64d200251797 100644
--- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
+++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
@@ -33,8 +33,8 @@ import {
EmbeddableInput,
EmbeddableOutput,
Embeddable,
- Container,
EmbeddableVisTriggerContext,
+ IContainer,
} from '../../../../plugins/embeddable/public';
import { dispatchRenderComplete } from '../../../../plugins/kibana_utils/public';
import { IExpressionLoaderParams, ExpressionsStart } from '../../../../plugins/expressions/public';
@@ -89,7 +89,7 @@ export class VisualizeEmbeddable extends Embeddable {
+export class VisualizeEmbeddableFactory
+ implements
+ EmbeddableFactoryDefinition<
+ VisualizeInput,
+ VisualizeOutput | EmbeddableOutput,
+ VisualizeEmbeddable | DisabledLabEmbeddable,
+ VisualizationAttributes
+ > {
public readonly type = VISUALIZE_EMBEDDABLE_TYPE;
-
- constructor() {
- super({
- savedObjectMetaData: {
- name: i18n.translate('visualizations.savedObjectName', { defaultMessage: 'Visualization' }),
- includeFields: ['visState'],
- type: 'visualization',
- getIconForSavedObject: savedObject => {
- return (
- getTypes().get(JSON.parse(savedObject.attributes.visState).type).icon || 'visualizeApp'
- );
- },
- getTooltipForSavedObject: savedObject => {
- return `${savedObject.attributes.title} (${
- getTypes().get(JSON.parse(savedObject.attributes.visState).type).title
- })`;
- },
- showSavedObject: savedObject => {
- const typeName: string = JSON.parse(savedObject.attributes.visState).type;
- const visType = getTypes().get(typeName);
- if (!visType) {
- return false;
- }
- if (getUISettings().get('visualize:enableLabs')) {
- return true;
- }
- return visType.stage !== 'experimental';
- },
- },
- });
- }
+ public readonly savedObjectMetaData: SavedObjectMetaData = {
+ name: i18n.translate('visualizations.savedObjectName', { defaultMessage: 'Visualization' }),
+ includeFields: ['visState'],
+ type: 'visualization',
+ getIconForSavedObject: savedObject => {
+ return (
+ getTypes().get(JSON.parse(savedObject.attributes.visState).type).icon || 'visualizeApp'
+ );
+ },
+ getTooltipForSavedObject: savedObject => {
+ return `${savedObject.attributes.title} (${
+ getTypes().get(JSON.parse(savedObject.attributes.visState).type).title
+ })`;
+ },
+ showSavedObject: savedObject => {
+ const typeName: string = JSON.parse(savedObject.attributes.visState).type;
+ const visType = getTypes().get(typeName);
+ if (!visType) {
+ return false;
+ }
+ if (getUISettings().get('visualize:enableLabs')) {
+ return true;
+ }
+ return visType.stage !== 'experimental';
+ },
+ };
+ constructor() {}
public async isEditable() {
return getCapabilities().visualize.save as boolean;
@@ -93,56 +91,17 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory<
});
}
- public async createFromObject(
- vis: Vis,
- input: Partial & { id: string },
- parent?: Container
- ): Promise {
- const savedVisualizations = getSavedVisualizationsLoader();
-
- try {
- const visId = vis.id as string;
-
- const editUrl = visId
- ? getHttp().basePath.prepend(`/app/kibana${savedVisualizations.urlFor(visId)}`)
- : '';
- const isLabsEnabled = getUISettings().get('visualize:enableLabs');
-
- if (!isLabsEnabled && vis.type.stage === 'experimental') {
- return new DisabledLabEmbeddable(vis.title, input);
- }
-
- const indexPattern = vis.data.indexPattern;
- const indexPatterns = indexPattern ? [indexPattern] : [];
- const editable = await this.isEditable();
- return new VisualizeEmbeddable(
- getTimeFilter(),
- {
- vis,
- indexPatterns,
- editUrl,
- editable,
- },
- input,
- parent
- );
- } catch (e) {
- console.error(e); // eslint-disable-line no-console
- return new ErrorEmbeddable(e, input, parent);
- }
- }
-
public async createFromSavedObject(
savedObjectId: string,
input: Partial & { id: string },
- parent?: Container
+ parent?: IContainer
): Promise {
const savedVisualizations = getSavedVisualizationsLoader();
try {
const savedObject = await savedVisualizations.get(savedObjectId);
const vis = new Vis(savedObject.visState.type, await convertToSerializedVis(savedObject));
- return this.createFromObject(vis, input, parent);
+ return createVisEmbeddableFromObject(vis, input, parent);
} catch (e) {
console.error(e); // eslint-disable-line no-console
return new ErrorEmbeddable(e, input, parent);
diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts
index f4983a4313c4d..2aa346423297a 100644
--- a/src/plugins/visualizations/public/mocks.ts
+++ b/src/plugins/visualizations/public/mocks.ts
@@ -43,6 +43,9 @@ const createStartContract = (): VisualizationsStart => ({
createVis: jest.fn(),
convertFromSerializedVis: jest.fn(),
convertToSerializedVis: jest.fn(),
+ __LEGACY: {
+ createVisEmbeddableFromObject: jest.fn(),
+ },
});
const createInstance = async () => {
diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts
index d3e7b759a4416..216defcee9016 100644
--- a/src/plugins/visualizations/public/plugin.ts
+++ b/src/plugins/visualizations/public/plugin.ts
@@ -37,7 +37,11 @@ import {
setChrome,
setOverlays,
} from './services';
-import { VISUALIZE_EMBEDDABLE_TYPE, VisualizeEmbeddableFactory } from './embeddable';
+import {
+ VISUALIZE_EMBEDDABLE_TYPE,
+ VisualizeEmbeddableFactory,
+ createVisEmbeddableFromObject,
+} from './embeddable';
import { ExpressionsSetup, ExpressionsStart } from '../../../plugins/expressions/public';
import { EmbeddableSetup } from '../../../plugins/embeddable/public';
import { visualization as visualizationFunction } from './expressions/visualization_function';
@@ -69,6 +73,7 @@ export interface VisualizationsStart extends TypesStart {
convertToSerializedVis: typeof convertToSerializedVis;
convertFromSerializedVis: typeof convertFromSerializedVis;
showNewVisModal: typeof showNewVisModal;
+ __LEGACY: { createVisEmbeddableFromObject: typeof createVisEmbeddableFromObject };
}
export interface VisualizationsSetupDeps {
@@ -163,6 +168,7 @@ export class VisualizationsPlugin
convertToSerializedVis,
convertFromSerializedVis,
savedVisualizationsLoader,
+ __LEGACY: { createVisEmbeddableFromObject },
};
}
diff --git a/test/functional/apps/visualize/_vertical_bar_chart.js b/test/functional/apps/visualize/_vertical_bar_chart.js
index 9bb220a11a86a..d5f4c45f8bdbc 100644
--- a/test/functional/apps/visualize/_vertical_bar_chart.js
+++ b/test/functional/apps/visualize/_vertical_bar_chart.js
@@ -54,6 +54,25 @@ export default function({ getService, getPageObjects }) {
});
});
+ describe('bar charts range on x axis', () => {
+ it('should individual bars for each configured range', async function() {
+ await PageObjects.visualize.navigateToNewVisualization();
+ await PageObjects.visualize.clickVerticalBarChart();
+ await PageObjects.visualize.clickNewSearch();
+ await PageObjects.timePicker.setDefaultAbsoluteRange();
+ await PageObjects.visEditor.clickBucket('X-axis');
+ log.debug('Aggregation = Date Range');
+ await PageObjects.visEditor.selectAggregation('Date Range');
+ log.debug('Field = @timestamp');
+ await PageObjects.visEditor.selectField('@timestamp');
+ await PageObjects.visEditor.clickAddDateRange();
+ await PageObjects.visEditor.setDateRangeByIndex('1', 'now-2w/w', 'now-1w/w');
+ await PageObjects.visEditor.clickGo();
+ const bottomLabels = await PageObjects.visChart.getXAxisLabels();
+ expect(bottomLabels.length).to.be(2);
+ });
+ });
+
// FLAKY: https://github.com/elastic/kibana/issues/22322
describe.skip('vertical bar chart flaky part', function() {
const vizName1 = 'Visualization VerticalBarChart';
diff --git a/test/functional/page_objects/visualize_editor_page.ts b/test/functional/page_objects/visualize_editor_page.ts
index b1c3e924b3c1b..41c12170cf4dc 100644
--- a/test/functional/page_objects/visualize_editor_page.ts
+++ b/test/functional/page_objects/visualize_editor_page.ts
@@ -103,6 +103,15 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP
await radioBtn.click();
}
+ public async clickAddDateRange() {
+ await testSubjects.click(`visEditorAddDateRange`);
+ }
+
+ public async setDateRangeByIndex(index: string, from: string, to: string) {
+ await testSubjects.setValue(`visEditorDateRange${index}__from`, from);
+ await testSubjects.setValue(`visEditorDateRange${index}__to`, to);
+ }
+
/**
* Adds new bucket
* @param bucketName bucket name, like 'X-axis', 'Split rows', 'Split series'
diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx
index f8625e4490e51..fd07416cadbc5 100644
--- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx
+++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx
@@ -29,7 +29,6 @@ import {
import {
DASHBOARD_CONTAINER_TYPE,
DashboardContainer,
- DashboardContainerFactory,
DashboardContainerInput,
} from '../../../../../../../../src/plugins/dashboard/public';
@@ -70,8 +69,9 @@ export class DashboardContainerExample extends React.Component {
this.mounted = true;
const dashboardFactory = this.props.getEmbeddableFactory<
DashboardContainerInput,
- ContainerOutput
- >(DASHBOARD_CONTAINER_TYPE) as DashboardContainerFactory;
+ ContainerOutput,
+ DashboardContainer
+ >(DASHBOARD_CONTAINER_TYPE);
if (dashboardFactory) {
this.container = await dashboardFactory.create(dashboardInput);
if (this.mounted) {
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts
index 1caea1b4b728f..99a59c756e228 100644
--- a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts
@@ -18,7 +18,7 @@ import {
} from '../../../../../../../src/plugins/data/public';
import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public';
import {
- EmbeddableFactory as AbstractEmbeddableFactory,
+ EmbeddableFactoryDefinition,
ErrorEmbeddable,
EmbeddableInput,
IContainer,
@@ -36,25 +36,22 @@ interface StartServices {
indexPatternService: IndexPatternsContract;
}
-export class EmbeddableFactory extends AbstractEmbeddableFactory {
+export class EmbeddableFactory implements EmbeddableFactoryDefinition {
type = DOC_TYPE;
+ savedObjectMetaData = {
+ name: i18n.translate('xpack.lens.lensSavedObjectLabel', {
+ defaultMessage: 'Lens Visualization',
+ }),
+ type: DOC_TYPE,
+ getIconForSavedObject: () => 'lensApp',
+ };
- constructor(private getStartServices: () => Promise) {
- super({
- savedObjectMetaData: {
- name: i18n.translate('xpack.lens.lensSavedObjectLabel', {
- defaultMessage: 'Lens Visualization',
- }),
- type: DOC_TYPE,
- getIconForSavedObject: () => 'lensApp',
- },
- });
- }
+ constructor(private getStartServices: () => Promise) {}
- public async isEditable() {
+ public isEditable = async () => {
const { capabilities } = await this.getStartServices();
return capabilities.visualize.save as boolean;
- }
+ };
canCreateNew() {
return false;
@@ -66,11 +63,11 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory {
});
}
- async createFromSavedObject(
+ createFromSavedObject = async (
savedObjectId: string,
input: Partial & { id: string },
parent?: IContainer
- ) {
+ ) => {
const {
savedObjectsClient,
coreHttp,
@@ -111,7 +108,7 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory {
input,
parent
);
- }
+ };
async create(input: EmbeddableInput) {
return new ErrorEmbeddable('Lens can only be created from a saved object', input);
diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.js b/x-pack/legacy/plugins/maps/public/actions/map_actions.js
index 415630d9f730b..aa55cf0808ef2 100644
--- a/x-pack/legacy/plugins/maps/public/actions/map_actions.js
+++ b/x-pack/legacy/plugins/maps/public/actions/map_actions.js
@@ -174,9 +174,16 @@ export function removeTrackedLayerStateForSelectedLayer() {
export function replaceLayerList(newLayerList) {
return (dispatch, getState) => {
- getLayerListRaw(getState()).forEach(({ id }) => {
- dispatch(removeLayerFromLayerList(id));
- });
+ const isMapReady = getMapReady(getState());
+ if (!isMapReady) {
+ dispatch({
+ type: CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST,
+ });
+ } else {
+ getLayerListRaw(getState()).forEach(({ id }) => {
+ dispatch(removeLayerFromLayerList(id));
+ });
+ }
newLayerList.forEach(layerDescriptor => {
dispatch(addLayer(layerDescriptor));
diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js
index fdc8ad2176d08..2995ea039e7a8 100644
--- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js
+++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js
@@ -235,12 +235,12 @@ export class MBMapContainer extends React.Component {
//clamping ot -89/89 latitudes since Mapboxgl does not seem to handle bounds that contain the poles (logs errors to the console when using -90/90)
const lnLatBounds = new mapboxgl.LngLatBounds(
new mapboxgl.LngLat(
- clampToLonBounds(goto.bounds.min_lon),
- clampToLatBounds(goto.bounds.min_lat)
+ clampToLonBounds(goto.bounds.minLon),
+ clampToLatBounds(goto.bounds.minLat)
),
new mapboxgl.LngLat(
- clampToLonBounds(goto.bounds.max_lon),
- clampToLatBounds(goto.bounds.max_lat)
+ clampToLonBounds(goto.bounds.maxLon),
+ clampToLatBounds(goto.bounds.maxLat)
)
);
//maxZoom ensure we're not zooming in too far on single points or small shapes
diff --git a/x-pack/legacy/plugins/maps/public/embeddable/README.md b/x-pack/legacy/plugins/maps/public/embeddable/README.md
index 1de327702fb87..8ce3794e2ed2c 100644
--- a/x-pack/legacy/plugins/maps/public/embeddable/README.md
+++ b/x-pack/legacy/plugins/maps/public/embeddable/README.md
@@ -30,17 +30,15 @@
### Creating a Map embeddable from state
```
const factory = new MapEmbeddableFactory();
-const state = {
- layerList: [], // where layerList is same as saved object layerListJSON property (unstringified)
- title: 'my map',
-}
const input = {
hideFilterActions: true,
isLayerTOCOpen: false,
openTOCDetails: ['tfi3f', 'edh66'],
mapCenter: { lat: 0.0, lon: 0.0, zoom: 7 }
}
-const mapEmbeddable = await factory.createFromState(state, input, parent);
+const mapEmbeddable = await factory.create(input, parent);
+// where layerList is same as saved object layerListJSON property (unstringified))
+mapEmbeddable.setLayerList([]);
```
#### Customize tooltip
@@ -62,7 +60,9 @@ const renderTooltipContent = ({ addFilters, closeTooltip, features, isLocked, lo
return Custom tooltip content
;
}
-const mapEmbeddable = await factory.createFromState(state, input, parent, renderTooltipContent);
+const mapEmbeddable = await factory.create(input, parent)
+mapEmbeddable.setLayerList(layerList);
+mapEmbeddable.setRenderTooltipContent(renderTooltipContent);
```
@@ -80,7 +80,10 @@ const eventHandlers = {
},
}
-const mapEmbeddable = await factory.createFromState(state, input, parent, renderTooltipContent, eventHandlers);
+const mapEmbeddable = await factory.create(input, parent);
+mapEmbeddable.setLayerList(layerList);
+mapEmbeddable.setRenderTooltipContent(renderTooltipContent);
+mapEmbeddable.setEventHandlers(eventHandlers);
```
@@ -90,55 +93,13 @@ Geojson sources will not update unless you modify `__featureCollection` property
```
const factory = new MapEmbeddableFactory();
-const state = {
- layerList: [
- {
- 'id': 'gaxya',
- 'label': 'My geospatial data',
- 'minZoom': 0,
- 'maxZoom': 24,
- 'alpha': 1,
- 'sourceDescriptor': {
- 'id': 'b7486',
- 'type': 'GEOJSON_FILE',
- '__featureCollection': {
- "type": "FeatureCollection",
- "features": [
- {
- "type": "Feature",
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [0, 0], [10, 10], [10, 0], [0, 0]
- ]
- ]
- },
- "properties": {
- "name": "null island",
- "another_prop": "something else interesting"
- }
- }
- ]
- }
- },
- 'visible': true,
- 'style': {
- 'type': 'VECTOR',
- 'properties': {}
- },
- 'type': 'VECTOR'
- }
- ],
- title: 'my map',
-}
const input = {
hideFilterActions: true,
isLayerTOCOpen: false,
openTOCDetails: ['tfi3f', 'edh66'],
mapCenter: { lat: 0.0, lon: 0.0, zoom: 7 }
}
-const mapEmbeddable = await factory.createFromState(state, input, parent);
+const mapEmbeddable = await factory.create(input, parent);
mapEmbeddable.setLayerList([
{
diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx
index 3c9069c7a836f..9544e8714f265 100644
--- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx
+++ b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx
@@ -129,6 +129,14 @@ export class MapEmbeddable extends Embeddable this.onContainerStateChanged(input));
}
+ setRenderTooltipContent = (renderTooltipContent: RenderToolTipContent) => {
+ this._renderTooltipContent = renderTooltipContent;
+ };
+
+ setEventHandlers = (eventHandlers: EventHandlers) => {
+ this._eventHandlers = eventHandlers;
+ };
+
getInspectorAdapters() {
return getInspectorAdapters(this._store.getState());
}
diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts
index b9cb66f831281..5a036ed47fb62 100644
--- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts
+++ b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts
@@ -14,8 +14,7 @@ import { IIndexPattern } from 'src/plugins/data/public';
import { MapEmbeddable, MapEmbeddableInput } from './map_embeddable';
import { getIndexPatternService } from '../kibana_services';
import {
- EmbeddableFactory,
- ErrorEmbeddable,
+ EmbeddableFactoryDefinition,
IContainer,
} from '../../../../../../src/plugins/embeddable/public';
@@ -28,25 +27,17 @@ import { getInitialLayers } from '../angular/get_initial_layers';
import { mergeInputWithSavedMap } from './merge_input_with_saved_map';
import '../angular/services/gis_map_saved_object_loader';
import { bindSetupCoreAndPlugins, bindStartCoreAndPlugins } from '../plugin';
-import { RenderToolTipContent } from '../layers/tooltips/tooltip_property';
-import {
- EventHandlers,
- // eslint-disable-next-line @kbn/eslint/no-restricted-paths
-} from '../../../../../plugins/maps/public/reducers/non_serializable_instances';
-export class MapEmbeddableFactory extends EmbeddableFactory {
+export class MapEmbeddableFactory implements EmbeddableFactoryDefinition {
type = MAP_SAVED_OBJECT_TYPE;
-
+ savedObjectMetaData = {
+ name: i18n.translate('xpack.maps.mapSavedObjectLabel', {
+ defaultMessage: 'Map',
+ }),
+ type: MAP_SAVED_OBJECT_TYPE,
+ getIconForSavedObject: () => APP_ICON,
+ };
constructor() {
- super({
- savedObjectMetaData: {
- name: i18n.translate('xpack.maps.mapSavedObjectLabel', {
- defaultMessage: 'Map',
- }),
- type: MAP_SAVED_OBJECT_TYPE,
- getIconForSavedObject: () => APP_ICON,
- },
- });
// Init required services. Necessary while in legacy
bindSetupCoreAndPlugins(npSetup.core, npSetup.plugins);
bindStartCoreAndPlugins(npStart.core, npStart.plugins);
@@ -103,11 +94,11 @@ export class MapEmbeddableFactory extends EmbeddableFactory {
return await savedObjectLoader.get(savedObjectId);
}
- async createFromSavedObject(
+ createFromSavedObject = async (
savedObjectId: string,
input: MapEmbeddableInput,
parent?: IContainer
- ) {
+ ) => {
const savedMap = await this._fetchSavedMap(savedObjectId);
const layerList = getInitialLayers(savedMap.layerListJSON);
const indexPatterns = await this._getIndexPatterns(layerList);
@@ -135,39 +126,23 @@ export class MapEmbeddableFactory extends EmbeddableFactory {
}
return embeddable;
- }
+ };
- async createFromState(
- state: { title?: string; layerList?: unknown[] },
- input: MapEmbeddableInput,
- parent: IContainer,
- renderTooltipContent: RenderToolTipContent,
- eventHandlers: EventHandlers
- ) {
- const layerList = state && state.layerList ? state.layerList : getInitialLayers();
+ create = async (input: MapEmbeddableInput, parent?: IContainer) => {
+ const layerList = getInitialLayers();
const indexPatterns = await this._getIndexPatterns(layerList);
return new MapEmbeddable(
{
layerList,
- title: state && state.title ? state.title : '',
+ title: input.title ?? '',
indexPatterns,
editable: false,
},
input,
- parent,
- renderTooltipContent,
- eventHandlers
- );
- }
-
- async create(input: MapEmbeddableInput) {
- window.location.href = chrome.addBasePath(createMapPath(''));
- return new ErrorEmbeddable(
- 'Maps can only be created with createFromSavedObject or createFromState',
- input
+ parent
);
- }
+ };
}
npSetup.plugins.embeddable.registerEmbeddableFactory(
diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.d.ts b/x-pack/legacy/plugins/maps/public/layers/layer.d.ts
index 777566298e607..de59642ede8ab 100644
--- a/x-pack/legacy/plugins/maps/public/layers/layer.d.ts
+++ b/x-pack/legacy/plugins/maps/public/layers/layer.d.ts
@@ -3,12 +3,13 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { LayerDescriptor } from '../../common/descriptor_types';
+import { LayerDescriptor, MapExtent, MapFilters } from '../../common/descriptor_types';
import { ISource } from './sources/source';
import { DataRequest } from './util/data_request';
import { SyncContext } from '../actions/map_actions';
export interface ILayer {
+ getBounds(mapFilters: MapFilters): Promise;
getDataRequest(id: string): DataRequest | undefined;
getDisplayName(source?: ISource): Promise;
getId(): string;
@@ -25,6 +26,7 @@ export interface ILayerArguments {
export class AbstractLayer implements ILayer {
constructor(layerArguments: ILayerArguments);
+ getBounds(mapFilters: MapFilters): Promise;
getDataRequest(id: string): DataRequest | undefined;
getDisplayName(source?: ISource): Promise;
getId(): string;
diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.js b/x-pack/legacy/plugins/maps/public/layers/layer.js
index d162e342dfd1a..e9616be89b601 100644
--- a/x-pack/legacy/plugins/maps/public/layers/layer.js
+++ b/x-pack/legacy/plugins/maps/public/layers/layer.js
@@ -320,12 +320,12 @@ export class AbstractLayer {
return sourceDataRequest && sourceDataRequest.hasData();
}
- async getBounds() {
+ async getBounds(/* mapFilters: MapFilters */) {
return {
- min_lon: -180,
- max_lon: 180,
- min_lat: -89,
- max_lat: 89,
+ minLon: -180,
+ maxLon: 180,
+ minLat: -89,
+ maxLat: 89,
};
}
diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js
index bf04a73cfba77..441d52d23398a 100644
--- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js
+++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js
@@ -175,10 +175,10 @@ export class AbstractESSource extends AbstractVectorSource {
}
return {
- min_lon: esBounds.top_left.lon,
- max_lon: esBounds.bottom_right.lon,
- min_lat: esBounds.bottom_right.lat,
- max_lat: esBounds.top_left.lat,
+ minLon: esBounds.top_left.lon,
+ maxLon: esBounds.bottom_right.lon,
+ minLat: esBounds.bottom_right.lat,
+ maxLat: esBounds.top_left.lat,
};
}
diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts
index fbec329447121..d597e64277186 100644
--- a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts
+++ b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts
@@ -8,7 +8,12 @@
import { FeatureCollection } from 'geojson';
import { AbstractSource, ISource } from './source';
import { IField } from '../fields/field';
-import { ESSearchSourceResponseMeta, VectorSourceSyncMeta } from '../../../common/descriptor_types';
+import {
+ ESSearchSourceResponseMeta,
+ MapExtent,
+ VectorSourceRequestMeta,
+ VectorSourceSyncMeta,
+} from '../../../common/descriptor_types';
export type GeoJsonFetchMeta = ESSearchSourceResponseMeta;
@@ -18,6 +23,7 @@ export type GeoJsonWithMeta = {
};
export interface IVectorSource extends ISource {
+ getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent;
getGeoJsonWithMeta(
layerName: 'string',
searchFilters: unknown[],
@@ -30,6 +36,7 @@ export interface IVectorSource extends ISource {
}
export class AbstractVectorSource extends AbstractSource implements IVectorSource {
+ getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent;
getGeoJsonWithMeta(
layerName: 'string',
searchFilters: unknown[],
diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js
index 6b89554546330..d606420909281 100644
--- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js
+++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js
@@ -167,10 +167,10 @@ export class VectorLayer extends AbstractLayer {
features: visibleFeatures,
});
return {
- min_lon: bbox[0],
- min_lat: bbox[1],
- max_lon: bbox[2],
- max_lat: bbox[3],
+ minLon: bbox[0],
+ minLat: bbox[1],
+ maxLon: bbox[2],
+ maxLat: bbox[3],
};
}
diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx
index c7e368da1338f..cbb4006bbf933 100644
--- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx
@@ -10,7 +10,10 @@ import { createPortalNode, InPortal } from 'react-reverse-portal';
import styled, { css } from 'styled-components';
import { npStart } from 'ui/new_platform';
-import { EmbeddablePanel } from '../../../../../../../src/plugins/embeddable/public';
+import {
+ EmbeddablePanel,
+ ErrorEmbeddable,
+} from '../../../../../../../src/plugins/embeddable/public';
import { DEFAULT_INDEX_KEY } from '../../../common/constants';
import { getIndexPatternTitleIdMapping } from '../../hooks/api/helpers';
import { useIndexPatterns } from '../../hooks/use_index_patterns';
@@ -84,7 +87,9 @@ export const EmbeddedMapComponent = ({
setQuery,
startDate,
}: EmbeddedMapProps) => {
- const [embeddable, setEmbeddable] = React.useState(null);
+ const [embeddable, setEmbeddable] = React.useState(
+ undefined
+ );
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
const [isIndexError, setIsIndexError] = useState(false);
diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx
index 0ffb13cd66028..f4e6ee5f878a6 100644
--- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx
@@ -20,8 +20,10 @@ jest.mock('ui/new_platform');
const { npStart } = createUiNewPlatformMock();
npStart.plugins.embeddable.getEmbeddableFactory = jest.fn().mockImplementation(() => ({
- createFromState: () => ({
+ create: () => ({
reload: jest.fn(),
+ setRenderTooltipContent: jest.fn(),
+ setLayerList: jest.fn(),
}),
}));
diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx
index 56211c9ff8935..0c7a1212ba280 100644
--- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx
@@ -11,10 +11,20 @@ import minimatch from 'minimatch';
import { IndexPatternMapping, SetQuery } from './types';
import { getLayerList } from './map_config';
import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../plugins/maps/public';
-import { MapEmbeddable, RenderTooltipContentParams } from '../../../../maps/public';
+import {
+ MapEmbeddable,
+ RenderTooltipContentParams,
+ MapEmbeddableInput,
+} from '../../../../maps/public';
import * as i18n from './translations';
import { Query, Filter } from '../../../../../../../src/plugins/data/public';
-import { EmbeddableStart, ViewMode } from '../../../../../../../src/plugins/embeddable/public';
+import {
+ EmbeddableStart,
+ isErrorEmbeddable,
+ EmbeddableOutput,
+ ViewMode,
+ ErrorEmbeddable,
+} from '../../../../../../../src/plugins/embeddable/public';
import { IndexPatternSavedObject } from '../../hooks/types';
/**
@@ -40,14 +50,19 @@ export const createEmbeddable = async (
setQuery: SetQuery,
portalNode: PortalNode,
embeddableApi: EmbeddableStart
-): Promise => {
- const factory = embeddableApi.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE);
+): Promise => {
+ const factory = embeddableApi.getEmbeddableFactory<
+ MapEmbeddableInput,
+ EmbeddableOutput,
+ MapEmbeddable
+ >(MAP_SAVED_OBJECT_TYPE);
- const state = {
- layerList: getLayerList(indexPatterns),
+ if (!factory) {
+ throw new Error('Map embeddable factory undefined');
+ }
+
+ const input: MapEmbeddableInput = {
title: i18n.MAP_TITLE,
- };
- const input = {
id: uuid.v4(),
filters,
hidePanelTitles: true,
@@ -86,13 +101,16 @@ export const createEmbeddable = async (
return ;
};
- // @ts-ignore method added in https://github.com/elastic/kibana/pull/43878
- const embeddableObject = await factory.createFromState(
- state,
- input,
- undefined,
- renderTooltipContent
- );
+ const embeddableObject = await factory.create(input);
+
+ if (!embeddableObject) {
+ throw new Error('Map embeddable is undefined');
+ }
+
+ if (!isErrorEmbeddable(embeddableObject)) {
+ embeddableObject.setRenderTooltipContent(renderTooltipContent);
+ embeddableObject.setLayerList(getLayerList(indexPatterns));
+ }
// Wire up to app refresh action
setQuery({
diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx
index 89227cdd56457..85d0b1b593704 100644
--- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx
+++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx
@@ -9,7 +9,12 @@ import uuid from 'uuid';
import styled from 'styled-components';
import { npStart } from 'ui/new_platform';
-import { ViewMode } from '../../../../../../../../../src/plugins/embeddable/public';
+import {
+ ViewMode,
+ EmbeddableOutput,
+ ErrorEmbeddable,
+ isErrorEmbeddable,
+} from '../../../../../../../../../src/plugins/embeddable/public';
import * as i18n from './translations';
import { MapEmbeddable, MapEmbeddableInput } from '../../../../../../maps/public';
import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../../../plugins/maps/public';
@@ -45,9 +50,13 @@ const EmbeddedPanel = styled.div`
export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProps) => {
const { colors } = useContext(UptimeThemeContext);
- const [embeddable, setEmbeddable] = useState();
+ const [embeddable, setEmbeddable] = useState();
const embeddableRoot: React.RefObject = useRef(null);
- const factory = npStart.plugins.embeddable.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE);
+ const factory = npStart.plugins.embeddable.getEmbeddableFactory<
+ MapEmbeddableInput,
+ EmbeddableOutput,
+ MapEmbeddable
+ >(MAP_SAVED_OBJECT_TYPE);
const input: MapEmbeddableInput = {
id: uuid.v4(),
@@ -76,12 +85,17 @@ export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProp
useEffect(() => {
async function setupEmbeddable() {
- const mapState = {
- layerList: getLayerList(upPoints, downPoints, colors),
+ if (!factory) {
+ throw new Error('Map embeddable not found.');
+ }
+ const embeddableObject = await factory.create({
+ ...input,
title: i18n.MAP_TITLE,
- };
- // @ts-ignore
- const embeddableObject = await factory.createFromState(mapState, input, undefined);
+ });
+
+ if (embeddableObject && !isErrorEmbeddable(embeddableObject)) {
+ embeddableObject.setLayerList(getLayerList(upPoints, downPoints, colors));
+ }
setEmbeddable(embeddableObject);
}
@@ -93,7 +107,7 @@ export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProp
// update map layers based on points
useEffect(() => {
- if (embeddable) {
+ if (embeddable && !isErrorEmbeddable(embeddable)) {
embeddable.setLayerList(getLayerList(upPoints, downPoints, colors));
}
}, [upPoints, downPoints, embeddable, colors]);
diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap
index 85988af04a939..9957f13fc1334 100644
--- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap
+++ b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap
@@ -3,7 +3,7 @@
exports[`ML JobLink renders without errors 1`] = `
diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout_container.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout_container.tsx
index 0a3226bc064a8..9eed24e2810d8 100644
--- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout_container.tsx
+++ b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout_container.tsx
@@ -50,13 +50,13 @@ const showMLJobNotification = (
),
- toastLifeTimeMs: 5000,
+ toastLifeTimeMs: 10000,
});
} else {
- notifications.toasts.warning({
+ notifications.toasts.danger({
title: {labels.JOB_CREATION_FAILED}
,
body: message ?? {labels.JOB_CREATION_FAILED_MESSAGE}
,
- toastLifeTimeMs: 5000,
+ toastLifeTimeMs: 10000,
});
}
};
@@ -119,7 +119,7 @@ export const MachineLearningFlyout: React.FC = ({ onClose }) => {
basePath,
{ to: dateRangeEnd, from: dateRangeStart },
false,
- error?.body?.message
+ error?.message || error?.body?.message
);
}
setIsCreatingJob(false);
diff --git a/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts b/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts
index 0b31a74a40133..bcd2582fe18b9 100644
--- a/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts
+++ b/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts
@@ -18,7 +18,7 @@ import {
import { DataRecognizerConfigResponse } from '../../../../../../plugins/ml/common/types/modules';
import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_recognizer';
-export const getMLJobId = (monitorId: string) => `${monitorId}_${ML_JOB_ID}`;
+export const getMLJobId = (monitorId: string) => `${monitorId}_${ML_JOB_ID}`.toLowerCase();
export const getMLCapabilities = async (): Promise => {
return await apiService.get(API_URLS.ML_CAPABILITIES);
@@ -34,8 +34,11 @@ export const createMLJob = async ({
}: MonitorIdParam & HeartbeatIndicesParam): Promise => {
const url = API_URLS.ML_SETUP_MODULE + ML_MODULE_ID;
+ // ML App doesn't support upper case characters in job name
+ const lowerCaseMonitorId = monitorId.toLowerCase();
+
const data = {
- prefix: `${monitorId}_`,
+ prefix: `${lowerCaseMonitorId}_`,
useDedicatedIndex: false,
startDatafeed: true,
start: moment()
@@ -47,7 +50,7 @@ export const createMLJob = async ({
filter: [
{
term: {
- 'monitor.id': monitorId,
+ 'monitor.id': lowerCaseMonitorId,
},
},
],
@@ -56,11 +59,17 @@ export const createMLJob = async ({
};
const response: DataRecognizerConfigResponse = await apiService.post(url, data);
- if (response?.jobs?.[0]?.id === getMLJobId(monitorId) && response?.jobs?.[0]?.success) {
- return {
- count: 1,
- jobId: response?.jobs?.[0]?.id,
- };
+ if (response?.jobs?.[0]?.id === getMLJobId(monitorId)) {
+ const jobResponse = response.jobs[0];
+ if (jobResponse.success) {
+ return {
+ count: 1,
+ jobId: jobResponse.id,
+ };
+ } else {
+ const { error } = jobResponse;
+ throw new Error(error?.msg);
+ }
} else {
return null;
}
diff --git a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.test.ts b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.test.ts
index 91b829a020048..10860fe471a3b 100644
--- a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.test.ts
+++ b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.test.ts
@@ -10,9 +10,7 @@ import { skip } from 'rxjs/operators';
import * as Rx from 'rxjs';
import { mount } from 'enzyme';
-import { EmbeddableFactory } from '../../../../src/plugins/embeddable/public';
import { TimeRangeEmbeddable, TimeRangeContainer, TIME_RANGE_EMBEDDABLE } from './test_helpers';
-import { TimeRangeEmbeddableFactory } from './test_helpers/time_range_embeddable_factory';
import { CustomTimeRangeAction } from './custom_time_range_action';
/* eslint-disable */
import {
@@ -21,7 +19,6 @@ import {
/* eslint-enable */
import {
- HelloWorldEmbeddableFactory,
HelloWorldEmbeddable,
HELLO_WORLD_EMBEDDABLE,
} from '../../../../examples/embeddable_examples/public';
@@ -38,9 +35,6 @@ const createOpenModalMock = () => {
};
test('Custom time range action prevents embeddable from using container time', async done => {
- const embeddableFactories = new Map();
- embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
-
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
@@ -105,9 +99,6 @@ test('Custom time range action prevents embeddable from using container time', a
});
test('Removing custom time range action resets embeddable back to container time', async done => {
- const embeddableFactories = new Map();
- embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
-
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
@@ -182,9 +173,6 @@ test('Removing custom time range action resets embeddable back to container time
});
test('Cancelling custom time range action leaves state alone', async done => {
- const embeddableFactories = new Map();
- embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
-
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
@@ -244,8 +232,6 @@ test('Cancelling custom time range action leaves state alone', async done => {
});
test(`badge is compatible with embeddable that inherits from parent`, async () => {
- const embeddableFactories = new Map();
- embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
@@ -279,8 +265,6 @@ test(`badge is compatible with embeddable that inherits from parent`, async () =
// TODO: uncomment when https://github.com/elastic/kibana/issues/43271 is fixed.
// test('Embeddable that does not use time range in a container that has time range is incompatible', async () => {
-// const embeddableFactories = new Map();
-// embeddableFactories.set(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory());
// const container = new TimeRangeContainer(
// {
// timeRange: { from: 'now-15m', to: 'now' },
@@ -315,8 +299,6 @@ test(`badge is compatible with embeddable that inherits from parent`, async () =
// });
test('Attempting to execute on incompatible embeddable throws an error', async () => {
- const embeddableFactories = new Map();
- embeddableFactories.set(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory());
const container = new HelloWorldContainer(
{
panels: {
diff --git a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.test.ts b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.test.ts
index d2b9fa9ac1655..3bf763470f002 100644
--- a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.test.ts
+++ b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.test.ts
@@ -9,17 +9,12 @@ import { findTestSubject } from '@elastic/eui/lib/test';
import { skip } from 'rxjs/operators';
import * as Rx from 'rxjs';
import { mount } from 'enzyme';
-import { EmbeddableFactory } from '../../../../src/plugins/embeddable/public';
import { TimeRangeEmbeddable, TimeRangeContainer, TIME_RANGE_EMBEDDABLE } from './test_helpers';
-import { TimeRangeEmbeddableFactory } from './test_helpers/time_range_embeddable_factory';
import { CustomTimeRangeBadge } from './custom_time_range_badge';
import { ReactElement } from 'react';
import { nextTick } from 'test_utils/enzyme_helpers';
test('Removing custom time range from badge resets embeddable back to container time', async done => {
- const embeddableFactories = new Map();
- embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
-
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
@@ -79,8 +74,6 @@ test('Removing custom time range from badge resets embeddable back to container
});
test(`badge is not compatible with embeddable that inherits from parent`, async () => {
- const embeddableFactories = new Map();
- embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
@@ -113,8 +106,6 @@ test(`badge is not compatible with embeddable that inherits from parent`, async
});
test(`badge is compatible with embeddable that has custom time range`, async () => {
- const embeddableFactories = new Map();
- embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
@@ -148,8 +139,6 @@ test(`badge is compatible with embeddable that has custom time range`, async ()
});
test('Attempting to execute on incompatible embeddable throws an error', async () => {
- const embeddableFactories = new Map();
- embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
diff --git a/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts b/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts
index 311d3357476b9..e8d9451f9f2a6 100644
--- a/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts
+++ b/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts
@@ -7,7 +7,7 @@
import {
EmbeddableInput,
IContainer,
- EmbeddableFactory,
+ EmbeddableFactoryDefinition,
} from '../../../../../src/plugins/embeddable/public';
import { TimeRange } from '../../../../../src/plugins/data/public';
import { TIME_RANGE_EMBEDDABLE, TimeRangeEmbeddable } from './time_range_embeddable';
@@ -16,7 +16,8 @@ interface EmbeddableTimeRangeInput extends EmbeddableInput {
timeRange: TimeRange;
}
-export class TimeRangeEmbeddableFactory extends EmbeddableFactory {
+export class TimeRangeEmbeddableFactory
+ implements EmbeddableFactoryDefinition {
public readonly type = TIME_RANGE_EMBEDDABLE;
public async isEditable() {
diff --git a/x-pack/plugins/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts
index 75351bb3bf07d..430ba1d422b96 100644
--- a/x-pack/plugins/endpoint/common/generate_data.ts
+++ b/x-pack/plugins/endpoint/common/generate_data.ts
@@ -83,6 +83,11 @@ const OTHER_EVENT_CATEGORIES: EventInfo[] = [
];
interface HostInfo {
+ elastic: {
+ agent: {
+ id: string;
+ };
+ };
agent: {
version: string;
id: string;
@@ -116,6 +121,11 @@ export class EndpointDocGenerator {
version: this.randomVersion(),
id: this.seededUUIDv4(),
},
+ elastic: {
+ agent: {
+ id: this.seededUUIDv4(),
+ },
+ },
host: {
id: this.seededUUIDv4(),
hostname: this.randomHostname(),
diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts
index b3eb518e35ae3..565f47e7a0d6f 100644
--- a/x-pack/plugins/endpoint/common/types.ts
+++ b/x-pack/plugins/endpoint/common/types.ts
@@ -257,6 +257,11 @@ export type HostMetadata = Immutable<{
event: {
created: number;
};
+ elastic: {
+ agent: {
+ id: string;
+ };
+ };
endpoint: {
policy: {
id: string;
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/host_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/host_accordion.tsx
index edaba3725e027..e332c96192fab 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/host_accordion.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/host_accordion.tsx
@@ -6,32 +6,60 @@
import React, { memo, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiAccordion, EuiDescriptionList } from '@elastic/eui';
-import { Immutable, AlertData } from '../../../../../../../common/types';
+import { EuiHealth } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { Immutable, AlertDetails } from '../../../../../../../common/types';
-export const HostAccordion = memo(({ alertData }: { alertData: Immutable }) => {
+export const HostAccordion = memo(({ alertData }: { alertData: Immutable }) => {
const columns = useMemo(() => {
return [
{
- title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.hostName', {
- defaultMessage: 'Host Name',
+ title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.hostNameCurrent', {
+ defaultMessage: 'Host Name (Current)',
+ }),
+ description: alertData.state.host_metadata.host.hostname,
+ },
+ {
+ title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.hostNameOriginal', {
+ defaultMessage: 'Host Name (At time of alert)',
}),
description: alertData.host.hostname,
},
{
- title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.hostIP', {
- defaultMessage: 'Host IP',
+ title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.hostIPCurrent', {
+ defaultMessage: 'Host IP (Current)',
+ }),
+ description: alertData.state.host_metadata.host.ip.join(', '),
+ },
+ {
+ title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.hostIPOriginal', {
+ defaultMessage: 'Host IP (At time of alert)',
}),
description: alertData.host.ip.join(', '),
},
{
- title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.status', {
- defaultMessage: 'Status',
+ title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.currentStatus', {
+ defaultMessage: 'Current Status',
+ }),
+ description: (
+
+ {' '}
+
+
+ ),
+ },
+ {
+ title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.osCurrent', {
+ defaultMessage: 'OS (Current)',
}),
- description: 'TODO',
+ description: alertData.state.host_metadata.host.os.name,
},
{
- title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.os', {
- defaultMessage: 'OS',
+ title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.osOriginal', {
+ defaultMessage: 'OS (At time of alert)',
}),
description: alertData.host.os.name,
},
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts
index c8e038869efcd..601f8a6bdc2c1 100644
--- a/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts
+++ b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts
@@ -6,13 +6,13 @@
import { i18n } from '@kbn/i18n';
import {
- EmbeddableFactory,
IContainer,
EmbeddableInput,
+ EmbeddableFactoryDefinition,
} from '../../../../../../src/plugins/embeddable/public';
import { ResolverEmbeddable } from './embeddable';
-export class ResolverEmbeddableFactory extends EmbeddableFactory {
+export class ResolverEmbeddableFactory implements EmbeddableFactoryDefinition {
public readonly type = 'resolver';
public async isEditable() {
diff --git a/x-pack/plugins/endpoint/server/test_data/all_metadata_data.json b/x-pack/plugins/endpoint/server/test_data/all_metadata_data.json
index 3c824185ec083..3c8486aa127ea 100644
--- a/x-pack/plugins/endpoint/server/test_data/all_metadata_data.json
+++ b/x-pack/plugins/endpoint/server/test_data/all_metadata_data.json
@@ -23,6 +23,11 @@
"event" : {
"created" : "2020-01-23T21:56:55.336Z"
},
+ "elastic": {
+ "agent": {
+ "id": "56a75650-3c8a-4e4f-ac17-6dd729c650e2"
+ }
+ },
"endpoint" : {
"policy" : {
"id" : "C2A9093E-E289-4C0A-AA44-8C32A414FA7A"
@@ -73,6 +78,11 @@
"event" : {
"created" : "2020-01-23T21:56:55.336Z"
},
+ "elastic": {
+ "agent": {
+ "id": "56a75650-3c8a-4e4f-ac17-6dd729c650e2"
+ }
+ },
"endpoint" : {
"policy" : {
"id" : "C2A9093E-E289-4C0A-AA44-8C32A414FA7A"
@@ -115,6 +125,11 @@
"event" : {
"created" : "2020-01-23T21:56:55.336Z"
},
+ "elastic": {
+ "agent": {
+ "id": "c2d84d8f-d355-40de-8b54-5d318d4d1312"
+ }
+ },
"endpoint" : {
"policy" : {
"id" : "C2A9093E-E289-4C0A-AA44-8C32A414FA7A"
@@ -165,6 +180,11 @@
"event" : {
"created" : "2020-01-23T21:56:55.336Z"
},
+ "elastic": {
+ "agent": {
+ "id": "c2d84d8f-d355-40de-8b54-5d318d4d1312"
+ }
+ },
"endpoint" : {
"policy" : {
"id" : "C2A9093E-E289-4C0A-AA44-8C32A414FA7A"
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts
index 7e3e1fba9c44a..397a78354f470 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts
@@ -16,7 +16,7 @@ import {
import { IndexManagementHome } from '../../../public/application/sections/home'; // eslint-disable-line @kbn/eslint/no-restricted-paths
import { BASE_PATH } from '../../../common/constants';
import { indexManagementStore } from '../../../public/application/store'; // eslint-disable-line @kbn/eslint/no-restricted-paths
-import { Template } from '../../../common/types';
+import { TemplateDeserialized } from '../../../common';
import { WithAppDependencies, services } from './setup_environment';
const testBedConfig: TestBedConfig = {
@@ -36,10 +36,13 @@ export interface IdxMgmtHomeTestBed extends TestBed {
selectHomeTab: (tab: 'indicesTab' | 'templatesTab') => void;
selectDetailsTab: (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => void;
clickReloadButton: () => void;
- clickTemplateAction: (name: Template['name'], action: 'edit' | 'clone' | 'delete') => void;
+ clickTemplateAction: (
+ name: TemplateDeserialized['name'],
+ action: 'edit' | 'clone' | 'delete'
+ ) => void;
clickTemplateAt: (index: number) => void;
clickCloseDetailsButton: () => void;
- clickActionMenu: (name: Template['name']) => void;
+ clickActionMenu: (name: TemplateDeserialized['name']) => void;
};
}
@@ -78,7 +81,7 @@ export const setup = async (): Promise => {
find('reloadButton').simulate('click');
};
- const clickActionMenu = async (templateName: Template['name']) => {
+ const clickActionMenu = async (templateName: TemplateDeserialized['name']) => {
const { component } = testBed;
// When a table has > 2 actions, EUI displays an overflow menu with an id "-actions"
@@ -87,7 +90,7 @@ export const setup = async (): Promise => {
};
const clickTemplateAction = (
- templateName: Template['name'],
+ templateName: TemplateDeserialized['name'],
action: 'edit' | 'clone' | 'delete'
) => {
const actions = ['edit', 'clone', 'delete'];
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts
index 9d4eb631a1c40..520b62083e7d3 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { TestBed, SetupFunc, UnwrapPromise } from '../../../../../test_utils';
-import { Template } from '../../../common/types';
+import { TemplateDeserialized } from '../../../common';
import { nextTick } from './index';
interface MappingField {
@@ -62,8 +62,8 @@ export const formSetup = async (initTestBed: SetupFunc) => {
indexPatterns,
order,
version,
- }: Partial = {}) => {
- const { form, find, component } = testBed;
+ }: Partial = {}) => {
+ const { form, find, waitFor } = testBed;
if (name) {
form.setInputValue('nameField.input', name);
@@ -88,12 +88,11 @@ export const formSetup = async (initTestBed: SetupFunc) => {
}
clickNextButton();
- await nextTick();
- component.update();
+ await waitFor('stepSettings');
};
const completeStepTwo = async (settings?: string) => {
- const { find, component } = testBed;
+ const { find, component, waitFor } = testBed;
if (settings) {
find('mockCodeEditor').simulate('change', {
@@ -104,42 +103,41 @@ export const formSetup = async (initTestBed: SetupFunc) => {
}
clickNextButton();
- await nextTick();
- component.update();
+ await waitFor('stepMappings');
};
const completeStepThree = async (mappingFields?: MappingField[]) => {
- const { component } = testBed;
+ const { waitFor } = testBed;
if (mappingFields) {
for (const field of mappingFields) {
const { name, type } = field;
await addMappingField(name, type);
}
- } else {
- await nextTick();
}
- await nextTick(50); // hooks updates cycles are tricky, adding some latency is needed
clickNextButton();
- await nextTick(50);
- component.update();
+ await waitFor('stepAliases');
};
- const completeStepFour = async (aliases?: string) => {
- const { find, component } = testBed;
+ const completeStepFour = async (aliases?: string, waitForNextStep = true) => {
+ const { find, component, waitFor } = testBed;
if (aliases) {
find('mockCodeEditor').simulate('change', {
jsonString: aliases,
}); // Using mocked EuiCodeEditor
- await nextTick(50);
+ await nextTick();
component.update();
}
clickNextButton();
- await nextTick(50);
- component.update();
+
+ if (waitForNextStep) {
+ await waitFor('summaryTab');
+ } else {
+ component.update();
+ }
};
const selectSummaryTab = (tab: 'summary' | 'request') => {
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home.test.ts
index 9e8af02b74631..a987535e0c291 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home.test.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home.test.ts
@@ -115,11 +115,13 @@ describe('', () => {
const template1 = fixtures.getTemplate({
name: `a${getRandomString()}`,
indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
- settings: {
- index: {
- number_of_shards: '1',
- lifecycle: {
- name: 'my_ilm_policy',
+ template: {
+ settings: {
+ index: {
+ number_of_shards: '1',
+ lifecycle: {
+ name: 'my_ilm_policy',
+ },
},
},
},
@@ -302,7 +304,10 @@ describe('', () => {
const templateId = rows[0].columns[2].value;
- const { name: templateName } = template1;
+ const {
+ name: templateName,
+ _kbnMeta: { formatVersion },
+ } = template1;
await actions.clickTemplateAction(templateName, 'delete');
const modal = document.body.querySelector(
@@ -327,8 +332,11 @@ describe('', () => {
const latestRequest = server.requests[server.requests.length - 1];
- expect(latestRequest.method).toBe('DELETE');
- expect(latestRequest.url).toBe(`${API_BASE_PATH}/templates/${template1.name}`);
+ expect(latestRequest.method).toBe('POST');
+ expect(latestRequest.url).toBe(`${API_BASE_PATH}/delete-templates`);
+ expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({
+ templates: [{ name: template1.name, formatVersion }],
+ });
});
});
@@ -396,24 +404,26 @@ describe('', () => {
const template = fixtures.getTemplate({
name: `a${getRandomString()}`,
indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
- settings: {
- index: {
- number_of_shards: '1',
- },
- },
- mappings: {
- _source: {
- enabled: false,
+ template: {
+ settings: {
+ index: {
+ number_of_shards: '1',
+ },
},
- properties: {
- created_at: {
- type: 'date',
- format: 'EEE MMM dd HH:mm:ss Z yyyy',
+ mappings: {
+ _source: {
+ enabled: false,
+ },
+ properties: {
+ created_at: {
+ type: 'date',
+ format: 'EEE MMM dd HH:mm:ss Z yyyy',
+ },
},
},
- },
- aliases: {
- alias1: {},
+ aliases: {
+ alias1: {},
+ },
},
});
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/template_clone.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/template_clone.test.tsx
index 5d895c8e98624..17e19bf881dee 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/template_clone.test.tsx
+++ b/x-pack/plugins/index_management/__jest__/client_integration/template_clone.test.tsx
@@ -54,21 +54,17 @@ describe('', () => {
const templateToClone = getTemplate({
name: TEMPLATE_NAME,
indexPatterns: ['indexPattern1'],
- mappings: {
- ...MAPPINGS,
- _meta: {},
- _source: {},
+ template: {
+ mappings: MAPPINGS,
},
});
beforeEach(async () => {
httpRequestsMockHelpers.setLoadTemplateResponse(templateToClone);
- testBed = await setup();
-
await act(async () => {
- await nextTick();
- testBed.component.update();
+ testBed = await setup();
+ await testBed.waitFor('templateForm');
});
});
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx
index 981067c09f8aa..ad8e8c22a87fa 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx
+++ b/x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx
@@ -6,6 +6,7 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
+import { DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../common';
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
import { TemplateFormTestBed } from './helpers/template_form.helpers';
import {
@@ -71,6 +72,7 @@ describe('', () => {
beforeEach(async () => {
await act(async () => {
testBed = await setup();
+ await testBed.waitFor('templateForm');
});
});
@@ -100,6 +102,7 @@ describe('', () => {
beforeEach(async () => {
await act(async () => {
testBed = await setup();
+ await testBed.waitFor('templateForm');
});
});
@@ -209,7 +212,7 @@ describe('', () => {
await act(async () => {
// Complete step 4 (aliases) with invalid json
- await actions.completeStepFour('{ invalidJsonString ');
+ await actions.completeStepFour('{ invalidJsonString ', false);
});
expect(form.getErrorsMessages()).toContain('Invalid JSON format.');
@@ -221,6 +224,7 @@ describe('', () => {
beforeEach(async () => {
await act(async () => {
testBed = await setup();
+ await testBed.waitFor('templateForm');
const { actions } = testBed;
@@ -275,6 +279,7 @@ describe('', () => {
it('should render a warning message if a wildcard is used as an index pattern', async () => {
await act(async () => {
testBed = await setup();
+ await testBed.waitFor('templateForm');
const { actions } = testBed;
// Complete step 1 (logistics)
@@ -308,6 +313,7 @@ describe('', () => {
await act(async () => {
testBed = await setup();
+ await testBed.waitFor('templateForm');
const { actions } = testBed;
// Complete step 1 (logistics)
@@ -323,7 +329,6 @@ describe('', () => {
await actions.completeStepThree(MAPPING_FIELDS);
// Complete step 4 (aliases)
- await nextTick(100);
await actions.completeStepFour(JSON.stringify(ALIASES));
});
});
@@ -338,29 +343,34 @@ describe('', () => {
const latestRequest = server.requests[server.requests.length - 1];
- const expected = JSON.stringify({
+ const expected = {
isManaged: false,
name: TEMPLATE_NAME,
indexPatterns: DEFAULT_INDEX_PATTERNS,
- settings: SETTINGS,
- mappings: {
- ...MAPPINGS,
- properties: {
- [BOOLEAN_MAPPING_FIELD.name]: {
- type: BOOLEAN_MAPPING_FIELD.type,
- },
- [TEXT_MAPPING_FIELD.name]: {
- type: TEXT_MAPPING_FIELD.type,
- },
- [KEYWORD_MAPPING_FIELD.name]: {
- type: KEYWORD_MAPPING_FIELD.type,
+ template: {
+ settings: SETTINGS,
+ mappings: {
+ ...MAPPINGS,
+ properties: {
+ [BOOLEAN_MAPPING_FIELD.name]: {
+ type: BOOLEAN_MAPPING_FIELD.type,
+ },
+ [TEXT_MAPPING_FIELD.name]: {
+ type: TEXT_MAPPING_FIELD.type,
+ },
+ [KEYWORD_MAPPING_FIELD.name]: {
+ type: KEYWORD_MAPPING_FIELD.type,
+ },
},
},
+ aliases: ALIASES,
},
- aliases: ALIASES,
- });
+ _kbnMeta: {
+ formatVersion: DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT,
+ },
+ };
- expect(JSON.parse(latestRequest.requestBody).body).toEqual(expected);
+ expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected);
});
it('should surface the API errors from the put HTTP request', async () => {
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/template_edit.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/template_edit.test.tsx
index 537b0d8ef4156..5b10ff226022d 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/template_edit.test.tsx
+++ b/x-pack/plugins/index_management/__jest__/client_integration/template_edit.test.tsx
@@ -99,17 +99,17 @@ describe('', () => {
const templateToEdit = fixtures.getTemplate({
name: TEMPLATE_NAME,
indexPatterns: ['indexPattern1'],
- mappings: MAPPING,
+ template: {
+ mappings: MAPPING,
+ },
});
beforeEach(async () => {
httpRequestsMockHelpers.setLoadTemplateResponse(templateToEdit);
- testBed = await setup();
-
await act(async () => {
- await nextTick();
- testBed.component.update();
+ testBed = await setup();
+ await testBed.waitFor('templateForm');
});
});
@@ -128,10 +128,9 @@ describe('', () => {
expect(nameInput.props().disabled).toEqual(true);
});
- // TODO: Flakey test
- describe.skip('form payload', () => {
+ describe('form payload', () => {
beforeEach(async () => {
- const { actions, component, find, form } = testBed;
+ const { actions } = testBed;
await act(async () => {
// Complete step 1 (logistics)
@@ -141,20 +140,32 @@ describe('', () => {
// Step 2 (index settings)
await actions.completeStepTwo(JSON.stringify(SETTINGS));
+ });
+ });
+
+ it('should send the correct payload with changed values', async () => {
+ const { actions, component, find, form } = testBed;
- // Step 3 (mappings)
- // Select the first field to edit
- actions.clickEditButtonAtField(0);
+ await act(async () => {
+ // Make some changes to the mappings (step 3)
+
+ actions.clickEditButtonAtField(0); // Select the first field to edit
await nextTick();
component.update();
- // verify edit field flyout
- expect(find('mappingsEditorFieldEdit').length).toEqual(1);
- // change field name
+ });
+
+ // verify edit field flyout
+ expect(find('mappingsEditorFieldEdit').length).toEqual(1);
+
+ await act(async () => {
+ // change the field name
form.setInputValue('nameParameterInput', UPDATED_MAPPING_TEXT_FIELD_NAME);
+
// Save changes
actions.clickEditFieldUpdateButton();
await nextTick();
component.update();
+
// Proceed to the next step
actions.clickNextButton();
await nextTick(50);
@@ -162,19 +173,13 @@ describe('', () => {
// Step 4 (aliases)
await actions.completeStepFour(JSON.stringify(ALIASES));
- });
- });
- it('should send the correct payload with changed values', async () => {
- const { actions } = testBed;
-
- await act(async () => {
+ // Submit the form
actions.clickSubmitButton();
await nextTick();
});
const latestRequest = server.requests[server.requests.length - 1];
-
const { version, order } = templateToEdit;
const expected = {
@@ -182,27 +187,31 @@ describe('', () => {
version,
order,
indexPatterns: UPDATED_INDEX_PATTERN,
- mappings: {
- ...MAPPING,
- _meta: {},
- _source: {},
- properties: {
- [UPDATED_MAPPING_TEXT_FIELD_NAME]: {
- type: 'text',
- store: false,
- index: true,
- fielddata: false,
- eager_global_ordinals: false,
- index_phrases: false,
- norms: true,
- index_options: 'positions',
+ template: {
+ mappings: {
+ ...MAPPING,
+ properties: {
+ [UPDATED_MAPPING_TEXT_FIELD_NAME]: {
+ type: 'text',
+ store: false,
+ index: true,
+ fielddata: false,
+ eager_global_ordinals: false,
+ index_phrases: false,
+ norms: true,
+ index_options: 'positions',
+ },
},
},
+ settings: SETTINGS,
+ aliases: ALIASES,
},
isManaged: false,
- settings: SETTINGS,
- aliases: ALIASES,
+ _kbnMeta: {
+ formatVersion: templateToEdit._kbnMeta.formatVersion,
+ },
};
+
expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected);
});
});
diff --git a/x-pack/plugins/index_management/common/constants/index.ts b/x-pack/plugins/index_management/common/constants/index.ts
index d1700f0e611c0..966e2e8e64838 100644
--- a/x-pack/plugins/index_management/common/constants/index.ts
+++ b/x-pack/plugins/index_management/common/constants/index.ts
@@ -9,6 +9,7 @@ export { BASE_PATH } from './base_path';
export { API_BASE_PATH } from './api_base_path';
export { INVALID_INDEX_PATTERN_CHARS, INVALID_TEMPLATE_NAME_CHARS } from './invalid_characters';
export * from './index_statuses';
+export { DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from './index_templates';
export {
UIM_APP_NAME,
diff --git a/x-pack/plugins/index_management/common/constants/index_templates.ts b/x-pack/plugins/index_management/common/constants/index_templates.ts
new file mode 100644
index 0000000000000..788e96ee895ed
--- /dev/null
+++ b/x-pack/plugins/index_management/common/constants/index_templates.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+/**
+ * Up until the end of the 8.x release cycle we need to support both
+ * V1 and V2 index template formats. This constant keeps track of whether
+ * we create V1 or V2 index template format in the UI.
+ */
+export const DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT = 1;
diff --git a/x-pack/plugins/index_management/common/index.ts b/x-pack/plugins/index_management/common/index.ts
index 0cc4ba79711ce..459eda7552c85 100644
--- a/x-pack/plugins/index_management/common/index.ts
+++ b/x-pack/plugins/index_management/common/index.ts
@@ -4,4 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { PLUGIN, API_BASE_PATH } from './constants';
+export { PLUGIN, API_BASE_PATH, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from './constants';
+
+export { getTemplateParameter } from './lib';
+
+export * from './types';
diff --git a/x-pack/plugins/index_management/common/lib/index.ts b/x-pack/plugins/index_management/common/lib/index.ts
index 83b22d8d72e92..33f7fbe45182e 100644
--- a/x-pack/plugins/index_management/common/lib/index.ts
+++ b/x-pack/plugins/index_management/common/lib/index.ts
@@ -5,6 +5,8 @@
*/
export {
deserializeTemplateList,
- deserializeTemplate,
- serializeTemplate,
+ deserializeV1Template,
+ serializeV1Template,
} from './template_serialization';
+
+export { getTemplateParameter } from './utils';
diff --git a/x-pack/plugins/index_management/common/lib/template_serialization.ts b/x-pack/plugins/index_management/common/lib/template_serialization.ts
index b7d3410c775d4..33a83d1e9335b 100644
--- a/x-pack/plugins/index_management/common/lib/template_serialization.ts
+++ b/x-pack/plugins/index_management/common/lib/template_serialization.ts
@@ -3,46 +3,25 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { Template, TemplateEs, TemplateListItem } from '../types';
+import {
+ TemplateDeserialized,
+ TemplateV1Serialized,
+ TemplateV2Serialized,
+ TemplateListItem,
+} from '../types';
const hasEntries = (data: object = {}) => Object.entries(data).length > 0;
-export function deserializeTemplateList(
- indexTemplatesByName: any,
- managedTemplatePrefix?: string
-): TemplateListItem[] {
- const indexTemplateNames: string[] = Object.keys(indexTemplatesByName);
-
- const deserializedTemplates: TemplateListItem[] = indexTemplateNames.map((name: string) => {
- const {
- version,
- order,
- index_patterns: indexPatterns = [],
- settings = {},
- aliases = {},
- mappings = {},
- } = indexTemplatesByName[name];
-
- return {
- name,
- version,
- order,
- indexPatterns: indexPatterns.sort(),
- hasSettings: hasEntries(settings),
- hasAliases: hasEntries(aliases),
- hasMappings: hasEntries(mappings),
- ilmPolicy: settings && settings.index && settings.index.lifecycle,
- isManaged: Boolean(managedTemplatePrefix && name.startsWith(managedTemplatePrefix)),
- };
- });
-
- return deserializedTemplates;
-}
-
-export function serializeTemplate(template: Template): TemplateEs {
- const { name, version, order, indexPatterns, settings, aliases, mappings } = template;
+export function serializeV1Template(template: TemplateDeserialized): TemplateV1Serialized {
+ const {
+ name,
+ version,
+ order,
+ indexPatterns,
+ template: { settings, aliases, mappings } = {} as TemplateDeserialized['template'],
+ } = template;
- const serializedTemplate: TemplateEs = {
+ const serializedTemplate: TemplateV1Serialized = {
name,
version,
order,
@@ -55,31 +34,88 @@ export function serializeTemplate(template: Template): TemplateEs {
return serializedTemplate;
}
-export function deserializeTemplate(
- templateEs: TemplateEs,
+export function serializeV2Template(template: TemplateDeserialized): TemplateV2Serialized {
+ const { aliases, mappings, settings, ...templateV1serialized } = serializeV1Template(template);
+
+ return {
+ ...templateV1serialized,
+ template: {
+ aliases,
+ mappings,
+ settings,
+ },
+ priority: template.priority,
+ composed_of: template.composedOf,
+ };
+}
+
+export function deserializeV2Template(
+ templateEs: TemplateV2Serialized,
managedTemplatePrefix?: string
-): Template {
+): TemplateDeserialized {
const {
name,
version,
order,
index_patterns: indexPatterns,
- settings,
- aliases,
- mappings,
+ template,
+ priority,
+ composed_of: composedOf,
} = templateEs;
+ const { settings } = template;
- const deserializedTemplate: Template = {
+ const deserializedTemplate: TemplateDeserialized = {
name,
version,
order,
indexPatterns: indexPatterns.sort(),
- settings,
- aliases,
- mappings,
+ template,
ilmPolicy: settings && settings.index && settings.index.lifecycle,
isManaged: Boolean(managedTemplatePrefix && name.startsWith(managedTemplatePrefix)),
+ priority,
+ composedOf,
+ _kbnMeta: {
+ formatVersion: 2,
+ },
};
return deserializedTemplate;
}
+
+export function deserializeV1Template(
+ templateEs: TemplateV1Serialized,
+ managedTemplatePrefix?: string
+): TemplateDeserialized {
+ const { settings, aliases, mappings, ...rest } = templateEs;
+
+ const deserializedTemplateV2 = deserializeV2Template(
+ { ...rest, template: { aliases, settings, mappings } },
+ managedTemplatePrefix
+ );
+
+ return {
+ ...deserializedTemplateV2,
+ _kbnMeta: {
+ formatVersion: 1,
+ },
+ };
+}
+
+export function deserializeTemplateList(
+ indexTemplatesByName: { [key: string]: Omit },
+ managedTemplatePrefix?: string
+): TemplateListItem[] {
+ return Object.entries(indexTemplatesByName).map(([name, templateSerialized]) => {
+ const {
+ template: { mappings, settings, aliases },
+ ...deserializedTemplate
+ } = deserializeV1Template({ name, ...templateSerialized }, managedTemplatePrefix);
+
+ return {
+ ...deserializedTemplate,
+ hasSettings: hasEntries(settings),
+ hasAliases: hasEntries(aliases),
+ hasMappings: hasEntries(mappings),
+ };
+ });
+}
diff --git a/x-pack/plugins/index_management/common/lib/utils.test.ts b/x-pack/plugins/index_management/common/lib/utils.test.ts
new file mode 100644
index 0000000000000..221d1b009cede
--- /dev/null
+++ b/x-pack/plugins/index_management/common/lib/utils.test.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { TemplateV1Serialized, TemplateV2Serialized } from '../types';
+import { getTemplateVersion } from './utils';
+
+describe('utils', () => {
+ describe('getTemplateVersion', () => {
+ test('should detect v1 template', () => {
+ const template = {
+ name: 'my_template',
+ index_patterns: ['logs*'],
+ mappings: {
+ properties: {},
+ },
+ };
+ expect(getTemplateVersion(template as TemplateV1Serialized)).toBe(1);
+ });
+
+ test('should detect v2 template', () => {
+ const template = {
+ name: 'my_template',
+ index_patterns: ['logs*'],
+ template: {
+ mappings: {
+ properties: {},
+ },
+ },
+ };
+ expect(getTemplateVersion(template as TemplateV2Serialized)).toBe(2);
+ });
+ });
+});
diff --git a/x-pack/plugins/index_management/common/lib/utils.ts b/x-pack/plugins/index_management/common/lib/utils.ts
new file mode 100644
index 0000000000000..eee35dc1ab467
--- /dev/null
+++ b/x-pack/plugins/index_management/common/lib/utils.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { TemplateDeserialized, TemplateV1Serialized, TemplateV2Serialized } from '../types';
+
+/**
+ * Helper to get the format version of an index template.
+ * v1 will be supported up until 9.x but marked as deprecated from 7.8
+ * v2 will be supported from 7.8
+ */
+export const getTemplateVersion = (
+ template: TemplateDeserialized | TemplateV1Serialized | TemplateV2Serialized
+): 1 | 2 => {
+ return {}.hasOwnProperty.call(template, 'template') ? 2 : 1;
+};
+
+export const getTemplateParameter = (
+ template: TemplateV1Serialized | TemplateV2Serialized,
+ setting: 'aliases' | 'settings' | 'mappings'
+) => {
+ const formatVersion = getTemplateVersion(template);
+
+ return formatVersion === 1
+ ? (template as TemplateV1Serialized)[setting]
+ : (template as TemplateV2Serialized).template[setting];
+};
diff --git a/x-pack/plugins/index_management/common/types/aliases.ts b/x-pack/plugins/index_management/common/types/aliases.ts
new file mode 100644
index 0000000000000..76aae8585c065
--- /dev/null
+++ b/x-pack/plugins/index_management/common/types/aliases.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface Aliases {
+ [key: string]: any;
+}
diff --git a/x-pack/plugins/index_management/common/types/index.ts b/x-pack/plugins/index_management/common/types/index.ts
index 922cb63b70da3..b467f020978a5 100644
--- a/x-pack/plugins/index_management/common/types/index.ts
+++ b/x-pack/plugins/index_management/common/types/index.ts
@@ -4,4 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
+export * from './aliases';
+
+export * from './indices';
+
+export * from './mappings';
+
export * from './templates';
diff --git a/x-pack/plugins/index_management/common/types/indices.ts b/x-pack/plugins/index_management/common/types/indices.ts
new file mode 100644
index 0000000000000..ecf5ba21fe60c
--- /dev/null
+++ b/x-pack/plugins/index_management/common/types/indices.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+interface IndexModule {
+ number_of_shards: number | string;
+ codec: string;
+ routing_partition_size: number;
+ load_fixed_bitset_filters_eagerly: boolean;
+ shard: {
+ check_on_startup: boolean | 'checksum';
+ };
+ number_of_replicas: number;
+ auto_expand_replicas: false | string;
+ lifecycle: LifecycleModule;
+}
+
+interface AnalysisModule {
+ analyzer: {
+ [key: string]: {
+ type: string;
+ tokenizer: string;
+ char_filter?: string[];
+ filter?: string[];
+ position_increment_gap?: number;
+ };
+ };
+}
+
+interface LifecycleModule {
+ name: string;
+ rollover_alias?: string;
+ parse_origination_date?: boolean;
+ origination_date?: number;
+}
+
+export interface IndexSettings {
+ index?: Partial;
+ analysis?: AnalysisModule;
+ [key: string]: any;
+}
diff --git a/x-pack/plugins/index_management/common/types/mappings.ts b/x-pack/plugins/index_management/common/types/mappings.ts
new file mode 100644
index 0000000000000..0bd3e38ed07a5
--- /dev/null
+++ b/x-pack/plugins/index_management/common/types/mappings.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+// TODO: Move mappings type from Mappings editor here
+
+export interface Mappings {
+ [key: string]: any;
+}
diff --git a/x-pack/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts
index e31c10a775156..c37088982f207 100644
--- a/x-pack/plugins/index_management/common/types/templates.ts
+++ b/x-pack/plugins/index_management/common/types/templates.ts
@@ -4,6 +4,39 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { IndexSettings } from './indices';
+import { Aliases } from './aliases';
+import { Mappings } from './mappings';
+
+// Template serialized (from Elasticsearch)
+interface TemplateBaseSerialized {
+ name: string;
+ index_patterns: string[];
+ version?: number;
+ order?: number;
+}
+
+export interface TemplateV1Serialized extends TemplateBaseSerialized {
+ settings?: IndexSettings;
+ aliases?: Aliases;
+ mappings?: Mappings;
+}
+
+export interface TemplateV2Serialized extends TemplateBaseSerialized {
+ template: {
+ settings?: IndexSettings;
+ aliases?: Aliases;
+ mappings?: Mappings;
+ };
+ priority?: number;
+ composed_of?: string[];
+}
+
+/**
+ * Interface for the template list in our UI table
+ * we don't include the mappings, settings and aliases
+ * to reduce the payload size sent back to the client.
+ */
export interface TemplateListItem {
name: string;
indexPatterns: string[];
@@ -16,37 +49,35 @@ export interface TemplateListItem {
name: string;
};
isManaged: boolean;
+ _kbnMeta: {
+ formatVersion: IndexTemplateFormatVersion;
+ };
}
-export interface Template {
+
+/**
+ * TemplateDeserialized falls back to index template V2 format
+ * The UI will only be dealing with this interface, conversion from and to V1 format
+ * is done server side.
+ */
+export interface TemplateDeserialized {
name: string;
indexPatterns: string[];
+ isManaged: boolean;
+ template: {
+ settings?: IndexSettings;
+ aliases?: Aliases;
+ mappings?: Mappings;
+ };
+ _kbnMeta: {
+ formatVersion: IndexTemplateFormatVersion;
+ };
version?: number;
+ priority?: number;
order?: number;
- settings?: object;
- aliases?: object;
- mappings?: object;
ilmPolicy?: {
name: string;
};
- isManaged: boolean;
+ composedOf?: string[];
}
-export interface TemplateEs {
- name: string;
- index_patterns: string[];
- version?: number;
- order?: number;
- settings?: {
- [key: string]: any;
- index?: {
- [key: string]: any;
- lifecycle?: {
- name: string;
- };
- };
- };
- aliases?: {
- [key: string]: any;
- };
- mappings?: object;
-}
+export type IndexTemplateFormatVersion = 1 | 2;
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx
index b66a42fc50ef4..da4b8e6f6eef2 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx
@@ -38,22 +38,20 @@ const setup = (props: any) =>
defaultProps: props,
})();
-const openModalWithJsonContent = ({ find, component }: TestBed) => async (json: any) => {
- find('load-json-button').simulate('click');
- component.update();
-
+const openModalWithJsonContent = ({ find, waitFor }: TestBed) => async (json: any) => {
// Set the mappings to load
- // @ts-ignore
await act(async () => {
+ find('load-json-button').simulate('click');
+ await waitFor('mockCodeEditor');
+
find('mockCodeEditor').simulate('change', {
jsonString: JSON.stringify(json),
});
- await nextTick(300); // There is a debounce in the JsonEditor that we need to wait for
+ await nextTick(500); // There is a debounce in the JsonEditor that we need to wait for
});
};
-// FLAKY: https://github.com/elastic/kibana/issues/59030
-describe.skip('', () => {
+describe('', () => {
test('it should forward valid mapping definition', async () => {
const mappingsToLoad = {
properties: {
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx
index 247bd183baddf..a9d26b953b96e 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx
@@ -111,13 +111,14 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P
let nextState = state;
if (
+ state.fieldForm &&
state.documentFields.status === 'creatingField' &&
isValid &&
!bypassFieldFormValidation
) {
// If the form field is valid and we are creating a new field that has some data
// we automatically add the field to our state.
- const fieldFormData = state.fieldForm!.data.format() as Field;
+ const fieldFormData = state.fieldForm.data.format() as Field;
if (Object.keys(fieldFormData).length !== 0) {
nextState = addFieldToState(fieldFormData, state);
dispatch({ type: 'field.add', value: fieldFormData });
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts
index 1e6733b1632d7..2f30363c1ff58 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts
@@ -33,11 +33,21 @@ export interface MappingsConfiguration {
}
export interface MappingsTemplates {
- dynamic_templates: Template[];
+ dynamic_templates: DynamicTemplate[];
}
-interface Template {
- [key: string]: any;
+interface DynamicTemplate {
+ [key: string]: {
+ mapping: {
+ [key: string]: any;
+ };
+ match_mapping_type?: string;
+ match?: string;
+ unmatch?: string;
+ match_pattern?: string;
+ path_match?: string;
+ path_unmatch?: string;
+ };
}
export interface MappingsFields {
diff --git a/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx b/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx
index 4e36643dbe117..b80e51d8d139f 100644
--- a/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx
@@ -4,28 +4,27 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import React, { Fragment, useState } from 'react';
import { EuiConfirmModal, EuiOverlayMask, EuiCallOut, EuiCheckbox, EuiBadge } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import React, { Fragment, useState } from 'react';
+
+import { IndexTemplateFormatVersion } from '../../../common';
import { deleteTemplates } from '../services/api';
import { notificationService } from '../services/notification';
-import { Template } from '../../../common/types';
export const TemplateDeleteModal = ({
templatesToDelete,
callback,
}: {
- templatesToDelete: Array;
+ templatesToDelete: Array<{ name: string; formatVersion: IndexTemplateFormatVersion }>;
callback: (data?: { hasDeletedTemplates: boolean }) => void;
}) => {
const [isDeleteConfirmed, setIsDeleteConfirmed] = useState(false);
const numTemplatesToDelete = templatesToDelete.length;
- const hasSystemTemplate = Boolean(
- templatesToDelete.find(templateName => templateName.startsWith('.'))
- );
+ const hasSystemTemplate = Boolean(templatesToDelete.find(({ name }) => name.startsWith('.')));
const handleDeleteTemplates = () => {
deleteTemplates(templatesToDelete).then(({ data: { templatesDeleted, errors }, error }) => {
@@ -38,7 +37,7 @@ export const TemplateDeleteModal = ({
'xpack.idxMgmt.deleteTemplatesModal.successDeleteSingleNotificationMessageText',
{
defaultMessage: "Deleted template '{templateName}'",
- values: { templateName: templatesToDelete[0] },
+ values: { templateName: templatesToDelete[0].name },
}
)
: i18n.translate(
@@ -120,10 +119,10 @@ export const TemplateDeleteModal = ({
- {templatesToDelete.map(template => (
- -
- {template}
- {template.startsWith('.') ? (
+ {templatesToDelete.map(({ name }) => (
+
-
+ {name}
+ {name.startsWith('.') ? (
{' '}
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases.tsx
index 8628b6d8b8d74..50a32787c7a04 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases.tsx
@@ -29,7 +29,7 @@ export const StepAliases: React.FunctionComponent = ({
}) => {
const { content, setContent, error } = useJsonStep({
prop: 'aliases',
- defaultValue: template.aliases,
+ defaultValue: template?.template.aliases,
setDataGetter,
onStepValidityChange,
});
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx
index d51d512429ea4..cf9b57dcbcb14 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx
@@ -15,7 +15,7 @@ import {
EuiText,
} from '@elastic/eui';
import { documentationService } from '../../../services/documentation';
-import { StepProps } from '../types';
+import { StepProps, DataGetterFunc } from '../types';
import { MappingsEditor, OnUpdateHandler, LoadMappingsFromJsonButton } from '../../mappings_editor';
export const StepMappings: React.FunctionComponent = ({
@@ -23,16 +23,23 @@ export const StepMappings: React.FunctionComponent = ({
setDataGetter,
onStepValidityChange,
}) => {
- const [mappings, setMappings] = useState(template.mappings);
+ const [mappings, setMappings] = useState(template?.template.mappings);
const onMappingsEditorUpdate = useCallback(
({ isValid, getData, validate }) => {
onStepValidityChange(isValid);
- setDataGetter(async () => {
+
+ const dataGetterFunc: DataGetterFunc = async () => {
const isMappingsValid = isValid === undefined ? await validate() : isValid;
const data = getData(isMappingsValid);
- return Promise.resolve({ isValid: isMappingsValid, data: { mappings: data } });
- });
+ return {
+ isValid: isMappingsValid,
+ data: { mappings: data },
+ path: 'template',
+ };
+ };
+
+ setDataGetter(dataGetterFunc);
},
[setDataGetter, onStepValidityChange]
);
@@ -96,7 +103,7 @@ export const StepMappings: React.FunctionComponent = ({
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx
index 09da43b83c3c5..0cb2ae9fbcd92 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx
@@ -22,8 +22,11 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import { serializers } from '../../../../shared_imports';
-import { serializeTemplate } from '../../../../../common/lib/template_serialization';
-import { Template } from '../../../../../common/types';
+import {
+ serializeV1Template,
+ serializeV2Template,
+} from '../../../../../common/lib/template_serialization';
+import { TemplateDeserialized, getTemplateParameter } from '../../../../../common';
import { StepProps } from '../types';
const { stripEmptyFields } = serializers;
@@ -52,16 +55,25 @@ const getDescriptionText = (data: any) => {
};
export const StepReview: React.FunctionComponent = ({ template, updateCurrentStep }) => {
- const { name, indexPatterns, version, order } = template;
+ const {
+ name,
+ indexPatterns,
+ version,
+ order,
+ _kbnMeta: { formatVersion },
+ } = template!;
+
+ const serializedTemplate =
+ formatVersion === 1
+ ? serializeV1Template(stripEmptyFields(template!) as TemplateDeserialized)
+ : serializeV2Template(stripEmptyFields(template!) as TemplateDeserialized);
- const serializedTemplate = serializeTemplate(stripEmptyFields(template) as Template);
// Name not included in ES request body
delete serializedTemplate.name;
- const {
- mappings: serializedMappings,
- settings: serializedSettings,
- aliases: serializedAliases,
- } = serializedTemplate;
+
+ const serializedMappings = getTemplateParameter(serializedTemplate, 'mappings');
+ const serializedSettings = getTemplateParameter(serializedTemplate, 'settings');
+ const serializedAliases = getTemplateParameter(serializedTemplate, 'aliases');
const numIndexPatterns = indexPatterns!.length;
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings.tsx
index cead652c9f6fc..7c1ee6388a618 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings.tsx
@@ -29,7 +29,7 @@ export const StepSettings: React.FunctionComponent = ({
}) => {
const { content, setContent, error } = useJsonStep({
prop: 'settings',
- defaultValue: template.settings,
+ defaultValue: template?.template.settings,
setDataGetter,
onStepValidityChange,
});
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts b/x-pack/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts
index fbe479ea0cf23..25dbe784db3a1 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts
+++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts
@@ -8,7 +8,7 @@ import { useEffect, useState, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { isJSON } from '../../../../shared_imports';
-import { StepProps } from '../types';
+import { StepProps, DataGetterFunc } from '../types';
interface Parameters {
prop: 'settings' | 'mappings' | 'aliases';
@@ -44,11 +44,13 @@ export const useJsonStep = ({
return isValid;
}, [content]);
- const dataGetter = useCallback(() => {
+ const dataGetter = useCallback(() => {
const isValid = validateContent();
const value = isValid && content.trim() !== '' ? JSON.parse(content) : {};
- const data = { [prop]: value };
- return Promise.resolve({ isValid, data });
+ // If no key has been added to the JSON object, we strip it out so an empty object is not sent in the request
+ const data = { [prop]: Object.keys(value).length > 0 ? value : undefined };
+
+ return Promise.resolve({ isValid, data, path: 'template' });
}, [content, validateContent, prop]);
useEffect(() => {
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
index 58be9b2c63365..f6193bc71aa91 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
@@ -15,7 +15,7 @@ import {
} from '@elastic/eui';
import { serializers } from '../../../shared_imports';
-import { Template } from '../../../../common/types';
+import { TemplateDeserialized, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../../../common';
import { TemplateSteps } from './template_steps';
import { StepAliases, StepLogistics, StepMappings, StepSettings, StepReview } from './steps';
import { StepProps, DataGetterFunc } from './types';
@@ -24,11 +24,11 @@ import { SectionError } from '../section_error';
const { stripEmptyFields } = serializers;
interface Props {
- onSave: (template: Template) => void;
+ onSave: (template: TemplateDeserialized) => void;
clearSaveError: () => void;
isSaving: boolean;
saveError: any;
- defaultValue?: Template;
+ defaultValue?: TemplateDeserialized;
isEditing?: boolean;
}
@@ -47,7 +47,15 @@ const stepComponentMap: { [key: number]: React.FunctionComponent } =
};
export const TemplateForm: React.FunctionComponent = ({
- defaultValue = { isManaged: false },
+ defaultValue = {
+ name: '',
+ indexPatterns: [],
+ template: {},
+ isManaged: false,
+ _kbnMeta: {
+ formatVersion: DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT,
+ },
+ },
onSave,
isSaving,
saveError,
@@ -63,7 +71,7 @@ export const TemplateForm: React.FunctionComponent = ({
5: defaultValidation,
});
- const template = useRef>(defaultValue);
+ const template = useRef(defaultValue);
const stepsDataGetters = useRef>({});
const lastStep = Object.keys(stepComponentMap).length;
@@ -91,17 +99,31 @@ export const TemplateForm: React.FunctionComponent = ({
);
const validateAndGetDataFromCurrentStep = async () => {
- const validateAndGetData = stepsDataGetters.current[currentStep];
+ const validateAndGetStepData = stepsDataGetters.current[currentStep];
- if (!validateAndGetData) {
+ if (!validateAndGetStepData) {
throw new Error(`No data getter has been set for step "${currentStep}"`);
}
- const { isValid, data } = await validateAndGetData();
+ const { isValid, data, path } = await validateAndGetStepData();
if (isValid) {
- // Update the template object
- template.current = { ...template.current, ...data };
+ // Update the template object with the current step data
+ if (path) {
+ // We only update a "slice" of the template
+ const sliceToUpdate = template.current[path as keyof TemplateDeserialized];
+
+ if (sliceToUpdate === null || typeof sliceToUpdate !== 'object') {
+ return { isValid, data };
+ }
+
+ template.current = {
+ ...template.current,
+ [path]: { ...sliceToUpdate, ...data },
+ };
+ } else {
+ template.current = { ...template.current, ...data };
+ }
}
return { isValid, data };
@@ -111,9 +133,9 @@ export const TemplateForm: React.FunctionComponent = ({
// All steps needs validation, except for the last step
const shouldValidate = currentStep !== lastStep;
- let isValid = isStepValid;
if (shouldValidate) {
- isValid = isValid === false ? false : (await validateAndGetDataFromCurrentStep()).isValid;
+ const isValid =
+ isStepValid === false ? false : (await validateAndGetDataFromCurrentStep()).isValid;
// If step is invalid do not let user proceed
if (!isValid) {
@@ -222,7 +244,10 @@ export const TemplateForm: React.FunctionComponent = ({
fill
color="secondary"
iconType="check"
- onClick={onSave.bind(null, stripEmptyFields(template.current) as Template)}
+ onClick={onSave.bind(
+ null,
+ stripEmptyFields(template.current!) as TemplateDeserialized
+ )}
data-test-subj="submitButton"
isLoading={isSaving}
>
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/types.ts b/x-pack/plugins/index_management/public/application/components/template_form/types.ts
index 9385f0c9f738b..5db53e91ed261 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/types.ts
+++ b/x-pack/plugins/index_management/public/application/components/template_form/types.ts
@@ -4,14 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Template } from '../../../../common/types';
+import { TemplateDeserialized } from '../../../../common';
export interface StepProps {
- template: Partial;
+ template?: TemplateDeserialized;
setDataGetter: (dataGetter: DataGetterFunc) => void;
updateCurrentStep: (step: number) => void;
onStepValidityChange: (isValid: boolean | undefined) => void;
isEditing?: boolean;
}
-export type DataGetterFunc = () => Promise<{ isValid: boolean; data: any }>;
+export type DataGetterFunc = () => Promise<{
+ /** Is the step data valid or not */
+ isValid: boolean;
+ /** The current step data (can be invalid) */
+ data: any;
+ /** Optional "slice" of the complete object the step is updating */
+ path?: string;
+}>;
diff --git a/x-pack/plugins/index_management/public/application/lib/index_templates.ts b/x-pack/plugins/index_management/public/application/lib/index_templates.ts
new file mode 100644
index 0000000000000..7129e536287c1
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/lib/index_templates.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { parse } from 'query-string';
+import { Location } from 'history';
+
+export const getFormatVersionFromQueryparams = (location: Location): 1 | 2 | undefined => {
+ const { v: version } = parse(location.search.substring(1));
+
+ if (!Boolean(version) || typeof version !== 'string') {
+ return undefined;
+ }
+
+ return +version as 1 | 2;
+};
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx
index 421119bd8df96..fa7d734ad0d2b 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx
@@ -7,14 +7,16 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCodeBlock, EuiCallOut } from '@elastic/eui';
-import { Template } from '../../../../../../../common/types';
+import { TemplateDeserialized } from '../../../../../../../common';
interface Props {
- templateDetails: Template;
+ templateDetails: TemplateDeserialized;
}
export const TabAliases: React.FunctionComponent = ({ templateDetails }) => {
- const { aliases } = templateDetails;
+ const {
+ template: { aliases },
+ } = templateDetails;
if (aliases && Object.keys(aliases).length) {
return (
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx
index 83f2e67fb12c5..6e0257c6b377b 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx
@@ -7,14 +7,16 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCodeBlock, EuiCallOut } from '@elastic/eui';
-import { Template } from '../../../../../../../common/types';
+import { TemplateDeserialized } from '../../../../../../../common';
interface Props {
- templateDetails: Template;
+ templateDetails: TemplateDeserialized;
}
export const TabMappings: React.FunctionComponent = ({ templateDetails }) => {
- const { mappings } = templateDetails;
+ const {
+ template: { mappings },
+ } = templateDetails;
if (mappings && Object.keys(mappings).length) {
return (
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx
index 8b2a431bee65a..8f75c2cb77801 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx
@@ -7,14 +7,16 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCodeBlock, EuiCallOut } from '@elastic/eui';
-import { Template } from '../../../../../../../common/types';
+import { TemplateDeserialized } from '../../../../../../../common';
interface Props {
- templateDetails: Template;
+ templateDetails: TemplateDeserialized;
}
export const TabSettings: React.FunctionComponent = ({ templateDetails }) => {
- const { settings } = templateDetails;
+ const {
+ template: { settings },
+ } = templateDetails;
if (settings && Object.keys(settings).length) {
return (
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx
index 99f5db54b4ba2..9ce29ab746a2f 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx
@@ -14,11 +14,11 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
-import { Template } from '../../../../../../../common/types';
+import { TemplateDeserialized } from '../../../../../../../common';
import { getILMPolicyPath } from '../../../../../services/navigation';
interface Props {
- templateDetails: Template;
+ templateDetails: TemplateDeserialized;
}
const NoneDescriptionText = () => (
@@ -35,6 +35,7 @@ export const TabSummary: React.FunctionComponent = ({ templateDetails })
return (
+ {/* Index patterns */}
= ({ templateDetails })
indexPatterns.toString()
)}
+
+ {/* // ILM Policy */}
= ({ templateDetails })
)}
+
+ {/* // Order */}
= ({ templateDetails })
{order || order === 0 ? order : }
+
+ {/* // Version */}
void;
- editTemplate: (templateName: Template['name']) => void;
- cloneTemplate: (templateName: Template['name']) => void;
+ editTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void;
+ cloneTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void;
reload: () => Promise;
}
@@ -79,7 +79,7 @@ const TABS = [
];
const tabToComponentMap: {
- [key: string]: React.FunctionComponent<{ templateDetails: Template }>;
+ [key: string]: React.FunctionComponent<{ templateDetails: TemplateDeserialized }>;
} = {
[SUMMARY_TAB_ID]: TabSummary,
[SETTINGS_TAB_ID]: TabSettings,
@@ -95,7 +95,7 @@ const tabToUiMetricMap: { [key: string]: string } = {
};
export const TemplateDetails: React.FunctionComponent = ({
- templateName,
+ template: { name: templateName, formatVersion },
onClose,
editTemplate,
cloneTemplate,
@@ -103,10 +103,14 @@ export const TemplateDetails: React.FunctionComponent = ({
}) => {
const { uiMetricService } = useServices();
const decodedTemplateName = decodePath(templateName);
- const { error, data: templateDetails, isLoading } = useLoadIndexTemplate(decodedTemplateName);
- // TS complains if we use destructuring here. Fixed in 3.6.0 (https://github.com/microsoft/TypeScript/pull/31711).
- const isManaged = templateDetails ? templateDetails.isManaged : undefined;
- const [templateToDelete, setTemplateToDelete] = useState>([]);
+ const { error, data: templateDetails, isLoading } = useLoadIndexTemplate(
+ decodedTemplateName,
+ formatVersion
+ );
+ const isManaged = templateDetails?.isManaged;
+ const [templateToDelete, setTemplateToDelete] = useState<
+ Array<{ name: string; formatVersion: IndexTemplateFormatVersion }>
+ >([]);
const [activeTab, setActiveTab] = useState(SUMMARY_TAB_ID);
const [isPopoverOpen, setIsPopOverOpen] = useState(false);
@@ -275,7 +279,7 @@ export const TemplateDetails: React.FunctionComponent = ({
defaultMessage: 'Edit',
}),
icon: 'pencil',
- onClick: () => editTemplate(decodedTemplateName),
+ onClick: () => editTemplate(templateName, formatVersion),
disabled: isManaged,
},
{
@@ -283,7 +287,7 @@ export const TemplateDetails: React.FunctionComponent = ({
defaultMessage: 'Clone',
}),
icon: 'copy',
- onClick: () => cloneTemplate(decodedTemplateName),
+ onClick: () => cloneTemplate(templateName, formatVersion),
},
{
name: i18n.translate(
@@ -293,7 +297,8 @@ export const TemplateDetails: React.FunctionComponent = ({
}
),
icon: 'trash',
- onClick: () => setTemplateToDelete([decodedTemplateName]),
+ onClick: () =>
+ setTemplateToDelete([{ name: decodedTemplateName, formatVersion }]),
disabled: isManaged,
},
],
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
index ffdb224f16271..1e84202639ee8 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
@@ -16,31 +16,35 @@ import {
EuiFlexItem,
EuiFlexGroup,
} from '@elastic/eui';
+
+import { UIM_TEMPLATE_LIST_LOAD } from '../../../../../common/constants';
+import { IndexTemplateFormatVersion } from '../../../../../common';
import { SectionError, SectionLoading, Error } from '../../../components';
-import { TemplateTable } from './template_table';
import { useLoadIndexTemplates } from '../../../services/api';
-import { Template } from '../../../../../common/types';
import { useServices } from '../../../app_context';
import {
getTemplateEditLink,
getTemplateListLink,
getTemplateCloneLink,
} from '../../../services/routing';
-import { UIM_TEMPLATE_LIST_LOAD } from '../../../../../common/constants';
+import { getFormatVersionFromQueryparams } from '../../../lib/index_templates';
+import { TemplateTable } from './template_table';
import { TemplateDetails } from './template_details';
interface MatchParams {
- templateName?: Template['name'];
+ templateName?: string;
}
export const TemplateList: React.FunctionComponent> = ({
match: {
params: { templateName },
},
+ location,
history,
}) => {
const { uiMetricService } = useServices();
const { error, isLoading, data: templates, sendRequest: reload } = useLoadIndexTemplates();
+ const queryParamsFormatVersion = getFormatVersionFromQueryparams(location);
let content;
@@ -48,8 +52,7 @@ export const TemplateList: React.FunctionComponent
- templates ? templates.filter((template: Template) => !template.name.startsWith('.')) : [],
+ () => (templates ? templates.filter(template => !template.name.startsWith('.')) : []),
[templates]
);
@@ -57,12 +60,12 @@ export const TemplateList: React.FunctionComponent {
- history.push(getTemplateEditLink(name));
+ const editTemplate = (name: string, formatVersion: IndexTemplateFormatVersion) => {
+ history.push(getTemplateEditLink(name, formatVersion));
};
- const cloneTemplate = (name: Template['name']) => {
- history.push(getTemplateCloneLink(name));
+ const cloneTemplate = (name: string, formatVersion: IndexTemplateFormatVersion) => {
+ history.push(getTemplateCloneLink(name, formatVersion));
};
// Track component loaded
@@ -149,9 +152,12 @@ export const TemplateList: React.FunctionComponent
{content}
- {templateName && (
+ {templateName && queryParamsFormatVersion !== undefined && (
Promise;
- editTemplate: (name: Template['name']) => void;
- cloneTemplate: (name: Template['name']) => void;
+ editTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void;
+ cloneTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void;
}
export const TemplateTable: React.FunctionComponent = ({
@@ -30,7 +30,9 @@ export const TemplateTable: React.FunctionComponent = ({
}) => {
const { uiMetricService } = useServices();
const [selection, setSelection] = useState([]);
- const [templatesToDelete, setTemplatesToDelete] = useState>([]);
+ const [templatesToDelete, setTemplatesToDelete] = useState<
+ Array<{ name: string; formatVersion: IndexTemplateFormatVersion }>
+ >([]);
const columns: Array> = [
{
@@ -40,11 +42,11 @@ export const TemplateTable: React.FunctionComponent = ({
}),
truncateText: true,
sortable: true,
- render: (name: TemplateListItem['name']) => {
+ render: (name: TemplateListItem['name'], item: TemplateListItem) => {
return (
/* eslint-disable-next-line @elastic/eui/href-or-on-click */
uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK)}
>
@@ -133,10 +135,10 @@ export const TemplateTable: React.FunctionComponent = ({
}),
icon: 'pencil',
type: 'icon',
- onClick: ({ name }: Template) => {
- editTemplate(name);
+ onClick: ({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => {
+ editTemplate(name, formatVersion);
},
- enabled: ({ isManaged }: Template) => !isManaged,
+ enabled: ({ isManaged }: TemplateListItem) => !isManaged,
},
{
type: 'icon',
@@ -147,8 +149,8 @@ export const TemplateTable: React.FunctionComponent = ({
defaultMessage: 'Clone this template',
}),
icon: 'copy',
- onClick: ({ name }: Template) => {
- cloneTemplate(name);
+ onClick: ({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => {
+ cloneTemplate(name, formatVersion);
},
},
{
@@ -161,11 +163,11 @@ export const TemplateTable: React.FunctionComponent = ({
icon: 'trash',
color: 'danger',
type: 'icon',
- onClick: ({ name }: Template) => {
- setTemplatesToDelete([name]);
+ onClick: ({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => {
+ setTemplatesToDelete([{ name, formatVersion }]);
},
isPrimary: true,
- enabled: ({ isManaged }: Template) => !isManaged,
+ enabled: ({ isManaged }: TemplateListItem) => !isManaged,
},
],
},
@@ -185,7 +187,7 @@ export const TemplateTable: React.FunctionComponent = ({
const selectionConfig = {
onSelectionChange: setSelection,
- selectable: ({ isManaged }: Template) => !isManaged,
+ selectable: ({ isManaged }: TemplateListItem) => !isManaged,
selectableMessage: (selectable: boolean) => {
if (!selectable) {
return i18n.translate('xpack.idxMgmt.templateList.table.deleteManagedTemplateTooltip', {
@@ -205,7 +207,12 @@ export const TemplateTable: React.FunctionComponent = ({
- setTemplatesToDelete(selection.map((selected: TemplateListItem) => selected.name))
+ setTemplatesToDelete(
+ selection.map(({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => ({
+ name,
+ formatVersion,
+ }))
+ )
}
color="danger"
>
diff --git a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
index cf6ca3c065777..b69e441feb176 100644
--- a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
@@ -7,11 +7,13 @@ import React, { useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
+
+import { TemplateDeserialized, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../../../common';
import { TemplateForm, SectionLoading, SectionError, Error } from '../../components';
import { breadcrumbService } from '../../services/breadcrumbs';
import { decodePath, getTemplateDetailsLink } from '../../services/routing';
-import { Template } from '../../../../common/types';
import { saveTemplate, useLoadIndexTemplate } from '../../services/api';
+import { getFormatVersionFromQueryparams } from '../../lib/index_templates';
interface MatchParams {
name: string;
@@ -21,17 +23,21 @@ export const TemplateClone: React.FunctionComponent {
const decodedTemplateName = decodePath(name);
+ const formatVersion =
+ getFormatVersionFromQueryparams(location) ?? DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT;
+
const [isSaving, setIsSaving] = useState(false);
const [saveError, setSaveError] = useState(null);
-
const { error: templateToCloneError, data: templateToClone, isLoading } = useLoadIndexTemplate(
- decodedTemplateName
+ decodedTemplateName,
+ formatVersion
);
- const onSave = async (template: Template) => {
+ const onSave = async (template: TemplateDeserialized) => {
setIsSaving(true);
setSaveError(null);
@@ -46,7 +52,7 @@ export const TemplateClone: React.FunctionComponent {
@@ -85,7 +91,7 @@ export const TemplateClone: React.FunctionComponent