diff --git a/packages/kbn-config-schema/package.json b/packages/kbn-config-schema/package.json index 71c0ae4bff1f9d..06342127b0d890 100644 --- a/packages/kbn-config-schema/package.json +++ b/packages/kbn-config-schema/package.json @@ -10,7 +10,8 @@ "kbn:bootstrap": "yarn build" }, "devDependencies": { - "typescript": "3.7.2" + "typescript": "3.7.2", + "tsd": "^0.7.4" }, "peerDependencies": { "joi": "^13.5.2", diff --git a/packages/kbn-config-schema/src/index.ts b/packages/kbn-config-schema/src/index.ts index 5d387f327e58fd..2319fe4395e3f6 100644 --- a/packages/kbn-config-schema/src/index.ts +++ b/packages/kbn-config-schema/src/index.ts @@ -44,6 +44,7 @@ import { ObjectType, ObjectTypeOptions, Props, + NullableProps, RecordOfOptions, RecordOfType, StringOptions, @@ -57,7 +58,7 @@ import { StreamType, } from './types'; -export { ObjectType, TypeOf, Type }; +export { ObjectType, TypeOf, Type, Props, NullableProps }; export { ByteSizeValue } from './byte_size_value'; export { SchemaTypeError, ValidationError } from './errors'; export { isConfigSchema } from './typeguards'; diff --git a/packages/kbn-config-schema/src/types/index.ts b/packages/kbn-config-schema/src/types/index.ts index 9db79b8bf9e00b..c7900e1923e786 100644 --- a/packages/kbn-config-schema/src/types/index.ts +++ b/packages/kbn-config-schema/src/types/index.ts @@ -29,7 +29,7 @@ export { LiteralType } from './literal_type'; export { MaybeType } from './maybe_type'; export { MapOfOptions, MapOfType } from './map_type'; export { NumberOptions, NumberType } from './number_type'; -export { ObjectType, ObjectTypeOptions, Props, TypeOf } from './object_type'; +export { ObjectType, ObjectTypeOptions, Props, NullableProps, TypeOf } from './object_type'; export { RecordOfOptions, RecordOfType } from './record_type'; export { StreamType } from './stream_type'; export { StringOptions, StringType } from './string_type'; diff --git a/packages/kbn-config-schema/src/types/object_type.test.ts b/packages/kbn-config-schema/src/types/object_type.test.ts index 5ab59d1c020779..334e814aa52e48 100644 --- a/packages/kbn-config-schema/src/types/object_type.test.ts +++ b/packages/kbn-config-schema/src/types/object_type.test.ts @@ -17,6 +17,7 @@ * under the License. */ +import { expectType } from 'tsd'; import { schema } from '..'; import { TypeOf } from './object_type'; @@ -360,17 +361,142 @@ test('handles optional properties', () => { type SchemaType = TypeOf; - let foo: SchemaType = { + expectType({ required: 'foo', - }; - foo = { + }); + expectType({ required: 'hello', optional: undefined, - }; - foo = { + }); + expectType({ required: 'hello', optional: 'bar', - }; + }); +}); + +describe('#extends', () => { + it('allows to extend an existing schema by adding new properties', () => { + const origin = schema.object({ + initial: schema.string(), + }); + + const extended = origin.extends({ + added: schema.number(), + }); + + expect(() => { + extended.validate({ initial: 'foo' }); + }).toThrowErrorMatchingInlineSnapshot( + `"[added]: expected value of type [number] but got [undefined]"` + ); + + expect(() => { + extended.validate({ initial: 'foo', added: 42 }); + }).not.toThrowError(); - expect(foo).toBeDefined(); + expectType>({ + added: 12, + initial: 'foo', + }); + }); + + it('allows to extend an existing schema by removing properties', () => { + const origin = schema.object({ + string: schema.string(), + number: schema.number(), + }); + + const extended = origin.extends({ number: undefined }); + + expect(() => { + extended.validate({ string: 'foo', number: 12 }); + }).toThrowErrorMatchingInlineSnapshot(`"[number]: definition for this key is missing"`); + + expect(() => { + extended.validate({ string: 'foo' }); + }).not.toThrowError(); + + expectType>({ + string: 'foo', + }); + }); + + it('allows to extend an existing schema by overriding an existing properties', () => { + const origin = schema.object({ + string: schema.string(), + mutated: schema.number(), + }); + + const extended = origin.extends({ + mutated: schema.string(), + }); + + expect(() => { + extended.validate({ string: 'foo', mutated: 12 }); + }).toThrowErrorMatchingInlineSnapshot( + `"[mutated]: expected value of type [string] but got [number]"` + ); + + expect(() => { + extended.validate({ string: 'foo', mutated: 'bar' }); + }).not.toThrowError(); + + expectType>({ + string: 'foo', + mutated: 'bar', + }); + }); + + it('properly infer the type from optional properties', () => { + const origin = schema.object({ + original: schema.maybe(schema.string()), + mutated: schema.maybe(schema.number()), + removed: schema.maybe(schema.string()), + }); + + const extended = origin.extends({ + removed: undefined, + mutated: schema.string(), + }); + + expect(() => { + extended.validate({ original: 'foo' }); + }).toThrowErrorMatchingInlineSnapshot( + `"[mutated]: expected value of type [string] but got [undefined]"` + ); + expect(() => { + extended.validate({ original: 'foo' }); + }).toThrowErrorMatchingInlineSnapshot( + `"[mutated]: expected value of type [string] but got [undefined]"` + ); + expect(() => { + extended.validate({ original: 'foo', mutated: 'bar' }); + }).not.toThrowError(); + + expectType>({ + original: 'foo', + mutated: 'bar', + }); + expectType>({ + mutated: 'bar', + }); + }); + + it(`allows to override the original schema's options`, () => { + const origin = schema.object( + { + initial: schema.string(), + }, + { defaultValue: { initial: 'foo' } } + ); + + const extended = origin.extends( + { + added: schema.number(), + }, + { defaultValue: { initial: 'bar', added: 42 } } + ); + + expect(extended.validate(undefined)).toEqual({ initial: 'bar', added: 42 }); + }); }); diff --git a/packages/kbn-config-schema/src/types/object_type.ts b/packages/kbn-config-schema/src/types/object_type.ts index fee2d02c1bfb96..431b6e905bcd45 100644 --- a/packages/kbn-config-schema/src/types/object_type.ts +++ b/packages/kbn-config-schema/src/types/object_type.ts @@ -24,6 +24,8 @@ import { ValidationError } from '../errors'; export type Props = Record>; +export type NullableProps = Record | undefined | null>; + export type TypeOf> = RT['type']; type OptionalProperties = Pick< @@ -47,6 +49,24 @@ export type ObjectResultType

= Readonly< { [K in keyof RequiredProperties

]: TypeOf } >; +type DefinedProperties = Pick< + Base, + { + [Key in keyof Base]: undefined extends Base[Key] ? never : null extends Base[Key] ? never : Key; + }[keyof Base] +>; + +type ExtendedProps

= Omit & + { [K in keyof DefinedProperties]: NP[K] }; + +type ExtendedObjectType

= ObjectType< + ExtendedProps +>; + +type ExtendedObjectTypeOptions

= ObjectTypeOptions< + ExtendedProps +>; + interface UnknownOptions { /** * Options for dealing with unknown keys: @@ -61,10 +81,13 @@ export type ObjectTypeOptions

= TypeOptions extends Type> { - private props: Record; + private props: P; + private options: ObjectTypeOptions

; + private propSchemas: Record; - constructor(props: P, { unknowns = 'forbid', ...typeOptions }: ObjectTypeOptions

= {}) { + constructor(props: P, options: ObjectTypeOptions

= {}) { const schemaKeys = {} as Record; + const { unknowns = 'forbid', ...typeOptions } = options; for (const [key, value] of Object.entries(props)) { schemaKeys[key] = value.getSchema(); } @@ -77,7 +100,93 @@ export class ObjectType

extends Type> .options({ stripUnknown: { objects: unknowns === 'ignore' } }); super(schema, typeOptions); - this.props = schemaKeys; + this.props = props; + this.propSchemas = schemaKeys; + this.options = options; + } + + /** + * Return a new `ObjectType` instance extended with given `newProps` properties. + * Original properties can be deleted from the copy by passing a `null` or `undefined` value for the key. + * + * @example + * How to add a new key to an object schema + * ```ts + * const origin = schema.object({ + * initial: schema.string(), + * }); + * + * const extended = origin.extends({ + * added: schema.number(), + * }); + * ``` + * + * How to remove an existing key from an object schema + * ```ts + * const origin = schema.object({ + * initial: schema.string(), + * toRemove: schema.number(), + * }); + * + * const extended = origin.extends({ + * toRemove: undefined, + * }); + * ``` + * + * How to override the schema's options + * ```ts + * const origin = schema.object({ + * initial: schema.string(), + * }, { defaultValue: { initial: 'foo' }}); + * + * const extended = origin.extends({ + * added: schema.number(), + * }, { defaultValue: { initial: 'foo', added: 'bar' }}); + * + * @remarks + * `extends` only support extending first-level properties. It's currently not possible to perform deep/nested extensions. + * + * ```ts + * const origin = schema.object({ + * foo: schema.string(), + * nested: schema.object({ + * a: schema.string(), + * b: schema.string(), + * }), + * }); + * + * const extended = origin.extends({ + * nested: schema.object({ + * c: schema.string(), + * }), + * }); + * + * // TypeOf is `{ foo: string; nested: { c: string } }` + * ``` + */ + public extends( + newProps: NP, + newOptions?: ExtendedObjectTypeOptions + ): ExtendedObjectType { + const extendedProps = Object.entries({ + ...this.props, + ...newProps, + }).reduce((memo, [key, value]) => { + if (value !== null && value !== undefined) { + return { + ...memo, + [key]: value, + }; + } + return memo; + }, {} as ExtendedProps); + + const extendedOptions = { + ...this.options, + ...newOptions, + } as ExtendedObjectTypeOptions; + + return new ObjectType(extendedProps, extendedOptions); } protected handleError(type: string, { reason, value }: Record) { @@ -95,10 +204,10 @@ export class ObjectType

extends Type> } validateKey(key: string, value: any) { - if (!this.props[key]) { + if (!this.propSchemas[key]) { throw new Error(`${key} is not a valid part of this schema`); } - const { value: validatedValue, error } = this.props[key].validate(value); + const { value: validatedValue, error } = this.propSchemas[key].validate(value); if (error) { throw new ValidationError(error as any, key); } diff --git a/src/legacy/ui/public/new_platform/set_services.ts b/src/legacy/ui/public/new_platform/set_services.ts index 9d02ad67b39378..ee92eda064aa86 100644 --- a/src/legacy/ui/public/new_platform/set_services.ts +++ b/src/legacy/ui/public/new_platform/set_services.ts @@ -65,6 +65,7 @@ export function setStartServices(npStart: NpStart) { visualizationsServices.setCapabilities(npStart.core.application.capabilities); visualizationsServices.setHttp(npStart.core.http); visualizationsServices.setApplication(npStart.core.application); + visualizationsServices.setEmbeddable(npStart.plugins.embeddable); visualizationsServices.setSavedObjects(npStart.core.savedObjects); visualizationsServices.setIndexPatterns(npStart.plugins.data.indexPatterns); visualizationsServices.setFilterManager(npStart.plugins.data.query.filterManager); diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index 7e25d80c9d6191..5d4cc851cf4554 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -186,7 +186,11 @@ export class DashboardContainer extends Container - + , dom diff --git a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx index 9b7cec2f182ba8..9a2610a82b97d0 100644 --- a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx +++ b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx @@ -80,6 +80,7 @@ function prepare(props?: Partial) { dashboardContainer = new DashboardContainer(initialInput, options); const defaultTestProps: DashboardGridProps = { container: dashboardContainer, + PanelComponent: () =>

, kibana: null as any, intl: null as any, }; diff --git a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx index 19d9ad34e729c8..dcd07fe394c7d1 100644 --- a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx +++ b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx @@ -30,7 +30,7 @@ import React from 'react'; import { Subscription } from 'rxjs'; import ReactGridLayout, { Layout } from 'react-grid-layout'; import { GridData } from '../../../../common'; -import { ViewMode, EmbeddableChildPanel } from '../../../embeddable_plugin'; +import { ViewMode, EmbeddableChildPanel, EmbeddableStart } from '../../../embeddable_plugin'; import { DASHBOARD_GRID_COLUMN_COUNT, DASHBOARD_GRID_HEIGHT } from '../dashboard_constants'; import { DashboardPanelState } from '../types'; import { withKibana } from '../../../../../kibana_react/public'; @@ -115,6 +115,7 @@ const ResponsiveSizedGrid = sizeMe(config)(ResponsiveGrid); export interface DashboardGridProps extends ReactIntl.InjectedIntlProps { kibana: DashboardReactContextValue; + PanelComponent: EmbeddableStart['EmbeddablePanel']; container: DashboardContainer; } @@ -271,14 +272,7 @@ class DashboardGridUi extends React.Component {
); diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx index 1b257880b94013..25e451dc7f793b 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx +++ b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx @@ -87,6 +87,7 @@ function getProps( dashboardContainer = new DashboardContainer(input, options); const defaultTestProps: DashboardViewportProps = { container: dashboardContainer, + PanelComponent: () =>
, }; return { diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx index ae239bc27fdbaa..429837583b6481 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx +++ b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { Subscription } from 'rxjs'; -import { PanelState } from '../../../embeddable_plugin'; +import { PanelState, EmbeddableStart } from '../../../embeddable_plugin'; import { DashboardContainer, DashboardReactContextValue } from '../dashboard_container'; import { DashboardGrid } from '../grid'; import { context } from '../../../../../kibana_react/public'; @@ -27,6 +27,7 @@ import { context } from '../../../../../kibana_react/public'; export interface DashboardViewportProps { container: DashboardContainer; renderEmpty?: () => React.ReactNode; + PanelComponent: EmbeddableStart['EmbeddablePanel']; } interface State { @@ -114,7 +115,7 @@ export class DashboardViewport extends React.Component )} - +
); } 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 b046376a304ae0..e29e941e898fbe 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 @@ -31,7 +31,7 @@ import { // eslint-disable-next-line import { inspectorPluginMock } from '../../../../inspector/public/mocks'; import { mount } from 'enzyme'; -import { embeddablePluginMock } from '../../mocks'; +import { embeddablePluginMock, createEmbeddablePanelMock } from '../../mocks'; test('EmbeddableChildPanel renders an embeddable when it is done loading', async () => { const inspector = inspectorPluginMock.createStartContract(); @@ -58,18 +58,17 @@ test('EmbeddableChildPanel renders an embeddable when it is done loading', async expect(newEmbeddable.id).toBeDefined(); + const testPanel = createEmbeddablePanelMock({ + getAllEmbeddableFactories: start.getEmbeddableFactories, + getEmbeddableFactory, + inspector, + }); + const component = mount( Promise.resolve([])} - getAllEmbeddableFactories={start.getEmbeddableFactories} - getEmbeddableFactory={getEmbeddableFactory} - notifications={{} as any} - application={{} as any} - overlays={{} as any} - inspector={inspector} - SavedObjectFinder={() => null} + PanelComponent={testPanel} /> ); @@ -97,19 +96,9 @@ test(`EmbeddableChildPanel renders an error message if the factory doesn't exist { getEmbeddableFactory } as any ); + const testPanel = createEmbeddablePanelMock({ inspector }); const component = mount( - Promise.resolve([])} - getAllEmbeddableFactories={(() => []) as any} - getEmbeddableFactory={(() => undefined) as any} - notifications={{} as any} - overlays={{} as any} - application={{} as any} - inspector={inspector} - SavedObjectFinder={() => null} - /> + ); await nextTick(); diff --git a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx index 70628665e6e8c9..be8ff2c95fe096 100644 --- a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx +++ b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx @@ -22,12 +22,7 @@ import React from 'react'; import { EuiLoadingChart } from '@elastic/eui'; import { Subscription } from 'rxjs'; -import { CoreStart } from 'src/core/public'; -import { UiActionsService } from 'src/plugins/ui_actions/public'; - -import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { ErrorEmbeddable, IEmbeddable } from '../embeddables'; -import { EmbeddablePanel } from '../panel'; import { IContainer } from './i_container'; import { EmbeddableStart } from '../../plugin'; @@ -35,14 +30,7 @@ export interface EmbeddableChildPanelProps { embeddableId: string; className?: string; container: IContainer; - getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; - getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; - overlays: CoreStart['overlays']; - notifications: CoreStart['notifications']; - application: CoreStart['application']; - inspector: InspectorStartContract; - SavedObjectFinder: React.ComponentType; + PanelComponent: EmbeddableStart['EmbeddablePanel']; } interface State { @@ -87,6 +75,7 @@ export class EmbeddableChildPanel extends React.Component ) : ( - + )} ); diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx index 31e14a0af59d7d..913c3a0b30826b 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx @@ -19,9 +19,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { CoreStart } from 'src/core/public'; -import { UiActionsService } from 'src/plugins/ui_actions/public'; -import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { Container, ViewMode, ContainerInput } from '../..'; import { HelloWorldContainerComponent } from './hello_world_container_component'; import { EmbeddableStart } from '../../../plugin'; @@ -45,14 +42,8 @@ interface HelloWorldContainerInput extends ContainerInput { } interface HelloWorldContainerOptions { - getActions: UiActionsService['getTriggerCompatibleActions']; getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; - getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; - overlays: CoreStart['overlays']; - application: CoreStart['application']; - notifications: CoreStart['notifications']; - inspector: InspectorStartContract; - SavedObjectFinder: React.ComponentType; + panelComponent: EmbeddableStart['EmbeddablePanel']; } export class HelloWorldContainer extends Container { @@ -78,14 +69,7 @@ export class HelloWorldContainer extends Container , node diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx index 6453046b86e205..5fefa1fc907201 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx @@ -20,22 +20,12 @@ import React, { Component, RefObject } from 'react'; import { Subscription } from 'rxjs'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { CoreStart } from 'src/core/public'; -import { UiActionsService } from 'src/plugins/ui_actions/public'; -import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { IContainer, PanelState, EmbeddableChildPanel } from '../..'; import { EmbeddableStart } from '../../../plugin'; interface Props { container: IContainer; - getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; - getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; - overlays: CoreStart['overlays']; - application: CoreStart['application']; - notifications: CoreStart['notifications']; - inspector: InspectorStartContract; - SavedObjectFinder: React.ComponentType; + panelComponent: EmbeddableStart['EmbeddablePanel']; } interface State { @@ -108,14 +98,7 @@ export class HelloWorldContainerComponent extends Component { ); diff --git a/src/plugins/embeddable/public/mocks.ts b/src/plugins/embeddable/public/mocks.tsx similarity index 62% rename from src/plugins/embeddable/public/mocks.ts rename to src/plugins/embeddable/public/mocks.tsx index f5487c381cfcb3..9da0b7602c4f8f 100644 --- a/src/plugins/embeddable/public/mocks.ts +++ b/src/plugins/embeddable/public/mocks.tsx @@ -16,14 +16,20 @@ * specific language governing permissions and limitations * under the License. */ +import React from 'react'; import { EmbeddableStart, EmbeddableSetup, EmbeddableSetupDependencies, EmbeddableStartDependencies, + IEmbeddable, + EmbeddablePanel, } from '.'; import { EmbeddablePublicPlugin } from './plugin'; import { coreMock } from '../../../core/public/mocks'; +import { UiActionsService } from './lib/ui_actions'; +import { CoreStart } from '../../../core/public'; +import { Start as InspectorStart } from '../../inspector/public'; // eslint-disable-next-line import { inspectorPluginMock } from '../../inspector/public/mocks'; @@ -33,6 +39,42 @@ import { uiActionsPluginMock } from '../../ui_actions/public/mocks'; export type Setup = jest.Mocked; export type Start = jest.Mocked; +interface CreateEmbeddablePanelMockArgs { + getActions: UiActionsService['getTriggerCompatibleActions']; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; + overlays: CoreStart['overlays']; + notifications: CoreStart['notifications']; + application: CoreStart['application']; + inspector: InspectorStart; + SavedObjectFinder: React.ComponentType; +} + +export const createEmbeddablePanelMock = ({ + getActions, + getEmbeddableFactory, + getAllEmbeddableFactories, + overlays, + notifications, + application, + inspector, + SavedObjectFinder, +}: Partial) => { + return ({ embeddable }: { embeddable: IEmbeddable }) => ( + Promise.resolve([]))} + getAllEmbeddableFactories={getAllEmbeddableFactories || ((() => []) as any)} + getEmbeddableFactory={getEmbeddableFactory || ((() => undefined) as any)} + notifications={notifications || ({} as any)} + application={application || ({} as any)} + overlays={overlays || ({} as any)} + inspector={inspector || ({} as any)} + SavedObjectFinder={SavedObjectFinder || (() => null)} + /> + ); +}; + const createSetupContract = (): Setup => { const setupContract: Setup = { registerEmbeddableFactory: jest.fn(), 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 ebb76c743393bc..ec92f334267f58 100644 --- a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts +++ b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts @@ -31,8 +31,7 @@ import { FilterableEmbeddableInput, } from '../lib/test_samples'; // eslint-disable-next-line -import { inspectorPluginMock } from '../../../../plugins/inspector/public/mocks'; -import { esFilters } from '../../../../plugins/data/public'; +import { esFilters } from '../../../data/public'; test('ApplyFilterAction applies the filter to the root of the container tree', async () => { const { doStart, setup } = testPlugin(); @@ -95,26 +94,16 @@ 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 inspector = inspectorPluginMock.createStartContract(); + const { doStart, setup } = testPlugin(); const factory = new FilterableEmbeddableFactory(); setup.registerEmbeddableFactory(factory.type, factory); const api = doStart(); const applyFilterAction = createFilterAction(); - const parent = new HelloWorldContainer( - { id: 'root', panels: {} }, - { - getActions: () => Promise.resolve([]), - getEmbeddableFactory: api.getEmbeddableFactory, - getAllEmbeddableFactories: api.getEmbeddableFactories, - overlays: coreStart.overlays, - notifications: coreStart.notifications, - application: coreStart.application, - inspector, - SavedObjectFinder: () => null, - } - ); + + const parent = new HelloWorldContainer({ id: 'root', panels: {} }, { + getEmbeddableFactory: api.getEmbeddableFactory, + } as any); const embeddable = await parent.addNewEmbeddable< FilterableContainerInput, EmbeddableOutput, @@ -130,27 +119,17 @@ 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 inspector = inspectorPluginMock.createStartContract(); + const { doStart, setup } = testPlugin(); const factory = new FilterableEmbeddableFactory(); setup.registerEmbeddableFactory(factory.type, factory); const api = doStart(); const applyFilterAction = createFilterAction(); - const parent = new HelloWorldContainer( - { id: 'root', panels: {} }, - { - getActions: () => Promise.resolve([]), - getEmbeddableFactory: api.getEmbeddableFactory, - getAllEmbeddableFactories: api.getEmbeddableFactories, - overlays: coreStart.overlays, - notifications: coreStart.notifications, - application: coreStart.application, - inspector, - SavedObjectFinder: () => null, - } - ); + + const parent = new HelloWorldContainer({ id: 'root', panels: {} }, { + getEmbeddableFactory: api.getEmbeddableFactory, + } as any); const embeddable = await parent.addNewEmbeddable< FilterableContainerInput, diff --git a/src/plugins/embeddable/public/tests/container.test.ts b/src/plugins/embeddable/public/tests/container.test.ts index 4cd01abaf79953..490f0c00c7c4d0 100644 --- a/src/plugins/embeddable/public/tests/container.test.ts +++ b/src/plugins/embeddable/public/tests/container.test.ts @@ -48,6 +48,7 @@ import { coreMock } from '../../../../core/public/mocks'; import { testPlugin } from './test_plugin'; import { of } from './helpers'; import { esFilters, Filter } from '../../../../plugins/data/public'; +import { createEmbeddablePanelMock } from '../mocks'; async function creatHelloWorldContainerAndEmbeddable( containerInput: ContainerInput = { id: 'hello', panels: {} }, @@ -68,15 +69,18 @@ async function creatHelloWorldContainerAndEmbeddable( const start = doStart(); - const container = new HelloWorldContainer(containerInput, { + const testPanel = createEmbeddablePanelMock({ getActions: uiActions.getTriggerCompatibleActions, getEmbeddableFactory: start.getEmbeddableFactory, getAllEmbeddableFactories: start.getEmbeddableFactories, overlays: coreStart.overlays, notifications: coreStart.notifications, application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + }); + + const container = new HelloWorldContainer(containerInput, { + getEmbeddableFactory: start.getEmbeddableFactory, + panelComponent: testPanel, }); const embeddable = await container.addNewEmbeddable< ContactCardEmbeddableInput, @@ -88,7 +92,7 @@ async function creatHelloWorldContainerAndEmbeddable( throw new Error('Error adding embeddable'); } - return { container, embeddable, coreSetup, coreStart, setup, start, uiActions }; + return { container, embeddable, coreSetup, coreStart, setup, start, uiActions, testPanel }; } test('Container initializes embeddables', async (done) => { @@ -131,7 +135,8 @@ test('Container.addNewEmbeddable', async () => { }); test('Container.removeEmbeddable removes and cleans up', async (done) => { - const { start, coreStart, uiActions } = await creatHelloWorldContainerAndEmbeddable(); + const { start, testPanel } = await creatHelloWorldContainerAndEmbeddable(); + const container = new HelloWorldContainer( { id: 'hello', @@ -143,14 +148,8 @@ test('Container.removeEmbeddable removes and cleans up', async (done) => { }, }, { - getActions: uiActions.getTriggerCompatibleActions, getEmbeddableFactory: start.getEmbeddableFactory, - getAllEmbeddableFactories: start.getEmbeddableFactories, - overlays: coreStart.overlays, - notifications: coreStart.notifications, - application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + panelComponent: testPanel, } ); const embeddable = await container.addNewEmbeddable< @@ -323,15 +322,17 @@ test(`Container updates its state when a child's input is updated`, async (done) // Make sure a brand new container built off the output of container also creates an embeddable // with "Dr.", not the default the embeddable was first added with. Makes sure changed input // is preserved with the container. - const containerClone = new HelloWorldContainer(container.getInput(), { + const testPanel = createEmbeddablePanelMock({ getActions: uiActions.getTriggerCompatibleActions, - getAllEmbeddableFactories: start.getEmbeddableFactories, getEmbeddableFactory: start.getEmbeddableFactory, - notifications: coreStart.notifications, + getAllEmbeddableFactories: start.getEmbeddableFactories, overlays: coreStart.overlays, + notifications: coreStart.notifications, application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + }); + const containerClone = new HelloWorldContainer(container.getInput(), { + getEmbeddableFactory: start.getEmbeddableFactory, + panelComponent: testPanel, }); const cloneSubscription = Rx.merge( containerClone.getOutput$(), @@ -575,6 +576,14 @@ test('Container changes made directly after adding a new embeddable are propagat const start = doStart(); + const testPanel = createEmbeddablePanelMock({ + getActions: uiActions.getTriggerCompatibleActions, + getEmbeddableFactory: start.getEmbeddableFactory, + getAllEmbeddableFactories: start.getEmbeddableFactories, + overlays: coreStart.overlays, + notifications: coreStart.notifications, + application: coreStart.application, + }); const container = new HelloWorldContainer( { id: 'hello', @@ -582,14 +591,8 @@ test('Container changes made directly after adding a new embeddable are propagat viewMode: ViewMode.EDIT, }, { - getActions: uiActions.getTriggerCompatibleActions, getEmbeddableFactory: start.getEmbeddableFactory, - getAllEmbeddableFactories: start.getEmbeddableFactories, - overlays: coreStart.overlays, - notifications: coreStart.notifications, - application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + panelComponent: testPanel, } ); @@ -701,20 +704,22 @@ test('untilEmbeddableLoaded() throws an error if there is no such child panel in coreMock.createStart() ); const start = doStart(); + const testPanel = createEmbeddablePanelMock({ + getActions: uiActions.getTriggerCompatibleActions, + getEmbeddableFactory: start.getEmbeddableFactory, + getAllEmbeddableFactories: start.getEmbeddableFactories, + overlays: coreStart.overlays, + notifications: coreStart.notifications, + application: coreStart.application, + }); const container = new HelloWorldContainer( { id: 'hello', panels: {}, }, { - getActions: uiActions.getTriggerCompatibleActions, getEmbeddableFactory: start.getEmbeddableFactory, - getAllEmbeddableFactories: start.getEmbeddableFactories, - overlays: coreStart.overlays, - notifications: coreStart.notifications, - application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + panelComponent: testPanel, } ); @@ -731,6 +736,14 @@ test('untilEmbeddableLoaded() resolves if child is loaded in the container', asy const factory = new HelloWorldEmbeddableFactory(); setup.registerEmbeddableFactory(factory.type, factory); const start = doStart(); + const testPanel = createEmbeddablePanelMock({ + getActions: uiActions.getTriggerCompatibleActions, + getEmbeddableFactory: start.getEmbeddableFactory, + getAllEmbeddableFactories: start.getEmbeddableFactories, + overlays: coreStart.overlays, + notifications: coreStart.notifications, + application: coreStart.application, + }); const container = new HelloWorldContainer( { id: 'hello', @@ -742,14 +755,8 @@ test('untilEmbeddableLoaded() resolves if child is loaded in the container', asy }, }, { - getActions: uiActions.getTriggerCompatibleActions, getEmbeddableFactory: start.getEmbeddableFactory, - getAllEmbeddableFactories: start.getEmbeddableFactories, - overlays: coreStart.overlays, - notifications: coreStart.notifications, - application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + panelComponent: testPanel, } ); @@ -771,6 +778,14 @@ test('untilEmbeddableLoaded resolves with undefined if child is subsequently rem setup.registerEmbeddableFactory(factory.type, factory); const start = doStart(); + const testPanel = createEmbeddablePanelMock({ + getActions: uiActions.getTriggerCompatibleActions, + getEmbeddableFactory: start.getEmbeddableFactory, + getAllEmbeddableFactories: start.getEmbeddableFactories, + overlays: coreStart.overlays, + notifications: coreStart.notifications, + application: coreStart.application, + }); const container = new HelloWorldContainer( { id: 'hello', @@ -782,14 +797,8 @@ test('untilEmbeddableLoaded resolves with undefined if child is subsequently rem }, }, { - getActions: uiActions.getTriggerCompatibleActions, getEmbeddableFactory: start.getEmbeddableFactory, - getAllEmbeddableFactories: start.getEmbeddableFactories, - overlays: coreStart.overlays, - notifications: coreStart.notifications, - application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + panelComponent: testPanel, } ); @@ -812,6 +821,14 @@ test('adding a panel then subsequently removing it before its loaded removes the }); setup.registerEmbeddableFactory(factory.type, factory); const start = doStart(); + const testPanel = createEmbeddablePanelMock({ + getActions: uiActions.getTriggerCompatibleActions, + getEmbeddableFactory: start.getEmbeddableFactory, + getAllEmbeddableFactories: start.getEmbeddableFactories, + overlays: coreStart.overlays, + notifications: coreStart.notifications, + application: coreStart.application, + }); const container = new HelloWorldContainer( { id: 'hello', @@ -823,14 +840,8 @@ test('adding a panel then subsequently removing it before its loaded removes the }, }, { - getActions: uiActions.getTriggerCompatibleActions, getEmbeddableFactory: start.getEmbeddableFactory, - getAllEmbeddableFactories: start.getEmbeddableFactories, - overlays: coreStart.overlays, - notifications: coreStart.notifications, - application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + panelComponent: testPanel, } ); 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 a9cb83504d9584..311efae49f7359 100644 --- a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx +++ b/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx @@ -37,6 +37,7 @@ import { testPlugin } from './test_plugin'; import { CustomizePanelModal } from '../lib/panel/panel_header/panel_actions/customize_title/customize_panel_modal'; import { mount } from 'enzyme'; import { EmbeddableStart } from '../plugin'; +import { createEmbeddablePanelMock } from '../mocks'; let api: EmbeddableStart; let container: Container; @@ -55,17 +56,20 @@ beforeEach(async () => { setup.registerEmbeddableFactory(contactCardFactory.type, contactCardFactory); api = doStart(); + + const testPanel = createEmbeddablePanelMock({ + getActions: uiActions.getTriggerCompatibleActions, + getEmbeddableFactory: api.getEmbeddableFactory, + getAllEmbeddableFactories: api.getEmbeddableFactories, + overlays: coreStart.overlays, + notifications: coreStart.notifications, + application: coreStart.application, + }); container = new HelloWorldContainer( { id: '123', panels: {} }, { - getActions: uiActions.getTriggerCompatibleActions, getEmbeddableFactory: api.getEmbeddableFactory, - getAllEmbeddableFactories: api.getEmbeddableFactories, - overlays: coreStart.overlays, - notifications: coreStart.notifications, - application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + panelComponent: testPanel, } ); const contactCardEmbeddable = await container.addNewEmbeddable< diff --git a/src/plugins/embeddable/public/tests/explicit_input.test.ts b/src/plugins/embeddable/public/tests/explicit_input.test.ts index 6bea4fe46a4973..d64ff94d71800e 100644 --- a/src/plugins/embeddable/public/tests/explicit_input.test.ts +++ b/src/plugins/embeddable/public/tests/explicit_input.test.ts @@ -36,6 +36,7 @@ import { HelloWorldContainer } from '../lib/test_samples/embeddables/hello_world // eslint-disable-next-line import { coreMock } from '../../../../core/public/mocks'; import { esFilters, Filter } from '../../../../plugins/data/public'; +import { createEmbeddablePanelMock } from '../mocks'; const { setup, doStart, coreStart, uiActions } = testPlugin( coreMock.createSetup(), @@ -80,17 +81,19 @@ test('Explicit embeddable input mapped to undefined will default to inherited', }); test('Explicit embeddable input mapped to undefined with no inherited value will get passed to embeddable', async (done) => { + const testPanel = createEmbeddablePanelMock({ + getActions: uiActions.getTriggerCompatibleActions, + getEmbeddableFactory: start.getEmbeddableFactory, + getAllEmbeddableFactories: start.getEmbeddableFactories, + overlays: coreStart.overlays, + notifications: coreStart.notifications, + application: coreStart.application, + }); const container = new HelloWorldContainer( { id: 'hello', panels: {} }, { - getActions: uiActions.getTriggerCompatibleActions, - getAllEmbeddableFactories: start.getEmbeddableFactories, getEmbeddableFactory: start.getEmbeddableFactory, - notifications: coreStart.notifications, - overlays: coreStart.overlays, - application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + panelComponent: testPanel, } ); @@ -121,6 +124,14 @@ test('Explicit embeddable input mapped to undefined with no inherited value will // but before the embeddable factory returns the embeddable, that the `inheritedChildInput` and // embeddable input comparisons won't cause explicit input to be set when it shouldn't. test('Explicit input tests in async situations', (done: () => void) => { + const testPanel = createEmbeddablePanelMock({ + getActions: uiActions.getTriggerCompatibleActions, + getEmbeddableFactory: start.getEmbeddableFactory, + getAllEmbeddableFactories: start.getEmbeddableFactories, + overlays: coreStart.overlays, + notifications: coreStart.notifications, + application: coreStart.application, + }); const container = new HelloWorldContainer( { id: 'hello', @@ -132,14 +143,8 @@ test('Explicit input tests in async situations', (done: () => void) => { }, }, { - getActions: uiActions.getTriggerCompatibleActions, - getAllEmbeddableFactories: start.getEmbeddableFactories, getEmbeddableFactory: start.getEmbeddableFactory, - notifications: coreStart.notifications, - overlays: coreStart.overlays, - application: coreStart.application, - inspector: {} as any, - SavedObjectFinder: () => null, + panelComponent: testPanel, } ); diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 70c3bc2c1ed05d..05644eddc5fcac 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -66,6 +66,7 @@ const createInstance = async () => { inspector: inspectorPluginMock.createStartContract(), uiActions: uiActionsPluginMock.createStartContract(), application: applicationServiceMock.createStartContract(), + embeddable: embeddablePluginMock.createStartContract(), }); return { diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index ef64eccfea31f2..20cf6c6337e423 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -45,6 +45,7 @@ import { setChrome, setOverlays, setSavedSearchLoader, + setEmbeddable, } from './services'; import { VISUALIZE_EMBEDDABLE_TYPE, @@ -52,7 +53,7 @@ import { createVisEmbeddableFromObject, } from './embeddable'; import { ExpressionsSetup, ExpressionsStart } from '../../expressions/public'; -import { EmbeddableSetup } from '../../embeddable/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../embeddable/public'; import { visualization as visualizationFunction } from './expressions/visualization_function'; import { visualization as visualizationRenderer } from './expressions/visualization_renderer'; import { range as rangeExpressionFunction } from './expression_functions/range'; @@ -102,6 +103,7 @@ export interface VisualizationsSetupDeps { export interface VisualizationsStartDeps { data: DataPublicPluginStart; expressions: ExpressionsStart; + embeddable: EmbeddableStart; inspector: InspectorStart; uiActions: UiActionsStart; application: ApplicationStart; @@ -151,11 +153,12 @@ export class VisualizationsPlugin public start( core: CoreStart, - { data, expressions, uiActions }: VisualizationsStartDeps + { data, expressions, uiActions, embeddable }: VisualizationsStartDeps ): VisualizationsStart { const types = this.types.start(); setI18n(core.i18n); setTypes(types); + setEmbeddable(embeddable); setApplication(core.application); setCapabilities(core.application.capabilities); setHttp(core.http); diff --git a/src/plugins/visualizations/public/services.ts b/src/plugins/visualizations/public/services.ts index 15055022af8aab..0761b8862e8e36 100644 --- a/src/plugins/visualizations/public/services.ts +++ b/src/plugins/visualizations/public/services.ts @@ -40,6 +40,7 @@ import { ExpressionsStart } from '../../../plugins/expressions/public'; import { UiActionsStart } from '../../../plugins/ui_actions/public'; import { SavedVisualizationsLoader } from './saved_visualizations'; import { SavedObjectLoader } from '../../saved_objects/public'; +import { EmbeddableStart } from '../../embeddable/public'; export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); @@ -49,6 +50,8 @@ export const [getHttp, setHttp] = createGetterSetter('Http'); export const [getApplication, setApplication] = createGetterSetter('Application'); +export const [getEmbeddable, setEmbeddable] = createGetterSetter('Embeddable'); + export const [getSavedObjects, setSavedObjects] = createGetterSetter( 'SavedObjects' ); diff --git a/src/plugins/visualize/kibana.json b/src/plugins/visualize/kibana.json index e6bd4d94be09e5..817a8262f91988 100644 --- a/src/plugins/visualize/kibana.json +++ b/src/plugins/visualize/kibana.json @@ -11,8 +11,5 @@ "visualizations", "dashboard" ], - "optionalPlugins": [ - "home", - "share" - ] + "optionalPlugins": ["home", "share"] } diff --git a/src/plugins/visualize/public/application/editor/editor.js b/src/plugins/visualize/public/application/editor/editor.js index 8e5ab5251015d7..5b6c6b41f08db7 100644 --- a/src/plugins/visualize/public/application/editor/editor.js +++ b/src/plugins/visualize/public/application/editor/editor.js @@ -114,7 +114,6 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState const originatingApp = $route.current.params[EMBEDDABLE_ORIGINATING_APP_PARAM]; removeQueryParam(history, EMBEDDABLE_ORIGINATING_APP_PARAM); - $scope.getOriginatingApp = () => originatingApp; const visStateToEditorState = () => { diff --git a/x-pack/legacy/plugins/reporting/.chromium/chromium-312d84c-darwin.zip b/x-pack/legacy/plugins/reporting/.chromium/chromium-312d84c-darwin.zip new file mode 100644 index 00000000000000..07cd5365424bea Binary files /dev/null and b/x-pack/legacy/plugins/reporting/.chromium/chromium-312d84c-darwin.zip differ diff --git a/x-pack/legacy/plugins/reporting/.chromium/chromium-312d84c-linux.zip b/x-pack/legacy/plugins/reporting/.chromium/chromium-312d84c-linux.zip new file mode 100644 index 00000000000000..9d5a18ff3a9062 Binary files /dev/null and b/x-pack/legacy/plugins/reporting/.chromium/chromium-312d84c-linux.zip differ diff --git a/x-pack/legacy/plugins/reporting/.chromium/chromium-312d84c-windows.zip b/x-pack/legacy/plugins/reporting/.chromium/chromium-312d84c-windows.zip new file mode 100644 index 00000000000000..43aa95c8f14b38 Binary files /dev/null and b/x-pack/legacy/plugins/reporting/.chromium/chromium-312d84c-windows.zip differ diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index 2dd116d5ada082..ad368a912cd8cc 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -11,12 +11,10 @@ import { StartDeps } from '../../plugin'; import { IEmbeddable, EmbeddableFactory, - EmbeddablePanel, EmbeddableFactoryNotFoundError, } from '../../../../../../src/plugins/embeddable/public'; import { EmbeddableExpression } from '../../expression_types/embeddable'; import { RendererStrings } from '../../../i18n'; -import { getSavedObjectFinder } from '../../../../../../src/plugins/saved_objects/public'; import { embeddableInputToExpression } from './embeddable_input_to_expression'; import { EmbeddableInput } from '../../expression_types'; import { RendererHandlers } from '../../../types'; @@ -38,17 +36,7 @@ const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => { style={{ width: domNode.offsetWidth, height: domNode.offsetHeight, cursor: 'auto' }} > - + ); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts index e5bce31ee6de13..da461609f0b836 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts @@ -12,7 +12,7 @@ type HttpResponse = Record | any[]; // Register helpers to mock HTTP Requests const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { const setLoadTemplatesResponse = (response: HttpResponse = []) => { - server.respondWith('GET', `${API_BASE_PATH}/templates`, [ + server.respondWith('GET', `${API_BASE_PATH}/index-templates`, [ 200, { 'Content-Type': 'application/json' }, JSON.stringify(response), @@ -28,7 +28,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { }; const setDeleteTemplateResponse = (response: HttpResponse = []) => { - server.respondWith('DELETE', `${API_BASE_PATH}/templates`, [ + server.respondWith('POST', `${API_BASE_PATH}/delete-index-templates`, [ 200, { 'Content-Type': 'application/json' }, JSON.stringify(response), @@ -39,7 +39,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { const status = error ? error.status || 400 : 200; const body = error ? error.body : response; - server.respondWith('GET', `${API_BASE_PATH}/templates/:id`, [ + server.respondWith('GET', `${API_BASE_PATH}/index-templates/:id`, [ status, { 'Content-Type': 'application/json' }, JSON.stringify(body), @@ -50,7 +50,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { const status = error ? error.body.status || 400 : 200; const body = error ? JSON.stringify(error.body) : JSON.stringify(response); - server.respondWith('PUT', `${API_BASE_PATH}/templates`, [ + server.respondWith('POST', `${API_BASE_PATH}/index-templates`, [ status, { 'Content-Type': 'application/json' }, body, @@ -61,7 +61,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { const status = error ? error.status || 400 : 200; const body = error ? JSON.stringify(error.body) : JSON.stringify(response); - server.respondWith('PUT', `${API_BASE_PATH}/templates/:name`, [ + server.respondWith('PUT', `${API_BASE_PATH}/index-templates/:name`, [ status, { 'Content-Type': 'application/json' }, body, diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts index 18e5edb5c2250e..8e7755a65af3cf 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts @@ -16,6 +16,7 @@ export type TestSubjects = | 'cell' | 'closeDetailsButton' | 'createTemplateButton' + | 'createLegacyTemplateButton' | 'deleteSystemTemplateCallOut' | 'deleteTemplateButton' | 'deleteTemplatesConfirmation' @@ -46,4 +47,7 @@ export type TestSubjects = | 'templateDetails.title' | 'templateList' | 'templateTable' - | 'templatesTab'; + | 'templatesTab' + | 'legacyTemplateTable' + | 'viewButton' + | 'filterList.filterItem'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts index d195ce46c2f540..a7ac2ebf9bb024 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts @@ -7,9 +7,17 @@ import { act } from 'react-dom/test-utils'; import { setupEnvironment, nextTick } from '../helpers'; - import { HomeTestBed, setup } from './home.helpers'; +/** + * The below import is required to avoid a console error warn from the "brace" package + * console.warn ../node_modules/brace/index.js:3999 + Could not load worker ReferenceError: Worker is not defined + at createWorker (//node_modules/brace/index.js:17992:5) + */ +import { stubWebWorker } from '../../../../../test_utils/stub_web_worker'; +stubWebWorker(); + describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: HomeTestBed; @@ -62,7 +70,7 @@ describe('', () => { expect(exists('indicesList')).toBe(true); expect(exists('templateList')).toBe(false); - httpRequestsMockHelpers.setLoadTemplatesResponse([]); + httpRequestsMockHelpers.setLoadTemplatesResponse({ templates: [], legacyTemplates: [] }); actions.selectHomeTab('templatesTab'); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts index 5260dc64d0c91c..98bd3077670a75 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts @@ -12,7 +12,6 @@ import { TestBed, TestBedConfig, findTestSubject, - nextTick, } from '../../../../../test_utils'; // NOTE: We have to use the Home component instead of the TemplateList component because we depend // upon react router to provide the name of the template to load in the detail panel. @@ -45,6 +44,7 @@ export interface IndexTemplatesTabTestBed extends TestBed { clickTemplateAt: (index: number) => void; clickCloseDetailsButton: () => void; clickActionMenu: (name: TemplateDeserialized['name']) => void; + toggleViewItem: (view: 'composable' | 'system') => void; }; } @@ -102,15 +102,14 @@ export const setup = async (): Promise => { const clickTemplateAt = async (index: number) => { const { component, table, router } = testBed; - const { rows } = table.getMetaData('templateTable'); + const { rows } = table.getMetaData('legacyTemplateTable'); const templateLink = findTestSubject(rows[index].reactWrapper, 'templateDetailsLink'); + const { href } = templateLink.props(); await act(async () => { - const { href } = templateLink.props(); router.navigateTo(href!); - await nextTick(); - component.update(); }); + component.update(); }; const clickCloseDetailsButton = () => { @@ -119,6 +118,23 @@ export const setup = async (): Promise => { find('closeDetailsButton').simulate('click'); }; + const toggleViewItem = (view: 'composable' | 'system') => { + const { find, component } = testBed; + const views = ['composable', 'system']; + + // First open the pop over + act(() => { + find('viewButton').simulate('click'); + }); + component.update(); + + // Then click on a filter item + act(() => { + find('filterList.filterItem').at(views.indexOf(view)).simulate('click'); + }); + component.update(); + }; + return { ...testBed, findAction, @@ -130,6 +146,7 @@ export const setup = async (): Promise => { clickTemplateAt, clickCloseDetailsButton, clickActionMenu, + toggleViewItem, }, }; }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts index c9a279e90d0e0a..8f6a8dddeb195e 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts @@ -8,16 +8,18 @@ import { act } from 'react-dom/test-utils'; import * as fixtures from '../../../test/fixtures'; import { API_BASE_PATH } from '../../../common/constants'; -import { setupEnvironment, nextTick, getRandomString } from '../helpers'; +import { setupEnvironment, getRandomString } from '../helpers'; import { IndexTemplatesTabTestBed, setup } from './index_templates_tab.helpers'; const removeWhiteSpaceOnArrayValues = (array: any[]) => array.map((value) => { - if (!value.trim) { + if (typeof value !== 'string') { return value; } - return value.trim(); + + // Convert non breaking spaces ( ) to ordinary space + return value.trim().replace(/\s/g, ' '); }); describe('Index Templates tab', () => { @@ -31,13 +33,8 @@ describe('Index Templates tab', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadIndicesResponse([]); - testBed = await setup(); - await act(async () => { - const { component } = testBed; - - await nextTick(); - component.update(); + testBed = await setup(); }); }); @@ -45,14 +42,12 @@ describe('Index Templates tab', () => { beforeEach(async () => { const { actions, component } = testBed; - httpRequestsMockHelpers.setLoadTemplatesResponse([]); - - actions.goToTemplatesList(); + httpRequestsMockHelpers.setLoadTemplatesResponse({ templates: [], legacyTemplates: [] }); await act(async () => { - await nextTick(); - component.update(); + actions.goToTemplatesList(); }); + component.update(); }); test('should display an empty prompt', async () => { @@ -64,6 +59,9 @@ describe('Index Templates tab', () => { }); describe('when there are index templates', () => { + // Add a default loadIndexTemplate response + httpRequestsMockHelpers.setLoadTemplateResponse(fixtures.getTemplate()); + const template1 = fixtures.getTemplate({ name: `a${getRandomString()}`, indexPatterns: ['template1Pattern1*', 'template1Pattern2'], @@ -78,37 +76,87 @@ describe('Index Templates tab', () => { }, }, }); + const template2 = fixtures.getTemplate({ name: `b${getRandomString()}`, indexPatterns: ['template2Pattern1*'], }); + const template3 = fixtures.getTemplate({ name: `.c${getRandomString()}`, // mock system template indexPatterns: ['template3Pattern1*', 'template3Pattern2', 'template3Pattern3'], }); + const template4 = fixtures.getTemplate({ + name: `a${getRandomString()}`, + indexPatterns: ['template4Pattern1*', 'template4Pattern2'], + template: { + settings: { + index: { + number_of_shards: '1', + lifecycle: { + name: 'my_ilm_policy', + }, + }, + }, + }, + isLegacy: true, + }); + + const template5 = fixtures.getTemplate({ + name: `b${getRandomString()}`, + indexPatterns: ['template5Pattern1*'], + isLegacy: true, + }); + + const template6 = fixtures.getTemplate({ + name: `.c${getRandomString()}`, // mock system template + indexPatterns: ['template6Pattern1*', 'template6Pattern2', 'template6Pattern3'], + isLegacy: true, + }); + const templates = [template1, template2, template3]; + const legacyTemplates = [template4, template5, template6]; beforeEach(async () => { const { actions, component } = testBed; - httpRequestsMockHelpers.setLoadTemplatesResponse(templates); - - actions.goToTemplatesList(); + httpRequestsMockHelpers.setLoadTemplatesResponse({ templates, legacyTemplates }); await act(async () => { - await nextTick(); - component.update(); + actions.goToTemplatesList(); }); + component.update(); }); test('should list them in the table', async () => { const { table } = testBed; const { tableCellsValues } = table.getMetaData('templateTable'); + const { tableCellsValues: legacyTableCellsValues } = table.getMetaData('legacyTemplateTable'); + // Test composable table content tableCellsValues.forEach((row, i) => { const template = templates[i]; + const { name, indexPatterns, priority, ilmPolicy, composedOf } = template; + + const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : ''; + const composedOfString = composedOf ? composedOf.join(',') : ''; + const priorityFormatted = priority ? priority.toString() : ''; + + expect(removeWhiteSpaceOnArrayValues(row)).toEqual([ + name, + indexPatterns.join(', '), + ilmPolicyName, + composedOfString, + priorityFormatted, + 'M S A', // Mappings Settings Aliases badges + ]); + }); + + // Test legacy table content + legacyTableCellsValues.forEach((row, i) => { + const template = legacyTemplates[i]; const { name, indexPatterns, order, ilmPolicy } = template; const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : ''; @@ -129,44 +177,40 @@ describe('Index Templates tab', () => { }); test('should have a button to reload the index templates', async () => { - const { component, exists, actions } = testBed; + const { exists, actions } = testBed; const totalRequests = server.requests.length; expect(exists('reloadButton')).toBe(true); await act(async () => { actions.clickReloadButton(); - await nextTick(); - component.update(); }); expect(server.requests.length).toBe(totalRequests + 1); - expect(server.requests[server.requests.length - 1].url).toBe(`${API_BASE_PATH}/templates`); + expect(server.requests[server.requests.length - 1].url).toBe( + `${API_BASE_PATH}/index-templates` + ); }); test('should have a button to create a new template', () => { const { exists } = testBed; - expect(exists('createTemplateButton')).toBe(true); + expect(exists('createLegacyTemplateButton')).toBe(true); }); test('should have a switch to view system templates', async () => { - const { table, exists, component, form } = testBed; - const { rows } = table.getMetaData('templateTable'); + const { table, exists, actions } = testBed; + const { rows } = table.getMetaData('legacyTemplateTable'); expect(rows.length).toEqual( - templates.filter((template) => !template.name.startsWith('.')).length + legacyTemplates.filter((template) => !template.name.startsWith('.')).length ); - expect(exists('systemTemplatesSwitch')).toBe(true); + expect(exists('viewButton')).toBe(true); - await act(async () => { - form.toggleEuiSwitch('systemTemplatesSwitch'); - await nextTick(); - component.update(); - }); + actions.toggleViewItem('system'); - const { rows: updatedRows } = table.getMetaData('templateTable'); - expect(updatedRows.length).toEqual(templates.length); + const { rows: updatedRows } = table.getMetaData('legacyTemplateTable'); + expect(updatedRows.length).toEqual(legacyTemplates.length); }); test('each row should have a link to the template details panel', async () => { @@ -176,12 +220,12 @@ describe('Index Templates tab', () => { expect(exists('templateList')).toBe(true); expect(exists('templateDetails')).toBe(true); - expect(find('templateDetails.title').text()).toBe(template1.name); + expect(find('templateDetails.title').text()).toBe(legacyTemplates[0].name); }); test('template actions column should have an option to delete', () => { const { actions, findAction } = testBed; - const { name: templateName } = template1; + const [{ name: templateName }] = legacyTemplates; actions.clickActionMenu(templateName); @@ -192,7 +236,7 @@ describe('Index Templates tab', () => { test('template actions column should have an option to clone', () => { const { actions, findAction } = testBed; - const { name: templateName } = template1; + const [{ name: templateName }] = legacyTemplates; actions.clickActionMenu(templateName); @@ -203,7 +247,7 @@ describe('Index Templates tab', () => { test('template actions column should have an option to edit', () => { const { actions, findAction } = testBed; - const { name: templateName } = template1; + const [{ name: templateName }] = legacyTemplates; actions.clickActionMenu(templateName); @@ -215,7 +259,7 @@ describe('Index Templates tab', () => { describe('delete index template', () => { test('should show a confirmation when clicking the delete template button', async () => { const { actions } = testBed; - const { name: templateName } = template1; + const [{ name: templateName }] = legacyTemplates; await actions.clickTemplateAction(templateName, 'delete'); @@ -231,32 +275,28 @@ describe('Index Templates tab', () => { }); test('should show a warning message when attempting to delete a system template', async () => { - const { component, form, actions } = testBed; + const { exists, actions } = testBed; - await act(async () => { - form.toggleEuiSwitch('systemTemplatesSwitch'); - await nextTick(); - component.update(); - }); + actions.toggleViewItem('system'); - const { name: systemTemplateName } = template3; + const { name: systemTemplateName } = legacyTemplates[2]; await actions.clickTemplateAction(systemTemplateName, 'delete'); - expect( - document.body.querySelector('[data-test-subj="deleteSystemTemplateCallOut"]') - ).not.toBe(null); + expect(exists('deleteSystemTemplateCallOut')).toBe(true); }); test('should send the correct HTTP request to delete an index template', async () => { - const { component, actions, table } = testBed; - const { rows } = table.getMetaData('templateTable'); + const { actions, table } = testBed; + const { rows } = table.getMetaData('legacyTemplateTable'); const templateId = rows[0].columns[2].value; - const { - name: templateName, - _kbnMeta: { formatVersion }, - } = template1; + const [ + { + name: templateName, + _kbnMeta: { isLegacy }, + }, + ] = legacyTemplates; await actions.clickTemplateAction(templateName, 'delete'); const modal = document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]'); @@ -273,16 +313,14 @@ describe('Index Templates tab', () => { await act(async () => { confirmButton!.click(); - await nextTick(); - component.update(); }); const latestRequest = server.requests[server.requests.length - 1]; expect(latestRequest.method).toBe('POST'); - expect(latestRequest.url).toBe(`${API_BASE_PATH}/delete-templates`); + expect(latestRequest.url).toBe(`${API_BASE_PATH}/delete-index-templates`); expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({ - templates: [{ name: template1.name, formatVersion }], + templates: [{ name: legacyTemplates[0].name, isLegacy }], }); }); }); @@ -292,6 +330,7 @@ describe('Index Templates tab', () => { const template = fixtures.getTemplate({ name: `a${getRandomString()}`, indexPatterns: ['template1Pattern1*', 'template1Pattern2'], + isLegacy: true, }); httpRequestsMockHelpers.setLoadTemplateResponse(template); @@ -316,7 +355,7 @@ describe('Index Templates tab', () => { test('should set the correct title', async () => { const { find } = testBed; - const { name } = template1; + const [{ name }] = legacyTemplates; expect(find('templateDetails.title').text()).toEqual(name); }); @@ -327,12 +366,10 @@ describe('Index Templates tab', () => { expect(exists('closeDetailsButton')).toBe(true); expect(exists('summaryTab')).toBe(true); - actions.clickCloseDetailsButton(); - await act(async () => { - await nextTick(); - component.update(); + actions.clickCloseDetailsButton(); }); + component.update(); expect(exists('summaryTab')).toBe(false); }); @@ -372,6 +409,7 @@ describe('Index Templates tab', () => { alias1: {}, }, }, + isLegacy: true, }); const { find, actions, exists } = testBed; @@ -412,19 +450,15 @@ describe('Index Templates tab', () => { const templateWithNoOptionalFields = fixtures.getTemplate({ name: `a${getRandomString()}`, indexPatterns: ['template1Pattern1*', 'template1Pattern2'], + isLegacy: true, }); - const { actions, find, exists, component } = testBed; + const { actions, find, exists } = testBed; httpRequestsMockHelpers.setLoadTemplateResponse(templateWithNoOptionalFields); await actions.clickTemplateAt(0); - await act(async () => { - await nextTick(); - component.update(); - }); - expect(find('templateDetails.tab').length).toBe(4); expect(exists('summaryTab')).toBe(true); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts index cf00e0f6d14e17..11c25ffbb590f5 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts @@ -8,9 +8,17 @@ import { act } from 'react-dom/test-utils'; import { API_BASE_PATH } from '../../../common/constants'; import { setupEnvironment, nextTick } from '../helpers'; - import { IndicesTestBed, setup } from './indices_tab.helpers'; +/** + * The below import is required to avoid a console error warn from the "brace" package + * console.warn ../node_modules/brace/index.js:3999 + Could not load worker ReferenceError: Worker is not defined + at createWorker (//node_modules/brace/index.js:17992:5) + */ +import { stubWebWorker } from '../../../../../test_utils/stub_web_worker'; +stubWebWorker(); + describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: IndicesTestBed; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.test.tsx index e0db9cd58ee238..6250ef0dc247d4 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.test.tsx @@ -53,6 +53,7 @@ describe.skip('', () => { template: { mappings: MAPPINGS, }, + isLegacy: true, }); beforeEach(async () => { diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx index 95545b6c66f54b..50b35fc76721a0 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../../common'; +import { CREATE_LEGACY_TEMPLATE_BY_DEFAULT } from '../../../common'; import { setupEnvironment, nextTick } from '../helpers'; import { @@ -344,7 +344,6 @@ describe.skip('', () => { const latestRequest = server.requests[server.requests.length - 1]; const expected = { - isManaged: false, name: TEMPLATE_NAME, indexPatterns: DEFAULT_INDEX_PATTERNS, template: { @@ -366,7 +365,8 @@ describe.skip('', () => { aliases: ALIASES, }, _kbnMeta: { - formatVersion: DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT, + isLegacy: CREATE_LEGACY_TEMPLATE_BY_DEFAULT, + isManaged: false, }, }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx index 6e935a5263301b..88067d479f7e7b 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx @@ -62,6 +62,7 @@ describe.skip('', () => { const templateToEdit = fixtures.getTemplate({ name: 'index_template_without_mappings', indexPatterns: ['indexPattern1'], + isLegacy: true, }); beforeEach(async () => { @@ -102,6 +103,7 @@ describe.skip('', () => { template: { mappings: MAPPING, }, + isLegacy: true, }); beforeEach(async () => { @@ -206,9 +208,9 @@ describe.skip('', () => { settings: SETTINGS, aliases: ALIASES, }, - isManaged: false, _kbnMeta: { - formatVersion: templateToEdit._kbnMeta.formatVersion, + isManaged: false, + isLegacy: templateToEdit._kbnMeta.isLegacy, }, }; diff --git a/x-pack/plugins/index_management/common/constants/index.ts b/x-pack/plugins/index_management/common/constants/index.ts index 966e2e8e64838a..526b9fede2a679 100644 --- a/x-pack/plugins/index_management/common/constants/index.ts +++ b/x-pack/plugins/index_management/common/constants/index.ts @@ -9,7 +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 { CREATE_LEGACY_TEMPLATE_BY_DEFAULT } 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 index 788e96ee895ed8..7696b3832c51e7 100644 --- a/x-pack/plugins/index_management/common/constants/index_templates.ts +++ b/x-pack/plugins/index_management/common/constants/index_templates.ts @@ -6,7 +6,7 @@ /** * 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. + * legacy and composable index template formats. This constant keeps track of whether + * we create legacy index template format by default in the UI. */ -export const DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT = 1; +export const CREATE_LEGACY_TEMPLATE_BY_DEFAULT = true; diff --git a/x-pack/plugins/index_management/common/index.ts b/x-pack/plugins/index_management/common/index.ts index 459eda7552c852..3792e322ae40be 100644 --- a/x-pack/plugins/index_management/common/index.ts +++ b/x-pack/plugins/index_management/common/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { PLUGIN, API_BASE_PATH, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from './constants'; +export { PLUGIN, API_BASE_PATH, CREATE_LEGACY_TEMPLATE_BY_DEFAULT } from './constants'; export { getTemplateParameter } from './lib'; diff --git a/x-pack/plugins/index_management/common/lib/index.ts b/x-pack/plugins/index_management/common/lib/index.ts index 33f7fbe45182e1..16eb544c56a089 100644 --- a/x-pack/plugins/index_management/common/lib/index.ts +++ b/x-pack/plugins/index_management/common/lib/index.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ export { + deserializeLegacyTemplateList, deserializeTemplateList, - deserializeV1Template, - serializeV1Template, + deserializeLegacyTemplate, + serializeLegacyTemplate, } 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 33a83d1e9335b5..249881f668d9f6 100644 --- a/x-pack/plugins/index_management/common/lib/template_serialization.ts +++ b/x-pack/plugins/index_management/common/lib/template_serialization.ts @@ -5,60 +5,34 @@ */ import { TemplateDeserialized, - TemplateV1Serialized, - TemplateV2Serialized, + LegacyTemplateSerialized, + TemplateSerialized, TemplateListItem, } from '../types'; const hasEntries = (data: object = {}) => Object.entries(data).length > 0; -export function serializeV1Template(template: TemplateDeserialized): TemplateV1Serialized { - const { - name, - version, - order, - indexPatterns, - template: { settings, aliases, mappings } = {} as TemplateDeserialized['template'], - } = template; +export function serializeTemplate(templateDeserialized: TemplateDeserialized): TemplateSerialized { + const { version, priority, indexPatterns, template, composedOf } = templateDeserialized; - const serializedTemplate: TemplateV1Serialized = { - name, + return { version, - order, + priority, + template, index_patterns: indexPatterns, - settings, - aliases, - mappings, - }; - - return serializedTemplate; -} - -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, + composed_of: composedOf, }; } -export function deserializeV2Template( - templateEs: TemplateV2Serialized, +export function deserializeTemplate( + templateEs: TemplateSerialized & { name: string }, managedTemplatePrefix?: string ): TemplateDeserialized { const { name, version, - order, index_patterns: indexPatterns, - template, + template = {}, priority, composed_of: composedOf, } = templateEs; @@ -67,49 +41,92 @@ export function deserializeV2Template( const deserializedTemplate: TemplateDeserialized = { name, version, - order, + priority, indexPatterns: indexPatterns.sort(), template, - ilmPolicy: settings && settings.index && settings.index.lifecycle, - isManaged: Boolean(managedTemplatePrefix && name.startsWith(managedTemplatePrefix)), - priority, + ilmPolicy: settings?.index?.lifecycle, composedOf, _kbnMeta: { - formatVersion: 2, + isManaged: Boolean(managedTemplatePrefix && name.startsWith(managedTemplatePrefix)), }, }; return deserializedTemplate; } -export function deserializeV1Template( - templateEs: TemplateV1Serialized, +export function deserializeTemplateList( + indexTemplates: Array<{ name: string; index_template: TemplateSerialized }>, + managedTemplatePrefix?: string +): TemplateListItem[] { + return indexTemplates.map(({ name, index_template: templateSerialized }) => { + const { + template: { mappings, settings, aliases }, + ...deserializedTemplate + } = deserializeTemplate({ name, ...templateSerialized }, managedTemplatePrefix); + + return { + ...deserializedTemplate, + hasSettings: hasEntries(settings), + hasAliases: hasEntries(aliases), + hasMappings: hasEntries(mappings), + }; + }); +} + +/** + * ------------------------------------------ + * --------- LEGACY INDEX TEMPLATES --------- + * ------------------------------------------ + */ + +export function serializeLegacyTemplate(template: TemplateDeserialized): LegacyTemplateSerialized { + const { + version, + order, + indexPatterns, + template: { settings, aliases, mappings }, + } = template; + + return { + version, + order, + index_patterns: indexPatterns, + settings, + aliases, + mappings, + }; +} + +export function deserializeLegacyTemplate( + templateEs: LegacyTemplateSerialized & { name: string }, managedTemplatePrefix?: string ): TemplateDeserialized { const { settings, aliases, mappings, ...rest } = templateEs; - const deserializedTemplateV2 = deserializeV2Template( + const deserializedTemplate = deserializeTemplate( { ...rest, template: { aliases, settings, mappings } }, managedTemplatePrefix ); return { - ...deserializedTemplateV2, + ...deserializedTemplate, + order: templateEs.order, _kbnMeta: { - formatVersion: 1, + ...deserializedTemplate._kbnMeta, + isLegacy: true, }, }; } -export function deserializeTemplateList( - indexTemplatesByName: { [key: string]: Omit }, +export function deserializeLegacyTemplateList( + indexTemplatesByName: { [key: string]: LegacyTemplateSerialized }, managedTemplatePrefix?: string ): TemplateListItem[] { return Object.entries(indexTemplatesByName).map(([name, templateSerialized]) => { const { template: { mappings, settings, aliases }, ...deserializedTemplate - } = deserializeV1Template({ name, ...templateSerialized }, managedTemplatePrefix); + } = deserializeLegacyTemplate({ name, ...templateSerialized }, managedTemplatePrefix); return { ...deserializedTemplate, diff --git a/x-pack/plugins/index_management/common/lib/utils.test.ts b/x-pack/plugins/index_management/common/lib/utils.test.ts index 221d1b009cede0..056101061a82bb 100644 --- a/x-pack/plugins/index_management/common/lib/utils.test.ts +++ b/x-pack/plugins/index_management/common/lib/utils.test.ts @@ -3,12 +3,12 @@ * 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'; +import { LegacyTemplateSerialized, TemplateSerialized } from '../types'; +import { isLegacyTemplate } from './utils'; describe('utils', () => { - describe('getTemplateVersion', () => { - test('should detect v1 template', () => { + describe('isLegacyTemplate', () => { + test('should detect legacy template', () => { const template = { name: 'my_template', index_patterns: ['logs*'], @@ -16,10 +16,10 @@ describe('utils', () => { properties: {}, }, }; - expect(getTemplateVersion(template as TemplateV1Serialized)).toBe(1); + expect(isLegacyTemplate(template as LegacyTemplateSerialized)).toBe(true); }); - test('should detect v2 template', () => { + test('should detect composable template', () => { const template = { name: 'my_template', index_patterns: ['logs*'], @@ -29,7 +29,7 @@ describe('utils', () => { }, }, }; - expect(getTemplateVersion(template as TemplateV2Serialized)).toBe(2); + expect(isLegacyTemplate(template as TemplateSerialized)).toBe(false); }); }); }); diff --git a/x-pack/plugins/index_management/common/lib/utils.ts b/x-pack/plugins/index_management/common/lib/utils.ts index eee35dc1ab4679..5a7db8ef50ab43 100644 --- a/x-pack/plugins/index_management/common/lib/utils.ts +++ b/x-pack/plugins/index_management/common/lib/utils.ts @@ -4,26 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TemplateDeserialized, TemplateV1Serialized, TemplateV2Serialized } from '../types'; +import { TemplateDeserialized, LegacyTemplateSerialized, TemplateSerialized } 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 + * Helper to know if a template has the legacy format or not + * legacy format will be supported up until 9.x but marked as deprecated from 7.8 + * new (composable) format is supported from 7.8 */ -export const getTemplateVersion = ( - template: TemplateDeserialized | TemplateV1Serialized | TemplateV2Serialized -): 1 | 2 => { - return {}.hasOwnProperty.call(template, 'template') ? 2 : 1; +export const isLegacyTemplate = ( + template: TemplateDeserialized | LegacyTemplateSerialized | TemplateSerialized +): boolean => { + return {}.hasOwnProperty.call(template, 'template') ? false : true; }; export const getTemplateParameter = ( - template: TemplateV1Serialized | TemplateV2Serialized, + template: LegacyTemplateSerialized | TemplateSerialized, setting: 'aliases' | 'settings' | 'mappings' ) => { - const formatVersion = getTemplateVersion(template); - - return formatVersion === 1 - ? (template as TemplateV1Serialized)[setting] - : (template as TemplateV2Serialized).template[setting]; + return isLegacyTemplate(template) + ? (template as LegacyTemplateSerialized)[setting] + : (template as TemplateSerialized).template[setting]; }; diff --git a/x-pack/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts index c37088982f207f..f113aa44d058f8 100644 --- a/x-pack/plugins/index_management/common/types/templates.ts +++ b/x-pack/plugins/index_management/common/types/templates.ts @@ -8,28 +8,45 @@ import { IndexSettings } from './indices'; import { Aliases } from './aliases'; import { Mappings } from './mappings'; -// Template serialized (from Elasticsearch) -interface TemplateBaseSerialized { - name: string; +/** + * Index template format from Elasticsearch + */ +export interface TemplateSerialized { index_patterns: string[]; + template: { + settings?: IndexSettings; + aliases?: Aliases; + mappings?: Mappings; + }; + composed_of?: string[]; version?: number; - order?: number; -} - -export interface TemplateV1Serialized extends TemplateBaseSerialized { - settings?: IndexSettings; - aliases?: Aliases; - mappings?: Mappings; + priority?: number; } -export interface TemplateV2Serialized extends TemplateBaseSerialized { +/** + * TemplateDeserialized is the format the UI will be working with, + * regardless if we are loading the new format (composable) index template, + * or the legacy one. Serialization is done server side. + */ +export interface TemplateDeserialized { + name: string; + indexPatterns: string[]; template: { settings?: IndexSettings; aliases?: Aliases; mappings?: Mappings; }; + composedOf?: string[]; // Used on composable index template + version?: number; priority?: number; - composed_of?: string[]; + order?: number; // Used on legacy index template + ilmPolicy?: { + name: string; + }; + _kbnMeta: { + isManaged: boolean; + isLegacy?: boolean; + }; } /** @@ -42,42 +59,30 @@ export interface TemplateListItem { indexPatterns: string[]; version?: number; order?: number; + priority?: number; hasSettings: boolean; hasAliases: boolean; hasMappings: boolean; ilmPolicy?: { name: string; }; - isManaged: boolean; _kbnMeta: { - formatVersion: IndexTemplateFormatVersion; + isManaged: boolean; + isLegacy?: boolean; }; } /** - * 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. + * ------------------------------------------ + * --------- LEGACY INDEX TEMPLATES --------- + * ------------------------------------------ */ -export interface TemplateDeserialized { - name: string; - indexPatterns: string[]; - isManaged: boolean; - template: { - settings?: IndexSettings; - aliases?: Aliases; - mappings?: Mappings; - }; - _kbnMeta: { - formatVersion: IndexTemplateFormatVersion; - }; + +export interface LegacyTemplateSerialized { + index_patterns: string[]; version?: number; - priority?: number; + settings?: IndexSettings; + aliases?: Aliases; + mappings?: Mappings; order?: number; - ilmPolicy?: { - name: string; - }; - composedOf?: string[]; } - -export type IndexTemplateFormatVersion = 1 | 2; 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 a87412ef929504..06babb0db3bd1d 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 @@ -9,7 +9,6 @@ import { EuiConfirmModal, EuiOverlayMask, EuiCallOut, EuiCheckbox, EuiBadge } fr import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { IndexTemplateFormatVersion } from '../../../common'; import { deleteTemplates } from '../services/api'; import { notificationService } from '../services/notification'; @@ -17,7 +16,7 @@ export const TemplateDeleteModal = ({ templatesToDelete, callback, }: { - templatesToDelete: Array<{ name: string; formatVersion: IndexTemplateFormatVersion }>; + templatesToDelete: Array<{ name: string; isLegacy?: boolean }>; callback: (data?: { hasDeletedTemplates: boolean }) => void; }) => { const [isDeleteConfirmed, setIsDeleteConfirmed] = useState(false); 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 7b266034bc336f..387887239aaf0b 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 @@ -23,8 +23,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { serializers } from '../../../../shared_imports'; import { - serializeV1Template, - serializeV2Template, + serializeLegacyTemplate, + serializeTemplate, } from '../../../../../common/lib/template_serialization'; import { TemplateDeserialized, getTemplateParameter } from '../../../../../common'; import { StepProps } from '../types'; @@ -60,16 +60,20 @@ export const StepReview: React.FunctionComponent = ({ template, updat indexPatterns, version, order, - _kbnMeta: { formatVersion }, + _kbnMeta: { isLegacy }, } = template!; - const serializedTemplate = - formatVersion === 1 - ? serializeV1Template(stripEmptyFields(template!) as TemplateDeserialized) - : serializeV2Template(stripEmptyFields(template!) as TemplateDeserialized); - - // Name not included in ES request body - delete serializedTemplate.name; + const serializedTemplate = isLegacy + ? serializeLegacyTemplate( + stripEmptyFields(template!, { + types: ['string'], + }) as TemplateDeserialized + ) + : serializeTemplate( + stripEmptyFields(template!, { + types: ['string'], + }) as TemplateDeserialized + ); const serializedMappings = getTemplateParameter(serializedTemplate, 'mappings'); const serializedSettings = getTemplateParameter(serializedTemplate, 'settings'); 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 0cdfaae70f1513..52e26e6d3e8959 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 { TemplateDeserialized, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../../../common'; +import { TemplateDeserialized, CREATE_LEGACY_TEMPLATE_BY_DEFAULT } from '../../../../common'; import { TemplateSteps } from './template_steps'; import { StepAliases, StepLogistics, StepMappings, StepSettings, StepReview } from './steps'; import { StepProps, DataGetterFunc } from './types'; @@ -51,9 +51,9 @@ export const TemplateForm: React.FunctionComponent = ({ name: '', indexPatterns: [], template: {}, - isManaged: false, _kbnMeta: { - formatVersion: DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT, + isManaged: false, + isLegacy: CREATE_LEGACY_TEMPLATE_BY_DEFAULT, }, }, onSave, @@ -246,7 +246,9 @@ export const TemplateForm: React.FunctionComponent = ({ iconType="check" onClick={onSave.bind( null, - stripEmptyFields(template.current!) as TemplateDeserialized + stripEmptyFields(template.current!, { + types: ['string'], + }) as TemplateDeserialized )} data-test-subj="submitButton" isLoading={isSaving} 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 index 7129e536287c11..08102ae93cc0e7 100644 --- a/x-pack/plugins/index_management/public/application/lib/index_templates.ts +++ b/x-pack/plugins/index_management/public/application/lib/index_templates.ts @@ -6,12 +6,12 @@ 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)); +export const getIsLegacyFromQueryParams = (location: Location): boolean => { + const { legacy } = parse(location.search.substring(1)); - if (!Boolean(version) || typeof version !== 'string') { - return undefined; + if (!Boolean(legacy) || typeof legacy !== 'string') { + return false; } - return +version as 1 | 2; + return legacy === 'true'; }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/filter_list_button.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/filter_list_button.tsx new file mode 100644 index 00000000000000..1c95cca3fead66 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/filter_list_button.tsx @@ -0,0 +1,89 @@ +/* + * 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 React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFilterButton, EuiPopover, EuiFilterSelectItem } from '@elastic/eui'; + +interface Filter { + name: string; + checked: 'on' | 'off'; +} + +interface Props { + filters: Filters; + onChange(filters: Filters): void; +} + +export type Filters = { + [key in T]: Filter; +}; + +export function FilterListButton({ onChange, filters }: Props) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const activeFilters = Object.values(filters).filter((v) => (v as Filter).checked === 'on'); + + const onButtonClick = () => { + setIsPopoverOpen(!isPopoverOpen); + }; + + const closePopover = () => { + setIsPopoverOpen(false); + }; + + const toggleFilter = (filter: T) => { + const previousValue = filters[filter].checked; + onChange({ + ...filters, + [filter]: { + ...filters[filter], + checked: previousValue === 'on' ? 'off' : 'on', + }, + }); + }; + + const button = ( + 0} + numActiveFilters={activeFilters.length} + data-test-subj="viewButton" + > + + + ); + + return ( + +
+ {Object.entries(filters).map(([filter, item], index) => ( + toggleFilter(filter as T)} + data-test-subj="filterItem" + > + {(item as Filter).name} + + ))} +
+
+ ); +} diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts new file mode 100644 index 00000000000000..dcaba319bb21a8 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './filter_list_button'; +export * from './template_content_indicator'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_content_indicator.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_content_indicator.tsx new file mode 100644 index 00000000000000..78e33d7940bd4e --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_content_indicator.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; + +interface Props { + mappings: boolean; + settings: boolean; + aliases: boolean; +} + +const texts = { + settings: i18n.translate('xpack.idxMgmt.templateContentIndicator.indexSettingsTooltipLabel', { + defaultMessage: 'Index settings', + }), + mappings: i18n.translate('xpack.idxMgmt.templateContentIndicator.mappingsTooltipLabel', { + defaultMessage: 'Mappings', + }), + aliases: i18n.translate('xpack.idxMgmt.templateContentIndicator.aliasesTooltipLabel', { + defaultMessage: 'Aliases', + }), +}; + +export const TemplateContentIndicator = ({ mappings, settings, aliases }: Props) => { + const getColor = (flag: boolean) => (flag ? 'primary' : 'hollow'); + + return ( + <> + + <> + M +   + + + + <> + S +   + + + + A + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/index.ts new file mode 100644 index 00000000000000..519120b559e7ba --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { LegacyTemplateDetails } from './template_details'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx new file mode 100644 index 00000000000000..ec2956973d4f6b --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx @@ -0,0 +1,327 @@ +/* + * 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 React, { Fragment, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiCallOut, + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiTab, + EuiTabs, + EuiSpacer, + EuiPopover, + EuiButton, + EuiContextMenu, +} from '@elastic/eui'; +import { + UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, + UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, + UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, + UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, +} from '../../../../../../../common/constants'; +import { TemplateDeserialized } from '../../../../../../../common'; +import { + TemplateDeleteModal, + SectionLoading, + SectionError, + Error, +} from '../../../../../components'; +import { useLoadIndexTemplate } from '../../../../../services/api'; +import { decodePath } from '../../../../../services/routing'; +import { SendRequestResponse } from '../../../../../../shared_imports'; +import { useServices } from '../../../../../app_context'; +import { TabSummary, TabMappings, TabSettings, TabAliases } from '../../template_details/tabs'; + +interface Props { + template: { name: string; isLegacy?: boolean }; + onClose: () => void; + editTemplate: (name: string, isLegacy?: boolean) => void; + cloneTemplate: (name: string, isLegacy?: boolean) => void; + reload: () => Promise; +} + +const SUMMARY_TAB_ID = 'summary'; +const MAPPINGS_TAB_ID = 'mappings'; +const ALIASES_TAB_ID = 'aliases'; +const SETTINGS_TAB_ID = 'settings'; + +const TABS = [ + { + id: SUMMARY_TAB_ID, + name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.summaryTabTitle', { + defaultMessage: 'Summary', + }), + }, + { + id: SETTINGS_TAB_ID, + name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.settingsTabTitle', { + defaultMessage: 'Settings', + }), + }, + { + id: MAPPINGS_TAB_ID, + name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.mappingsTabTitle', { + defaultMessage: 'Mappings', + }), + }, + { + id: ALIASES_TAB_ID, + name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.aliasesTabTitle', { + defaultMessage: 'Aliases', + }), + }, +]; + +const tabToComponentMap: { + [key: string]: React.FunctionComponent<{ templateDetails: TemplateDeserialized }>; +} = { + [SUMMARY_TAB_ID]: TabSummary, + [SETTINGS_TAB_ID]: TabSettings, + [MAPPINGS_TAB_ID]: TabMappings, + [ALIASES_TAB_ID]: TabAliases, +}; + +const tabToUiMetricMap: { [key: string]: string } = { + [SUMMARY_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, + [SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, + [MAPPINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, + [ALIASES_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, +}; + +export const LegacyTemplateDetails: React.FunctionComponent = ({ + template: { name: templateName, isLegacy }, + onClose, + editTemplate, + cloneTemplate, + reload, +}) => { + const { uiMetricService } = useServices(); + const decodedTemplateName = decodePath(templateName); + const { error, data: templateDetails, isLoading } = useLoadIndexTemplate( + decodedTemplateName, + isLegacy + ); + const isManaged = templateDetails?._kbnMeta.isManaged ?? false; + const [templateToDelete, setTemplateToDelete] = useState< + Array<{ name: string; isLegacy?: boolean }> + >([]); + const [activeTab, setActiveTab] = useState(SUMMARY_TAB_ID); + const [isPopoverOpen, setIsPopOverOpen] = useState(false); + + let content; + + if (isLoading) { + content = ( + + + + ); + } else if (error) { + content = ( + + } + error={error as Error} + data-test-subj="sectionError" + /> + ); + } else if (templateDetails) { + const Content = tabToComponentMap[activeTab]; + const managedTemplateCallout = isManaged ? ( + + + } + color="primary" + size="s" + > + + + + + ) : null; + + content = ( + + {managedTemplateCallout} + + + {TABS.map((tab) => ( + { + uiMetricService.trackMetric('click', tabToUiMetricMap[tab.id]); + setActiveTab(tab.id); + }} + isSelected={tab.id === activeTab} + key={tab.id} + data-test-subj="tab" + > + {tab.name} + + ))} + + + + + + + ); + } + + return ( + + {templateToDelete && templateToDelete.length > 0 ? ( + { + if (data && data.hasDeletedTemplates) { + reload(); + } else { + setTemplateToDelete([]); + } + onClose(); + }} + templatesToDelete={templateToDelete} + /> + ) : null} + + + + +

+ {decodedTemplateName} +

+
+
+ + {content} + + + + + + + + + {templateDetails && ( + + {/* Manage templates context menu */} + setIsPopOverOpen((prev) => !prev)} + > + + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopOverOpen(false)} + panelPaddingSize="none" + withTitle + anchorPosition="rightUp" + repositionOnScroll + > + editTemplate(templateName, isLegacy), + disabled: isManaged, + }, + { + name: i18n.translate( + 'xpack.idxMgmt.legacyTemplateDetails.cloneButtonLabel', + { + defaultMessage: 'Clone', + } + ), + icon: 'copy', + onClick: () => cloneTemplate(templateName, isLegacy), + }, + { + name: i18n.translate( + 'xpack.idxMgmt.legacyTemplateDetails.deleteButtonLabel', + { + defaultMessage: 'Delete', + } + ), + icon: 'trash', + onClick: () => + setTemplateToDelete([{ name: decodedTemplateName, isLegacy }]), + disabled: isManaged, + }, + ], + }, + ]} + /> + + + )} + + +
+
+ ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/index.ts new file mode 100644 index 00000000000000..a8499df45ce273 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { LegacyTemplateTable } from './template_table'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx new file mode 100644 index 00000000000000..92fedd5d68f002 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx @@ -0,0 +1,304 @@ +/* + * 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 React, { useState, Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiInMemoryTable, EuiIcon, EuiButton, EuiLink, EuiBasicTableColumn } from '@elastic/eui'; +import { ScopedHistory } from 'kibana/public'; +import { reactRouterNavigate } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { TemplateListItem } from '../../../../../../../common'; +import { UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../../../common/constants'; +import { TemplateDeleteModal } from '../../../../../components'; +import { useServices } from '../../../../../app_context'; +import { SendRequestResponse } from '../../../../../../shared_imports'; + +interface Props { + templates: TemplateListItem[]; + reload: () => Promise; + editTemplate: (name: string, isLegacy?: boolean) => void; + cloneTemplate: (name: string, isLegacy?: boolean) => void; + history: ScopedHistory; +} + +export const LegacyTemplateTable: React.FunctionComponent = ({ + templates, + reload, + editTemplate, + cloneTemplate, + history, +}) => { + const { uiMetricService } = useServices(); + const [selection, setSelection] = useState([]); + const [templatesToDelete, setTemplatesToDelete] = useState< + Array<{ name: string; isLegacy?: boolean }> + >([]); + + const columns: Array> = [ + { + field: 'name', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.nameColumnTitle', { + defaultMessage: 'Name', + }), + truncateText: true, + sortable: true, + 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) + )} + data-test-subj="templateDetailsLink" + > + {name} + + ); + }, + }, + { + field: 'indexPatterns', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.indexPatternsColumnTitle', { + defaultMessage: 'Index patterns', + }), + truncateText: true, + sortable: true, + render: (indexPatterns: string[]) => {indexPatterns.join(', ')}, + }, + { + field: 'ilmPolicy', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.ilmPolicyColumnTitle', { + defaultMessage: 'ILM policy', + }), + truncateText: true, + sortable: true, + render: (ilmPolicy: { name: string }) => + ilmPolicy && ilmPolicy.name ? ( + + {ilmPolicy.name} + + ) : null, + }, + { + field: 'order', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.orderColumnTitle', { + defaultMessage: 'Order', + }), + truncateText: true, + sortable: true, + }, + { + field: 'hasMappings', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.mappingsColumnTitle', { + defaultMessage: 'Mappings', + }), + truncateText: true, + sortable: true, + render: (hasMappings: boolean) => (hasMappings ? : null), + }, + { + field: 'hasSettings', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.settingsColumnTitle', { + defaultMessage: 'Settings', + }), + truncateText: true, + sortable: true, + render: (hasSettings: boolean) => (hasSettings ? : null), + }, + { + field: 'hasAliases', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.aliasesColumnTitle', { + defaultMessage: 'Aliases', + }), + truncateText: true, + sortable: true, + render: (hasAliases: boolean) => (hasAliases ? : null), + }, + { + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.actionColumnTitle', { + defaultMessage: 'Actions', + }), + actions: [ + { + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.actionEditText', { + defaultMessage: 'Edit', + }), + isPrimary: true, + description: i18n.translate( + 'xpack.idxMgmt.templateList.legacyTable.actionEditDecription', + { + defaultMessage: 'Edit this template', + } + ), + icon: 'pencil', + type: 'icon', + onClick: ({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => { + editTemplate(name, isLegacy); + }, + enabled: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged, + }, + { + type: 'icon', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.actionCloneTitle', { + defaultMessage: 'Clone', + }), + description: i18n.translate( + 'xpack.idxMgmt.templateList.legacyTable.actionCloneDescription', + { + defaultMessage: 'Clone this template', + } + ), + icon: 'copy', + onClick: ({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => { + cloneTemplate(name, isLegacy); + }, + }, + { + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.actionDeleteText', { + defaultMessage: 'Delete', + }), + description: i18n.translate( + 'xpack.idxMgmt.templateList.legacyTable.actionDeleteDecription', + { + defaultMessage: 'Delete this template', + } + ), + icon: 'trash', + color: 'danger', + type: 'icon', + onClick: ({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => { + setTemplatesToDelete([{ name, isLegacy }]); + }, + isPrimary: true, + enabled: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged, + }, + ], + }, + ]; + + const pagination = { + initialPageSize: 20, + pageSizeOptions: [10, 20, 50], + }; + + const sorting = { + sort: { + field: 'name', + direction: 'asc', + }, + } as const; + + const selectionConfig = { + onSelectionChange: setSelection, + selectable: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged, + selectableMessage: (selectable: boolean) => { + if (!selectable) { + return i18n.translate( + 'xpack.idxMgmt.templateList.legacyTable.deleteManagedTemplateTooltip', + { + defaultMessage: 'You cannot delete a managed template.', + } + ); + } + return ''; + }, + }; + + const searchConfig = { + box: { + incremental: true, + }, + toolsLeft: + selection.length > 0 ? ( + + setTemplatesToDelete( + selection.map(({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => ({ + name, + isLegacy, + })) + ) + } + color="danger" + > + + + ) : undefined, + toolsRight: [ + + + , + ], + }; + + return ( + + {templatesToDelete && templatesToDelete.length > 0 ? ( + { + if (data && data.hasDeletedTemplates) { + reload(); + } else { + setTemplatesToDelete([]); + } + }} + templatesToDelete={templatesToDelete} + /> + ) : null} + ({ + 'data-test-subj': 'row', + })} + cellProps={() => ({ + 'data-test-subj': 'cell', + })} + data-test-subj="legacyTemplateTable" + message={ + + } + /> + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx index ed403276af564f..9f51f114176fbd 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx @@ -4,313 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { - EuiCallOut, - EuiFlyout, - EuiFlyoutHeader, - EuiTitle, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiTab, - EuiTabs, - EuiSpacer, - EuiPopover, - EuiButton, - EuiContextMenu, -} from '@elastic/eui'; -import { - UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, - UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, - UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, - UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, -} from '../../../../../../common/constants'; -import { TemplateDeserialized, IndexTemplateFormatVersion } from '../../../../../../common'; -import { TemplateDeleteModal, SectionLoading, SectionError, Error } from '../../../../components'; -import { useLoadIndexTemplate } from '../../../../services/api'; -import { decodePath } from '../../../../services/routing'; -import { SendRequestResponse } from '../../../../../shared_imports'; -import { useServices } from '../../../../app_context'; -import { TabSummary, TabMappings, TabSettings, TabAliases } from './tabs'; +import React from 'react'; -interface Props { - template: { name: string; formatVersion: IndexTemplateFormatVersion }; - onClose: () => void; - editTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void; - cloneTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void; - reload: () => Promise; -} - -const SUMMARY_TAB_ID = 'summary'; -const MAPPINGS_TAB_ID = 'mappings'; -const ALIASES_TAB_ID = 'aliases'; -const SETTINGS_TAB_ID = 'settings'; - -const TABS = [ - { - id: SUMMARY_TAB_ID, - name: i18n.translate('xpack.idxMgmt.templateDetails.summaryTabTitle', { - defaultMessage: 'Summary', - }), - }, - { - id: SETTINGS_TAB_ID, - name: i18n.translate('xpack.idxMgmt.templateDetails.settingsTabTitle', { - defaultMessage: 'Settings', - }), - }, - { - id: MAPPINGS_TAB_ID, - name: i18n.translate('xpack.idxMgmt.templateDetails.mappingsTabTitle', { - defaultMessage: 'Mappings', - }), - }, - { - id: ALIASES_TAB_ID, - name: i18n.translate('xpack.idxMgmt.templateDetails.aliasesTabTitle', { - defaultMessage: 'Aliases', - }), - }, -]; - -const tabToComponentMap: { - [key: string]: React.FunctionComponent<{ templateDetails: TemplateDeserialized }>; -} = { - [SUMMARY_TAB_ID]: TabSummary, - [SETTINGS_TAB_ID]: TabSettings, - [MAPPINGS_TAB_ID]: TabMappings, - [ALIASES_TAB_ID]: TabAliases, -}; - -const tabToUiMetricMap: { [key: string]: string } = { - [SUMMARY_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, - [SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, - [MAPPINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, - [ALIASES_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, -}; - -export const TemplateDetails: React.FunctionComponent = ({ - template: { name: templateName, formatVersion }, - onClose, - editTemplate, - cloneTemplate, - reload, -}) => { - const { uiMetricService } = useServices(); - const decodedTemplateName = decodePath(templateName); - 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); - - let content; - - if (isLoading) { - content = ( - - - - ); - } else if (error) { - content = ( - - } - error={error as Error} - data-test-subj="sectionError" - /> - ); - } else if (templateDetails) { - const Content = tabToComponentMap[activeTab]; - const managedTemplateCallout = isManaged ? ( - - - } - color="primary" - size="s" - > - - - - - ) : null; - - content = ( - - {managedTemplateCallout} - - - {TABS.map((tab) => ( - { - uiMetricService.trackMetric('click', tabToUiMetricMap[tab.id]); - setActiveTab(tab.id); - }} - isSelected={tab.id === activeTab} - key={tab.id} - data-test-subj="tab" - > - {tab.name} - - ))} - - - - - - - ); - } - - return ( - - {templateToDelete && templateToDelete.length > 0 ? ( - { - if (data && data.hasDeletedTemplates) { - reload(); - } else { - setTemplateToDelete([]); - } - onClose(); - }} - templatesToDelete={templateToDelete} - /> - ) : null} - - - - -

- {decodedTemplateName} -

-
-
- - {content} - - - - - - - - - {templateDetails && ( - - {/* Manage templates context menu */} - setIsPopOverOpen((prev) => !prev)} - > - - - } - isOpen={isPopoverOpen} - closePopover={() => setIsPopOverOpen(false)} - panelPaddingSize="none" - withTitle - anchorPosition="rightUp" - repositionOnScroll - > - editTemplate(templateName, formatVersion), - disabled: isManaged, - }, - { - name: i18n.translate('xpack.idxMgmt.templateDetails.cloneButtonLabel', { - defaultMessage: 'Clone', - }), - icon: 'copy', - onClick: () => cloneTemplate(templateName, formatVersion), - }, - { - name: i18n.translate( - 'xpack.idxMgmt.templateDetails.deleteButtonLabel', - { - defaultMessage: 'Delete', - } - ), - icon: 'trash', - onClick: () => - setTemplateToDelete([{ name: decodedTemplateName, formatVersion }]), - disabled: isManaged, - }, - ], - }, - ]} - /> - - - )} - - -
-
- ); +export const TemplateDetails: React.FunctionComponent = () => { + // TODO new (V2) templatte details + return null; }; 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 db0833ea032336..fc3d5125e30667 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 @@ -7,19 +7,20 @@ import React, { Fragment, useState, useEffect, useMemo } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { ScopedHistory } from 'kibana/public'; import { EuiEmptyPrompt, EuiSpacer, EuiTitle, EuiText, - EuiSwitch, EuiFlexItem, EuiFlexGroup, + EuiButton, } from '@elastic/eui'; import { UIM_TEMPLATE_LIST_LOAD } from '../../../../../common/constants'; -import { IndexTemplateFormatVersion } from '../../../../../common'; +import { TemplateListItem } from '../../../../../common'; import { SectionError, SectionLoading, Error } from '../../../components'; import { useLoadIndexTemplates } from '../../../services/api'; import { useServices } from '../../../app_context'; @@ -28,14 +29,20 @@ import { getTemplateListLink, getTemplateCloneLink, } from '../../../services/routing'; -import { getFormatVersionFromQueryparams } from '../../../lib/index_templates'; +import { getIsLegacyFromQueryParams } from '../../../lib/index_templates'; import { TemplateTable } from './template_table'; -import { TemplateDetails } from './template_details'; +import { LegacyTemplateTable } from './legacy_templates/template_table'; +import { LegacyTemplateDetails } from './legacy_templates/template_details'; +import { FilterListButton, Filters } from './components'; +type FilterName = 'composable' | 'system'; interface MatchParams { templateName?: string; } +const stripOutSystemTemplates = (templates: TemplateListItem[]): TemplateListItem[] => + templates.filter((template) => !template.name.startsWith('.')); + export const TemplateList: React.FunctionComponent> = ({ match: { params: { templateName }, @@ -44,122 +51,188 @@ export const TemplateList: React.FunctionComponent { const { uiMetricService } = useServices(); - const { error, isLoading, data: templates, sendRequest: reload } = useLoadIndexTemplates(); - const queryParamsFormatVersion = getFormatVersionFromQueryparams(location); + const { error, isLoading, data: allTemplates, sendRequest: reload } = useLoadIndexTemplates(); - let content; + const [filters, setFilters] = useState>({ + composable: { + name: i18n.translate('xpack.idxMgmt.indexTemplatesList.viewComposableTemplateLabel', { + defaultMessage: 'Composable templates', + }), + checked: 'on', + }, + system: { + name: i18n.translate('xpack.idxMgmt.indexTemplatesList.viewSystemTemplateLabel', { + defaultMessage: 'System templates', + }), + checked: 'off', + }, + }); - const [showSystemTemplates, setShowSystemTemplates] = useState(false); + const filteredTemplates = useMemo(() => { + if (!allTemplates) { + return { templates: [], legacyTemplates: [] }; + } - // Filter out system index templates - const filteredTemplates = useMemo( - () => (templates ? templates.filter((template) => !template.name.startsWith('.')) : []), - [templates] - ); + return filters.system.checked === 'on' + ? allTemplates + : { + templates: stripOutSystemTemplates(allTemplates.templates), + legacyTemplates: stripOutSystemTemplates(allTemplates.legacyTemplates), + }; + }, [allTemplates, filters.system.checked]); + + const showComposableTemplateTable = filters.composable.checked === 'on'; + + const selectedTemplate = Boolean(templateName) + ? { + name: templateName!, + isLegacy: getIsLegacyFromQueryParams(location), + } + : null; + + const isLegacyTemplateDetailsVisible = selectedTemplate !== null && selectedTemplate.isLegacy; + const hasTemplates = + allTemplates && (allTemplates.legacyTemplates.length > 0 || allTemplates.templates.length > 0); const closeTemplateDetails = () => { history.push(getTemplateListLink()); }; - const editTemplate = (name: string, formatVersion: IndexTemplateFormatVersion) => { - history.push(getTemplateEditLink(name, formatVersion)); + const editTemplate = (name: string, isLegacy?: boolean) => { + history.push(getTemplateEditLink(name, isLegacy)); }; - const cloneTemplate = (name: string, formatVersion: IndexTemplateFormatVersion) => { - history.push(getTemplateCloneLink(name, formatVersion)); + const cloneTemplate = (name: string, isLegacy?: boolean) => { + history.push(getTemplateCloneLink(name, isLegacy)); }; - // Track component loaded - useEffect(() => { - uiMetricService.trackMetric('loaded', UIM_TEMPLATE_LIST_LOAD); - }, [uiMetricService]); + const renderHeader = () => ( + + + + + + + + + + filters={filters} onChange={setFilters} /> + + + + + + + + ); - if (isLoading) { - content = ( - - - - ); - } else if (error) { - content = ( - + showComposableTemplateTable ? ( + <> + + + + ) : null; + + const renderLegacyTemplatesTable = () => ( + <> + + +

- } - error={error as Error} +

+
+ + - ); - } else if (Array.isArray(templates) && templates.length === 0) { - content = ( - + + ); + + const renderContent = () => { + if (isLoading) { + return ( + + + + ); + } else if (error) { + return ( + - - } - data-test-subj="emptyPrompt" - /> - ); - } else if (Array.isArray(templates) && templates.length > 0) { - content = ( - - - - - - - - - - - setShowSystemTemplates(event.target.checked)} - label={ - - } - /> - - - - + ); + } else if (!hasTemplates) { + return ( + + + + } + data-test-subj="emptyPrompt" /> - - ); - } + ); + } else { + return ( + + {/* Header */} + {renderHeader()} + + {/* Composable index templates table */} + {renderTemplatesTable()} + + {/* Legacy index templates table */} + {renderLegacyTemplatesTable()} + + ); + } + }; + + // Track component loaded + useEffect(() => { + uiMetricService.trackMetric('loaded', UIM_TEMPLATE_LIST_LOAD); + }, [uiMetricService]); return (
- {content} - {templateName && queryParamsFormatVersion !== undefined && ( - Promise; - editTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void; - cloneTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void; - history: ScopedHistory; } -export const TemplateTable: React.FunctionComponent = ({ - templates, - reload, - editTemplate, - cloneTemplate, - history, -}) => { - const { uiMetricService } = useServices(); - const [selection, setSelection] = useState([]); +export const TemplateTable: React.FunctionComponent = ({ templates, reload }) => { const [templatesToDelete, setTemplatesToDelete] = useState< - Array<{ name: string; formatVersion: IndexTemplateFormatVersion }> + Array<{ name: string; isLegacy?: boolean }> >([]); const columns: Array> = [ @@ -45,24 +31,6 @@ export const TemplateTable: React.FunctionComponent = ({ }), truncateText: true, sortable: true, - 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) - )} - data-test-subj="templateDetailsLink" - > - {name} - - ); - }, }, { field: 'indexPatterns', @@ -95,90 +63,36 @@ export const TemplateTable: React.FunctionComponent = ({ ) : null, }, { - field: 'order', - name: i18n.translate('xpack.idxMgmt.templateList.table.orderColumnTitle', { - defaultMessage: 'Order', + field: 'composedOf', + name: i18n.translate('xpack.idxMgmt.templateList.table.componentsColumnTitle', { + defaultMessage: 'Components', }), truncateText: true, sortable: true, + render: (composedOf: string[] = []) => {composedOf.join(', ')}, }, { - field: 'hasMappings', - name: i18n.translate('xpack.idxMgmt.templateList.table.mappingsColumnTitle', { - defaultMessage: 'Mappings', + field: 'priority', + name: i18n.translate('xpack.idxMgmt.templateList.table.priorityColumnTitle', { + defaultMessage: 'Priority', }), truncateText: true, sortable: true, - render: (hasMappings: boolean) => (hasMappings ? : null), }, { - field: 'hasSettings', - name: i18n.translate('xpack.idxMgmt.templateList.table.settingsColumnTitle', { - defaultMessage: 'Settings', - }), - truncateText: true, - sortable: true, - render: (hasSettings: boolean) => (hasSettings ? : null), - }, - { - field: 'hasAliases', - name: i18n.translate('xpack.idxMgmt.templateList.table.aliasesColumnTitle', { - defaultMessage: 'Aliases', + field: 'hasMappings', + name: i18n.translate('xpack.idxMgmt.templateList.table.overridesColumnTitle', { + defaultMessage: 'Overrides', }), truncateText: true, - sortable: true, - render: (hasAliases: boolean) => (hasAliases ? : null), - }, - { - name: i18n.translate('xpack.idxMgmt.templateList.table.actionColumnTitle', { - defaultMessage: 'Actions', - }), - actions: [ - { - name: i18n.translate('xpack.idxMgmt.templateList.table.actionEditText', { - defaultMessage: 'Edit', - }), - isPrimary: true, - description: i18n.translate('xpack.idxMgmt.templateList.table.actionEditDecription', { - defaultMessage: 'Edit this template', - }), - icon: 'pencil', - type: 'icon', - onClick: ({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => { - editTemplate(name, formatVersion); - }, - enabled: ({ isManaged }: TemplateListItem) => !isManaged, - }, - { - type: 'icon', - name: i18n.translate('xpack.idxMgmt.templateList.table.actionCloneTitle', { - defaultMessage: 'Clone', - }), - description: i18n.translate('xpack.idxMgmt.templateList.table.actionCloneDescription', { - defaultMessage: 'Clone this template', - }), - icon: 'copy', - onClick: ({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => { - cloneTemplate(name, formatVersion); - }, - }, - { - name: i18n.translate('xpack.idxMgmt.templateList.table.actionDeleteText', { - defaultMessage: 'Delete', - }), - description: i18n.translate('xpack.idxMgmt.templateList.table.actionDeleteDecription', { - defaultMessage: 'Delete this template', - }), - icon: 'trash', - color: 'danger', - type: 'icon', - onClick: ({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => { - setTemplatesToDelete([{ name, formatVersion }]); - }, - isPrimary: true, - enabled: ({ isManaged }: TemplateListItem) => !isManaged, - }, - ], + sortable: false, + render: (_, item) => ( + + ), }, ]; @@ -194,70 +108,10 @@ export const TemplateTable: React.FunctionComponent = ({ }, } as const; - const selectionConfig = { - onSelectionChange: setSelection, - selectable: ({ isManaged }: TemplateListItem) => !isManaged, - selectableMessage: (selectable: boolean) => { - if (!selectable) { - return i18n.translate('xpack.idxMgmt.templateList.table.deleteManagedTemplateTooltip', { - defaultMessage: 'You cannot delete a managed template.', - }); - } - return ''; - }, - }; - const searchConfig = { box: { incremental: true, }, - toolsLeft: - selection.length > 0 ? ( - - setTemplatesToDelete( - selection.map(({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => ({ - name, - formatVersion, - })) - ) - } - color="danger" - > - - - ) : undefined, - toolsRight: [ - - - , - - - , - ], }; return ( @@ -280,8 +134,7 @@ export const TemplateTable: React.FunctionComponent = ({ columns={columns} search={searchConfig} sorting={sorting} - isSelectable={true} - selection={selectionConfig} + isSelectable={false} pagination={pagination} rowProps={() => ({ 'data-test-subj': 'row', 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 b69e441feb176d..8bdd230f899524 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 @@ -8,12 +8,12 @@ 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 { TemplateDeserialized } from '../../../../common'; import { TemplateForm, SectionLoading, SectionError, Error } from '../../components'; import { breadcrumbService } from '../../services/breadcrumbs'; import { decodePath, getTemplateDetailsLink } from '../../services/routing'; import { saveTemplate, useLoadIndexTemplate } from '../../services/api'; -import { getFormatVersionFromQueryparams } from '../../lib/index_templates'; +import { getIsLegacyFromQueryParams } from '../../lib/index_templates'; interface MatchParams { name: string; @@ -27,14 +27,13 @@ export const TemplateClone: React.FunctionComponent { const decodedTemplateName = decodePath(name); - const formatVersion = - getFormatVersionFromQueryparams(location) ?? DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT; + const isLegacy = getIsLegacyFromQueryParams(location); const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); const { error: templateToCloneError, data: templateToClone, isLoading } = useLoadIndexTemplate( decodedTemplateName, - formatVersion + isLegacy ); const onSave = async (template: TemplateDeserialized) => { @@ -52,7 +51,7 @@ export const TemplateClone: React.FunctionComponent { diff --git a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx index 27341685f3dc01..f567b9835d53df 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx @@ -33,7 +33,7 @@ export const TemplateCreate: React.FunctionComponent = ({ h return; } - history.push(getTemplateDetailsLink(name, template._kbnMeta.formatVersion)); + history.push(getTemplateDetailsLink(name, template._kbnMeta.isLegacy)); }; const clearSaveError = () => { diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx index 9ad26d0af802d8..d3e539989bc96c 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx @@ -8,12 +8,12 @@ import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui'; -import { TemplateDeserialized, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../../../common'; +import { TemplateDeserialized } from '../../../../common'; import { breadcrumbService } from '../../services/breadcrumbs'; import { useLoadIndexTemplate, updateTemplate } from '../../services/api'; import { decodePath, getTemplateDetailsLink } from '../../services/routing'; import { SectionLoading, SectionError, TemplateForm, Error } from '../../components'; -import { getFormatVersionFromQueryparams } from '../../lib/index_templates'; +import { getIsLegacyFromQueryParams } from '../../lib/index_templates'; interface MatchParams { name: string; @@ -27,16 +27,12 @@ export const TemplateEdit: React.FunctionComponent { const decodedTemplateName = decodePath(name); - const formatVersion = - getFormatVersionFromQueryparams(location) ?? DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT; + const isLegacy = getIsLegacyFromQueryParams(location); const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); - const { error, data: template, isLoading } = useLoadIndexTemplate( - decodedTemplateName, - formatVersion - ); + const { error, data: template, isLoading } = useLoadIndexTemplate(decodedTemplateName, isLegacy); useEffect(() => { breadcrumbService.setBreadcrumbs('templateEdit'); @@ -55,7 +51,7 @@ export const TemplateEdit: React.FunctionComponent { @@ -87,7 +83,10 @@ export const TemplateEdit: React.FunctionComponent ); } else if (template) { - const { name: templateName, isManaged } = template; + const { + name: templateName, + _kbnMeta: { isManaged }, + } = template; const isSystemTemplate = templateName && templateName.startsWith('.'); if (isManaged) { diff --git a/x-pack/plugins/index_management/public/application/services/api.ts b/x-pack/plugins/index_management/public/application/services/api.ts index 181707b3661b4f..3961942b83ea38 100644 --- a/x-pack/plugins/index_management/public/application/services/api.ts +++ b/x-pack/plugins/index_management/public/application/services/api.ts @@ -37,11 +37,7 @@ import { TAB_SETTINGS, TAB_MAPPING, TAB_STATS } from '../constants'; import { useRequest, sendRequest } from './use_request'; import { httpService } from './http'; import { UiMetricService } from './ui_metric'; -import { - TemplateDeserialized, - TemplateListItem, - IndexTemplateFormatVersion, -} from '../../../common'; +import { TemplateDeserialized, TemplateListItem } from '../../../common'; import { IndexMgmtMetricsType } from '../../types'; // Temporary hack to provide the uiMetricService instance to this file. @@ -214,17 +210,15 @@ export async function loadIndexData(type: string, indexName: string) { } export function useLoadIndexTemplates() { - return useRequest({ - path: `${API_BASE_PATH}/templates`, + return useRequest<{ templates: TemplateListItem[]; legacyTemplates: TemplateListItem[] }>({ + path: `${API_BASE_PATH}/index-templates`, method: 'get', }); } -export async function deleteTemplates( - templates: Array<{ name: string; formatVersion: IndexTemplateFormatVersion }> -) { +export async function deleteTemplates(templates: Array<{ name: string; isLegacy?: boolean }>) { const result = sendRequest({ - path: `${API_BASE_PATH}/delete-templates`, + path: `${API_BASE_PATH}/delete-index-templates`, method: 'post', body: { templates }, }); @@ -236,23 +230,20 @@ export async function deleteTemplates( return result; } -export function useLoadIndexTemplate( - name: TemplateDeserialized['name'], - formatVersion: IndexTemplateFormatVersion -) { +export function useLoadIndexTemplate(name: TemplateDeserialized['name'], isLegacy?: boolean) { return useRequest({ - path: `${API_BASE_PATH}/templates/${encodeURIComponent(name)}`, + path: `${API_BASE_PATH}/index-templates/${encodeURIComponent(name)}`, method: 'get', query: { - v: formatVersion, + legacy: isLegacy, }, }); } export async function saveTemplate(template: TemplateDeserialized, isClone?: boolean) { const result = await sendRequest({ - path: `${API_BASE_PATH}/templates`, - method: 'put', + path: `${API_BASE_PATH}/index-templates`, + method: 'post', body: JSON.stringify(template), }); @@ -266,7 +257,7 @@ export async function saveTemplate(template: TemplateDeserialized, isClone?: boo export async function updateTemplate(template: TemplateDeserialized) { const { name } = template; const result = await sendRequest({ - path: `${API_BASE_PATH}/templates/${encodeURIComponent(name)}`, + path: `${API_BASE_PATH}/index-templates/${encodeURIComponent(name)}`, method: 'put', body: JSON.stringify(template), }); diff --git a/x-pack/plugins/index_management/public/application/services/routing.ts b/x-pack/plugins/index_management/public/application/services/routing.ts index fe118b1181082e..a999c58f5bb429 100644 --- a/x-pack/plugins/index_management/public/application/services/routing.ts +++ b/x-pack/plugins/index_management/public/application/services/routing.ts @@ -3,33 +3,29 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { IndexTemplateFormatVersion } from '../../../common'; -export const getTemplateListLink = () => { - return `/templates`; -}; +export const getTemplateListLink = () => `/templates`; // Need to add some additonal encoding/decoding logic to work with React Router // For background, see: https://github.com/ReactTraining/history/issues/505 -export const getTemplateDetailsLink = ( - name: string, - formatVersion: IndexTemplateFormatVersion, - withHash = false -) => { - const baseUrl = `/templates/${encodeURIComponent(encodeURIComponent(name))}?v=${formatVersion}`; - const url = withHash ? `#${baseUrl}` : baseUrl; +export const getTemplateDetailsLink = (name: string, isLegacy?: boolean, withHash = false) => { + const baseUrl = `/templates/${encodeURIComponent(encodeURIComponent(name))}`; + let url = withHash ? `#${baseUrl}` : baseUrl; + if (isLegacy) { + url = `${url}?legacy=${isLegacy}`; + } return encodeURI(url); }; -export const getTemplateEditLink = (name: string, formatVersion: IndexTemplateFormatVersion) => { +export const getTemplateEditLink = (name: string, isLegacy?: boolean) => { return encodeURI( - `/edit_template/${encodeURIComponent(encodeURIComponent(name))}?v=${formatVersion}` + `/edit_template/${encodeURIComponent(encodeURIComponent(name))}?legacy=${isLegacy === true}` ); }; -export const getTemplateCloneLink = (name: string, formatVersion: IndexTemplateFormatVersion) => { +export const getTemplateCloneLink = (name: string, isLegacy?: boolean) => { return encodeURI( - `/clone_template/${encodeURIComponent(encodeURIComponent(name))}?v=${formatVersion}` + `/clone_template/${encodeURIComponent(encodeURIComponent(name))}?legacy=${isLegacy === true}` ); }; diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts index 1409fa8af2cebd..26e74847e3e05f 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { TemplateDeserialized } from '../../../../common'; -import { serializeV1Template } from '../../../../common/lib'; +import { serializeLegacyTemplate } from '../../../../common/lib'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; import { templateSchema } from './validate_schemas'; @@ -15,35 +15,26 @@ import { templateSchema } from './validate_schemas'; const bodySchema = templateSchema; export function registerCreateRoute({ router, license, lib }: RouteDependencies) { - router.put( - { path: addBasePath('/templates'), validate: { body: bodySchema } }, + router.post( + { path: addBasePath('/index-templates'), validate: { body: bodySchema } }, license.guardApiRoute(async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; const template = req.body as TemplateDeserialized; const { - _kbnMeta: { formatVersion }, + _kbnMeta: { isLegacy }, } = template; - if (formatVersion !== 1) { - return res.badRequest({ body: 'Only index template version 1 can be created.' }); + if (!isLegacy) { + return res.badRequest({ body: 'Only legacy index templates can be created.' }); } - // For now we format to V1 index templates. - // When the V2 API is ready we will only create V2 template format. - const serializedTemplate = serializeV1Template(template); - - const { - name, - order, - index_patterns, - version, - settings, - mappings, - aliases, - } = serializedTemplate; + const serializedTemplate = serializeLegacyTemplate(template); + const { order, index_patterns, version, settings, mappings, aliases } = serializedTemplate; // Check that template with the same name doesn't already exist - const templateExists = await callAsCurrentUser('indices.existsTemplate', { name }); + const templateExists = await callAsCurrentUser('indices.existsTemplate', { + name: template.name, + }); if (templateExists) { return res.conflict({ @@ -51,7 +42,7 @@ export function registerCreateRoute({ router, license, lib }: RouteDependencies) i18n.translate('xpack.idxMgmt.createRoute.duplicateTemplateIdErrorMessage', { defaultMessage: "There is already a template with name '{name}'.", values: { - name, + name: template.name, }, }) ), @@ -61,7 +52,7 @@ export function registerCreateRoute({ router, license, lib }: RouteDependencies) try { // Otherwise create new index template const response = await callAsCurrentUser('indices.putTemplate', { - name, + name: template.name, order, body: { index_patterns, diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts index 3dc31482b4947d..b5cc00ad6d8cc2 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts @@ -16,7 +16,7 @@ const bodySchema = schema.object({ templates: schema.arrayOf( schema.object({ name: schema.string(), - formatVersion: schema.oneOf([schema.literal(1), schema.literal(2)]), + isLegacy: schema.maybe(schema.boolean()), }) ), }); @@ -24,7 +24,7 @@ const bodySchema = schema.object({ export function registerDeleteRoute({ router, license }: RouteDependencies) { router.post( { - path: addBasePath('/delete-templates'), + path: addBasePath('/delete-index-templates'), validate: { body: bodySchema }, }, license.guardApiRoute(async (ctx, req, res) => { @@ -35,10 +35,10 @@ export function registerDeleteRoute({ router, license }: RouteDependencies) { }; await Promise.all( - templates.map(async ({ name, formatVersion }) => { + templates.map(async ({ name, isLegacy }) => { try { - if (formatVersion !== 1) { - return res.badRequest({ body: 'Only index template version 1 can be deleted.' }); + if (!isLegacy) { + return res.badRequest({ body: 'Only legacy index template can be deleted.' }); } await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.deleteTemplate', { diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts index b18a8d88d3a4a9..12ec005258a62f 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts @@ -5,20 +5,38 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { deserializeV1Template, deserializeTemplateList } from '../../../../common/lib'; +import { + deserializeLegacyTemplate, + deserializeLegacyTemplateList, + deserializeTemplateList, +} from '../../../../common/lib'; import { getManagedTemplatePrefix } from '../../../lib/get_managed_templates'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; export function registerGetAllRoute({ router, license }: RouteDependencies) { router.get( - { path: addBasePath('/templates'), validate: false }, + { path: addBasePath('/index-templates'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; const managedTemplatePrefix = await getManagedTemplatePrefix(callAsCurrentUser); - const indexTemplatesByName = await callAsCurrentUser('indices.getTemplate'); - const body = deserializeTemplateList(indexTemplatesByName, managedTemplatePrefix); + const _legacyTemplates = await callAsCurrentUser('indices.getTemplate'); + const { index_templates: _templates } = await callAsCurrentUser('transport.request', { + path: '_index_template', + method: 'GET', + }); + + const legacyTemplates = deserializeLegacyTemplateList( + _legacyTemplates, + managedTemplatePrefix + ); + const templates = deserializeTemplateList(_templates, managedTemplatePrefix); + + const body = { + templates, + legacyTemplates, + }; return res.ok({ body }); }) @@ -31,22 +49,22 @@ const paramsSchema = schema.object({ // Require the template format version (V1 or V2) to be provided as Query param const querySchema = schema.object({ - v: schema.oneOf([schema.literal('1'), schema.literal('2')]), + legacy: schema.maybe(schema.boolean()), }); export function registerGetOneRoute({ router, license, lib }: RouteDependencies) { router.get( { - path: addBasePath('/templates/{name}'), + path: addBasePath('/index-templates/{name}'), validate: { params: paramsSchema, query: querySchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { name } = req.params as typeof paramsSchema.type; + const { name } = req.params as TypeOf; const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; - const { v: version } = req.query as TypeOf; + const { legacy } = req.query as TypeOf; - if (version !== '1') { + if (!legacy) { return res.badRequest({ body: 'Only index template version 1 can be fetched.' }); } @@ -56,7 +74,7 @@ export function registerGetOneRoute({ router, license, lib }: RouteDependencies) if (indexTemplateByName[name]) { return res.ok({ - body: deserializeV1Template( + body: deserializeLegacyTemplate( { ...indexTemplateByName[name], name }, managedTemplatePrefix ), diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts index 81d7aa1b4978c1..5b2a0d8722e46a 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; import { TemplateDeserialized } from '../../../../common'; -import { serializeV1Template } from '../../../../common/lib'; +import { serializeLegacyTemplate } from '../../../../common/lib'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; import { templateSchema } from './validate_schemas'; @@ -19,7 +19,7 @@ const paramsSchema = schema.object({ export function registerUpdateRoute({ router, license, lib }: RouteDependencies) { router.put( { - path: addBasePath('/templates/{name}'), + path: addBasePath('/index-templates/{name}'), validate: { body: bodySchema, params: paramsSchema }, }, license.guardApiRoute(async (ctx, req, res) => { @@ -27,14 +27,14 @@ export function registerUpdateRoute({ router, license, lib }: RouteDependencies) const { name } = req.params as typeof paramsSchema.type; const template = req.body as TemplateDeserialized; const { - _kbnMeta: { formatVersion }, + _kbnMeta: { isLegacy }, } = template; - if (formatVersion !== 1) { - return res.badRequest({ body: 'Only index template version 1 can be edited.' }); + if (!isLegacy) { + return res.badRequest({ body: 'Only legacy index template can be edited.' }); } - const serializedTemplate = serializeV1Template(template); + const serializedTemplate = serializeLegacyTemplate(template); const { order, index_patterns, version, settings, mappings, aliases } = serializedTemplate; diff --git a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts index 491a686f811779..6ab28e90211237 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts @@ -24,8 +24,8 @@ export const templateSchema = schema.object({ rollover_alias: schema.maybe(schema.string()), }) ), - isManaged: schema.maybe(schema.boolean()), _kbnMeta: schema.object({ - formatVersion: schema.oneOf([schema.literal(1), schema.literal(2)]), + isManaged: schema.maybe(schema.boolean()), + isLegacy: schema.maybe(schema.boolean()), }), }); diff --git a/x-pack/plugins/index_management/test/fixtures/template.ts b/x-pack/plugins/index_management/test/fixtures/template.ts index 055c32d5cd5e4a..e2e93bfb365d4d 100644 --- a/x-pack/plugins/index_management/test/fixtures/template.ts +++ b/x-pack/plugins/index_management/test/fixtures/template.ts @@ -5,7 +5,7 @@ */ import { getRandomString, getRandomNumber } from '../../../../test_utils'; -import { TemplateDeserialized, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../common'; +import { TemplateDeserialized } from '../../common'; export const getTemplate = ({ name = getRandomString(), @@ -14,10 +14,11 @@ export const getTemplate = ({ indexPatterns = [], template: { settings, aliases, mappings } = {}, isManaged = false, - templateFormatVersion = DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT, + isLegacy = false, }: Partial< TemplateDeserialized & { - templateFormatVersion?: 1 | 2; + isLegacy?: boolean; + isManaged: boolean; } > = {}): TemplateDeserialized => ({ name, @@ -29,8 +30,8 @@ export const getTemplate = ({ mappings, settings, }, - isManaged, _kbnMeta: { - formatVersion: templateFormatVersion, + isManaged, + isLegacy, }, }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 233a34a67d1ec9..a282a742d614c9 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -336,6 +336,9 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: s group, alertState: stateToAlertMessage[nextState], reason, + value: mapToConditionsLookup(alertResults, (result) => result[group].currentValue), + threshold: mapToConditionsLookup(criteria, (c) => c.threshold), + metric: mapToConditionsLookup(criteria, (c) => c.metric), }); } @@ -352,3 +355,14 @@ export const FIRED_ACTIONS = { defaultMessage: 'Fired', }), }; + +const mapToConditionsLookup = ( + list: any[], + mapFn: (value: any, index: number, array: any[]) => unknown +) => + list + .map(mapFn) + .reduce( + (result: Record, value, i) => ({ ...result, [`condition${i}`]: value }), + {} + ); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 8b3903f2ee3be9..2c98a568d16de6 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -55,6 +55,30 @@ export function registerMetricThresholdAlertType(libs: InfraBackendLibs) { } ); + const valueActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.valueActionVariableDescription', + { + defaultMessage: + 'The value of the metric in the specified condition. Usage: (ctx.value.condition0, ctx.value.condition1, etc...).', + } + ); + + const metricActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.metricActionVariableDescription', + { + defaultMessage: + 'The metric name in the specified condition. Usage: (ctx.metric.condition0, ctx.metric.condition1, etc...).', + } + ); + + const thresholdActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.thresholdActionVariableDescription', + { + defaultMessage: + 'The threshold value of the metric for the specified condition. Usage: (ctx.threshold.condition0, ctx.threshold.condition1, etc...).', + } + ); + return { id: METRIC_THRESHOLD_ALERT_TYPE_ID, name: 'Metric threshold', @@ -82,6 +106,9 @@ export function registerMetricThresholdAlertType(libs: InfraBackendLibs) { { name: 'group', description: groupActionVariableDescription }, { name: 'alertState', description: alertStateActionVariableDescription }, { name: 'reason', description: reasonActionVariableDescription }, + { name: 'value', description: valueActionVariableDescription }, + { name: 'metric', description: metricActionVariableDescription }, + { name: 'threshold', description: thresholdActionVariableDescription }, ], }, producer: 'metrics', diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts index 5e83a976bd7a4a..635dce93f00276 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts @@ -83,4 +83,22 @@ foo: bar custom: { foo: 'bar' }, }); }); + + it('should support optional yaml values at root level', () => { + const streamTemplate = ` +input: logs +{{custom}} + `; + const vars = { + custom: { + type: 'yaml', + value: null, + }, + }; + + const output = createStream(vars, streamTemplate); + expect(output).toEqual({ + input: 'logs', + }); + }); }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts index 61f2f95fe20a9f..0bcb2464f8d742 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts @@ -94,7 +94,10 @@ function replaceRootLevelYamlVariables(yamlVariables: { [k: string]: any }, yaml let patchedTemplate = yamlTemplate; Object.entries(yamlVariables).forEach(([key, val]) => { - patchedTemplate = patchedTemplate.replace(new RegExp(`^"${key}"`, 'gm'), safeDump(val)); + patchedTemplate = patchedTemplate.replace( + new RegExp(`^"${key}"`, 'gm'), + val ? safeDump(val) : '' + ); }); return patchedTemplate; diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index f9a577e001c64e..3000c9321b3b9e 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -6,7 +6,7 @@ import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; -import { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; +import { EmbeddableSetup } from 'src/plugins/embeddable/public'; import { ExpressionsSetup, ExpressionsStart } from 'src/plugins/expressions/public'; import { VisualizationsSetup } from 'src/plugins/visualizations/public'; import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; @@ -38,7 +38,6 @@ export interface LensPluginSetupDependencies { export interface LensPluginStartDependencies { data: DataPublicPluginStart; - embeddable: EmbeddableStart; expressions: ExpressionsStart; navigation: NavigationPublicPluginStart; uiActions: UiActionsStart; diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts index 3fcb00d879583d..6c8c5e3f518082 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts @@ -3,7 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EndpointDocGenerator, Event, Tree, TreeNode } from './generate_data'; +import { + EndpointDocGenerator, + Event, + Tree, + TreeNode, + RelatedEventCategory, + ECSCategory, +} from './generate_data'; interface Node { events: Event[]; @@ -106,7 +113,11 @@ describe('data generator', () => { generations, percentTerminated: 100, percentWithRelated: 100, - relatedEvents: 4, + relatedEvents: [ + { category: RelatedEventCategory.Driver, count: 1 }, + { category: RelatedEventCategory.File, count: 2 }, + { category: RelatedEventCategory.Network, count: 1 }, + ], }); }); @@ -117,6 +128,36 @@ describe('data generator', () => { return (inRelated || inLifecycle) && event.process.entity_id === node.id; }; + it('has the right related events for each node', () => { + const checkRelatedEvents = (node: TreeNode) => { + expect(node.relatedEvents.length).toEqual(4); + + const counts: Record = {}; + for (const event of node.relatedEvents) { + if (Array.isArray(event.event.category)) { + for (const cat of event.event.category) { + counts[cat] = counts[cat] + 1 || 1; + } + } else { + counts[event.event.category] = counts[event.event.category] + 1 || 1; + } + } + expect(counts[ECSCategory.Driver]).toEqual(1); + expect(counts[ECSCategory.File]).toEqual(2); + expect(counts[ECSCategory.Network]).toEqual(1); + }; + + for (const node of tree.ancestry.values()) { + checkRelatedEvents(node); + } + + for (const node of tree.children.values()) { + checkRelatedEvents(node); + } + + checkRelatedEvents(tree.origin); + }); + it('has the right number of ancestors', () => { expect(tree.ancestry.size).toEqual(ancestors); }); diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 57d41b65549073..bf3c27beb7eab5 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -24,7 +24,7 @@ interface EventOptions { entityID?: string; parentEntityID?: string; eventType?: string; - eventCategory?: string; + eventCategory?: string | string[]; processName?: string; } @@ -75,21 +75,98 @@ const POLICIES: Array<{ name: string; id: string }> = [ const FILE_OPERATIONS: string[] = ['creation', 'open', 'rename', 'execution', 'deletion']; interface EventInfo { - category: string; + category: string | string[]; /** * This denotes the `event.type` field for when an event is created, this can be `start` or `creation` */ creationType: string; } +/** + * The valid ecs categories. + */ +export enum ECSCategory { + Driver = 'driver', + File = 'file', + Network = 'network', + /** + * Registry has not been added to ecs yet. + */ + Registry = 'registry', + Authentication = 'authentication', + Session = 'session', +} + +/** + * High level categories for related events. These specify the type of related events that should be generated. + */ +export enum RelatedEventCategory { + /** + * The Random category allows the related event categories to be chosen randomly + */ + Random = 'random', + Driver = 'driver', + File = 'file', + Network = 'network', + Registry = 'registry', + /** + * Security isn't an actual category but defines a type of related event to be created. + */ + Security = 'security', +} + +/** + * This map defines the relationship between a higher level event type defined by the RelatedEventCategory enums and + * the ECS categories that is should map to. This should only be used for tests that need to determine the exact + * ecs categories that were created based on the related event information passed to the generator. + */ +export const categoryMapping: Record = { + [RelatedEventCategory.Security]: [ECSCategory.Authentication, ECSCategory.Session], + [RelatedEventCategory.Driver]: ECSCategory.Driver, + [RelatedEventCategory.File]: ECSCategory.File, + [RelatedEventCategory.Network]: ECSCategory.Network, + [RelatedEventCategory.Registry]: ECSCategory.Registry, + /** + * Random is only used by the generator to indicate that it should randomly choose the event information when generating + * related events. It does not map to a specific ecs category. + */ + [RelatedEventCategory.Random]: '', +}; + +/** + * The related event category and number of events that should be generated. + */ +export interface RelatedEventInfo { + category: RelatedEventCategory; + count: number; +} + // These are from the v1 schemas and aren't all valid ECS event categories, still in flux -const OTHER_EVENT_CATEGORIES: EventInfo[] = [ - { category: 'driver', creationType: 'start' }, - { category: 'file', creationType: 'creation' }, - { category: 'library', creationType: 'start' }, - { category: 'network', creationType: 'start' }, - { category: 'registry', creationType: 'creation' }, -]; +const OTHER_EVENT_CATEGORIES: Record< + Exclude, + EventInfo +> = { + [RelatedEventCategory.Security]: { + category: categoryMapping[RelatedEventCategory.Security], + creationType: 'start', + }, + [RelatedEventCategory.Driver]: { + category: categoryMapping[RelatedEventCategory.Driver], + creationType: 'start', + }, + [RelatedEventCategory.File]: { + category: categoryMapping[RelatedEventCategory.File], + creationType: 'creation', + }, + [RelatedEventCategory.Network]: { + category: categoryMapping[RelatedEventCategory.Network], + creationType: 'start', + }, + [RelatedEventCategory.Registry]: { + category: categoryMapping[RelatedEventCategory.Registry], + creationType: 'creation', + }, +}; interface HostInfo { elastic: { @@ -164,7 +241,7 @@ export interface TreeOptions { ancestors?: number; generations?: number; children?: number; - relatedEvents?: number; + relatedEvents?: RelatedEventInfo[]; percentWithRelated?: number; percentTerminated?: number; alwaysGenMaxChildrenPerNode?: boolean; @@ -487,7 +564,8 @@ export class EndpointDocGenerator { * @param alertAncestors - number of ancestor generations to create relative to the alert * @param childGenerations - number of child generations to create relative to the alert * @param maxChildrenPerNode - maximum number of children for any given node in the tree - * @param relatedEventsPerNode - number of related events (file, registry, etc) to create for each process event in the tree + * @param relatedEventsPerNode - can be an array of RelatedEventInfo objects describing the related events that should be generated for each process node + * or a number which defines the number of related events and will default to random categories * @param percentNodesWithRelated - percent of nodes which should have related events * @param percentTerminated - percent of nodes which will have process termination events * @param alwaysGenMaxChildrenPerNode - flag to always return the max children per node instead of it being a random number of children @@ -496,7 +574,7 @@ export class EndpointDocGenerator { alertAncestors?: number, childGenerations?: number, maxChildrenPerNode?: number, - relatedEventsPerNode?: number, + relatedEventsPerNode?: RelatedEventInfo[] | number, percentNodesWithRelated?: number, percentTerminated?: number, alwaysGenMaxChildrenPerNode?: boolean @@ -525,13 +603,14 @@ export class EndpointDocGenerator { /** * Creates an alert event and associated process ancestry. The alert event will always be the last event in the return array. * @param alertAncestors - number of ancestor generations to create - * @param relatedEventsPerNode - number of related events to add to each process node being created + * @param relatedEventsPerNode - can be an array of RelatedEventInfo objects describing the related events that should be generated for each process node + * or a number which defines the number of related events and will default to random categories * @param pctWithRelated - percent of ancestors that will have related events * @param pctWithTerminated - percent of ancestors that will have termination events */ public createAlertEventAncestry( alertAncestors = 3, - relatedEventsPerNode = 5, + relatedEventsPerNode: RelatedEventInfo[] | number = 5, pctWithRelated = 30, pctWithTerminated = 100 ): Event[] { @@ -611,7 +690,8 @@ export class EndpointDocGenerator { * @param root - The process event to use as the root node of the tree * @param generations - number of child generations to create. The root node is not counted as a generation. * @param maxChildrenPerNode - maximum number of children for any given node in the tree - * @param relatedEventsPerNode - number of related events (file, registry, etc) to create for each process event in the tree + * @param relatedEventsPerNode - can be an array of RelatedEventInfo objects describing the related events that should be generated for each process node + * or a number which defines the number of related events and will default to random categories * @param percentNodesWithRelated - percent of nodes which should have related events * @param percentChildrenTerminated - percent of nodes which will have process termination events * @param alwaysGenMaxChildrenPerNode - flag to always return the max children per node instead of it being a random number of children @@ -620,7 +700,7 @@ export class EndpointDocGenerator { root: Event, generations = 2, maxChildrenPerNode = 2, - relatedEventsPerNode = 3, + relatedEventsPerNode: RelatedEventInfo[] | number = 3, percentNodesWithRelated = 100, percentChildrenTerminated = 100, alwaysGenMaxChildrenPerNode = false @@ -686,25 +766,40 @@ export class EndpointDocGenerator { /** * Creates related events for a process event * @param node - process event to relate events to by entityID - * @param numRelatedEvents - number of related events to generate + * @param relatedEvents - can be an array of RelatedEventInfo objects describing the related events that should be generated for each process node + * or a number which defines the number of related events and will default to random categories * @param processDuration - maximum number of seconds after process event that related event timestamp can be */ public *relatedEventsGenerator( node: Event, - numRelatedEvents = 10, + relatedEvents: RelatedEventInfo[] | number = 10, processDuration: number = 6 * 3600 ) { - for (let i = 0; i < numRelatedEvents; i++) { - const eventInfo = this.randomChoice(OTHER_EVENT_CATEGORIES); - - const ts = node['@timestamp'] + this.randomN(processDuration) * 1000; - yield this.generateEvent({ - timestamp: ts, - entityID: node.process.entity_id, - parentEntityID: node.process.parent?.entity_id, - eventCategory: eventInfo.category, - eventType: eventInfo.creationType, - }); + let relatedEventsInfo: RelatedEventInfo[]; + if (typeof relatedEvents === 'number') { + relatedEventsInfo = [{ category: RelatedEventCategory.Random, count: relatedEvents }]; + } else { + relatedEventsInfo = relatedEvents; + } + for (const event of relatedEventsInfo) { + let eventInfo: EventInfo; + + for (let i = 0; i < event.count; i++) { + if (event.category === RelatedEventCategory.Random) { + eventInfo = this.randomChoice(Object.values(OTHER_EVENT_CATEGORIES)); + } else { + eventInfo = OTHER_EVENT_CATEGORIES[event.category]; + } + + const ts = node['@timestamp'] + this.randomN(processDuration) * 1000; + yield this.generateEvent({ + timestamp: ts, + entityID: node.process.entity_id, + parentEntityID: node.process.parent?.entity_id, + eventCategory: eventInfo.category, + eventType: eventInfo.creationType, + }); + } } } diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index 45b5cf2526e12d..816f9b77115ec3 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -41,14 +41,30 @@ type ImmutableMap = ReadonlyMap, Immutable>; type ImmutableSet = ReadonlySet>; type ImmutableObject = { readonly [K in keyof T]: Immutable }; +export interface EventStats { + /** + * The total number of related events (all events except process and alerts) that exist for a node. + */ + total: number; + /** + * A mapping of ECS event.category to the number of related events are marked with that category + * For example: + * { + * network: 5, + * file: 2 + * } + */ + byCategory: Record; +} + /** * Statistical information for a node in a resolver tree. */ export interface ResolverNodeStats { /** - * The total number of related events (all events except process and alerts) that exist for a node. + * The stats for related events (excludes alerts and process events) for a particular node in the resolver tree. */ - totalEvents: number; + events: EventStats; /** * The total number of alerts that exist for a node. */ @@ -379,6 +395,7 @@ export interface LegacyEndpointEvent { event?: { action?: string; type?: string; + category?: string | string[]; }; } diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx index 890e13a17f42e8..5e0aa76e1d1399 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx @@ -9,10 +9,7 @@ import React, { useEffect, useState } from 'react'; import { createPortalNode, InPortal } from 'react-reverse-portal'; import styled, { css } from 'styled-components'; -import { - EmbeddablePanel, - ErrorEmbeddable, -} from '../../../../../../../src/plugins/embeddable/public'; +import { ErrorEmbeddable } from '../../../../../../../src/plugins/embeddable/public'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { getIndexPatternTitleIdMapping } from '../../../common/hooks/api/helpers'; import { useIndexPatterns } from '../../../common/hooks/use_index_patterns'; @@ -28,7 +25,6 @@ import { SetQuery } from './types'; import { MapEmbeddable } from '../../../../../../legacy/plugins/maps/public'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; -import { getSavedObjectFinder } from '../../../../../../../src/plugins/saved_objects/public'; interface EmbeddableMapProps { maintainRatio?: boolean; @@ -198,18 +194,7 @@ export const EmbeddedMapComponent = ({ {embeddable != null ? ( - + ) : !isLoading && isIndexError ? ( ) : ( diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts index a1bab707879a5b..359445f514b77e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts @@ -5,13 +5,23 @@ */ import { SearchResponse } from 'elasticsearch'; import { ResolverQuery } from './base'; -import { ResolverEvent } from '../../../../../common/endpoint/types'; +import { ResolverEvent, EventStats } from '../../../../../common/endpoint/types'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/public'; import { AggBucket } from '../utils/pagination'; export interface StatsResult { alerts: Record; - events: Record; + events: Record; +} + +interface CategoriesAgg extends AggBucket { + /** + * The reason categories is optional here is because if no data was returned in the query the categories aggregation + * will not be defined on the response (because it's a sub aggregation). + */ + categories?: { + buckets?: AggBucket[]; + }; } export class StatsQuery extends ResolverQuery { @@ -64,13 +74,25 @@ export class StatsQuery extends ResolverQuery { alerts: { filter: { term: { 'event.kind': 'alert' } }, aggs: { - ids: { terms: { field: 'endgame.data.alert_details.acting_process.unique_pid' } }, + ids: { + terms: { + field: 'endgame.data.alert_details.acting_process.unique_pid', + size: uniquePIDs.length, + }, + }, }, }, events: { filter: { term: { 'event.kind': 'event' } }, aggs: { - ids: { terms: { field: 'endgame.unique_pid' } }, + ids: { + terms: { field: 'endgame.unique_pid', size: uniquePIDs.length }, + aggs: { + categories: { + terms: { field: 'event.category', size: 1000 }, + }, + }, + }, }, }, }, @@ -112,34 +134,106 @@ export class StatsQuery extends ResolverQuery { alerts: { filter: { term: { 'event.kind': 'alert' } }, aggs: { - ids: { terms: { field: 'process.entity_id' } }, + ids: { terms: { field: 'process.entity_id', size: entityIDs.length } }, }, }, events: { filter: { term: { 'event.kind': 'event' } }, aggs: { - ids: { terms: { field: 'process.entity_id' } }, + ids: { + // The entityIDs array will be made up of alert and event entity_ids, so we're guaranteed that there + // won't be anymore unique process.entity_ids than the size of the array passed in + terms: { field: 'process.entity_id', size: entityIDs.length }, + aggs: { + categories: { + // Currently ECS defines a small number of valid categories (under 10 right now), as ECS grows it's possible that the + // valid categories could exceed this hardcoded limit. If that happens we might want to revisit this + // and transition it to a composite aggregation so that we can paginate through all the possible response + terms: { field: 'event.category', size: 1000 }, + }, + }, + }, }, }, }, }; } - public formatResponse(response: SearchResponse): StatsResult { - const alerts = response.aggregations.alerts.ids.buckets.reduce( - (cummulative: Record, bucket: AggBucket) => ({ - ...cummulative, - [bucket.key]: bucket.doc_count, - }), - {} - ); - const events = response.aggregations.events.ids.buckets.reduce( + private static getEventStats(catAgg: CategoriesAgg): EventStats { + const total = catAgg.doc_count; + if (!catAgg.categories?.buckets) { + return { + total, + byCategory: {}, + }; + } + + const byCategory: Record = catAgg.categories.buckets.reduce( (cummulative: Record, bucket: AggBucket) => ({ ...cummulative, [bucket.key]: bucket.doc_count, }), {} ); + return { + total, + byCategory, + }; + } + + public formatResponse(response: SearchResponse): StatsResult { + let alerts: Record = {}; + + if (response.aggregations?.alerts?.ids?.buckets) { + alerts = response.aggregations.alerts.ids.buckets.reduce( + (cummulative: Record, bucket: AggBucket) => ({ + ...cummulative, + [bucket.key]: bucket.doc_count, + }), + {} + ); + } + + /** + * The response for the events ids aggregation should look like this: + * "aggregations" : { + * "ids" : { + * "doc_count_error_upper_bound" : 0, + * "sum_other_doc_count" : 0, + * "buckets" : [ + * { + * "key" : "entity_id1", + * "doc_count" : 3, + * "categories" : { + * "doc_count_error_upper_bound" : 0, + * "sum_other_doc_count" : 0, + * "buckets" : [ + * { + * "key" : "session", + * "doc_count" : 3 + * }, + * { + * "key" : "authentication", + * "doc_count" : 2 + * } + * ] + * } + * }, + * + * Which would indicate that entity_id1 had 3 related events. 3 of the related events had category session, + * and 2 had authentication + */ + let events: Record = {}; + if (response.aggregations?.events?.ids?.buckets) { + events = response.aggregations.events.ids.buckets.reduce( + (cummulative: Record, bucket: CategoriesAgg) => ({ + ...cummulative, + [bucket.key]: StatsQuery.getEventStats(bucket), + }), + {} + ); + } + return { alerts, events, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts index 4b14c555d49b7c..4ac8e206d4f3ba 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts @@ -173,10 +173,13 @@ export class Fetcher { const statsQuery = new StatsQuery(this.indexPattern, this.endpointID); const ids = tree.ids(); const res = await statsQuery.search(this.client, ids); - const alerts = res?.alerts || {}; - const events = res?.events || {}; + const alerts = res.alerts; + const events = res.events; ids.forEach((id) => { - tree.addStats(id, { totalAlerts: alerts[id] || 0, totalEvents: events[id] || 0 }); + tree.addStats(id, { + totalAlerts: alerts[id] || 0, + events: events[id] || { total: 0, byCategory: {} }, + }); }); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts index ae078b5368a96b..2fe7e364bb4603 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts @@ -81,7 +81,10 @@ export function createTree(entityID: string): ResolverTree { }, stats: { totalAlerts: 0, - totalEvents: 0, + events: { + total: 0, + byCategory: {}, + }, }, }; } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c9eecc7d4779e8..88068145305739 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6579,7 +6579,6 @@ "xpack.idxMgmt.indexTemplatesList.emptyPrompt.noIndexTemplatesTitle": "まだテンプレートがありません", "xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesDescription": "テンプレートを読み込み中…", "xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesErrorMessage": "テンプレートの読み込み中にエラーが発生", - "xpack.idxMgmt.indexTemplatesTable.systemIndexTemplatesSwitchLabel": "システムテンプレートを含める", "xpack.idxMgmt.licenseCheckErrorMessage": "ライセンス確認失敗", "xpack.idxMgmt.mappingsEditor.addFieldButtonLabel": "フィールドの追加", "xpack.idxMgmt.mappingsEditor.addMultiFieldTooltipLabel": "同じフィールドを異なる方法でインデックスするために、マルチフィールドを追加します。", @@ -7077,27 +7076,13 @@ "xpack.idxMgmt.templateCreate.loadingTemplateToCloneDescription": "クローンを作成するテンプレートを読み込み中…", "xpack.idxMgmt.templateCreate.loadingTemplateToCloneErrorMessage": "クローンを作成するテンプレートを読み込み中にエラーが発生", "xpack.idxMgmt.templateDetails.aliasesTab.noAliasesTitle": "エイリアスが定義されていません。", - "xpack.idxMgmt.templateDetails.aliasesTabTitle": "エイリアス", - "xpack.idxMgmt.templateDetails.cloneButtonLabel": "クローンを作成", - "xpack.idxMgmt.templateDetails.closeButtonLabel": "閉じる", - "xpack.idxMgmt.templateDetails.deleteButtonLabel": "削除", - "xpack.idxMgmt.templateDetails.editButtonLabel": "編集", - "xpack.idxMgmt.templateDetails.loadingIndexTemplateDescription": "テンプレートを読み込み中…", - "xpack.idxMgmt.templateDetails.loadingIndexTemplateErrorMessage": "テンプレートの読み込み中にエラーが発生", - "xpack.idxMgmt.templateDetails.manageButtonLabel": "管理", - "xpack.idxMgmt.templateDetails.manageContextMenuPanelTitle": "テンプレートオプション", - "xpack.idxMgmt.templateDetails.managedTemplateInfoDescription": "マネージドテンプレートは内部オペレーションに不可欠です。", - "xpack.idxMgmt.templateDetails.managedTemplateInfoTitle": "マネジドテンプレートの編集は許可されていません。", "xpack.idxMgmt.templateDetails.mappingsTab.noMappingsTitle": "マッピングが定義されていません。", - "xpack.idxMgmt.templateDetails.mappingsTabTitle": "マッピング", "xpack.idxMgmt.templateDetails.settingsTab.noSettingsTitle": "設定が定義されていません。", - "xpack.idxMgmt.templateDetails.settingsTabTitle": "設定", "xpack.idxMgmt.templateDetails.summaryTab.ilmPolicyDescriptionListTitle": "ILM ポリシー", "xpack.idxMgmt.templateDetails.summaryTab.indexPatternsDescriptionListTitle": "インデックス{numIndexPatterns, plural, one {パターン} other {パターン}}", "xpack.idxMgmt.templateDetails.summaryTab.noneDescriptionText": "なし", "xpack.idxMgmt.templateDetails.summaryTab.orderDescriptionListTitle": "順序", "xpack.idxMgmt.templateDetails.summaryTab.versionDescriptionListTitle": "バージョン", - "xpack.idxMgmt.templateDetails.summaryTabTitle": "まとめ", "xpack.idxMgmt.templateEdit.loadingIndexTemplateDescription": "テンプレートを読み込み中…", "xpack.idxMgmt.templateEdit.loadingIndexTemplateErrorMessage": "テンプレートの読み込み中にエラーが発生", "xpack.idxMgmt.templateEdit.managedTemplateWarningDescription": "管理されているテンプレートは内部オペレーションに不可欠です。", @@ -7161,26 +7146,12 @@ "xpack.idxMgmt.templateForm.stepSettings.settingsDescription": "インデックスの動作を定義します。", "xpack.idxMgmt.templateForm.stepSettings.settingsEditorHelpText": "JSON フォーマットを使用: {code}", "xpack.idxMgmt.templateForm.stepSettings.stepTitle": "インデックス設定 (任意)", - "xpack.idxMgmt.templateList.table.actionCloneDescription": "このテンプレートのクローンを作成します", - "xpack.idxMgmt.templateList.table.actionCloneTitle": "クローンを作成", - "xpack.idxMgmt.templateList.table.actionColumnTitle": "アクション", - "xpack.idxMgmt.templateList.table.actionDeleteDecription": "このテンプレートを削除します", - "xpack.idxMgmt.templateList.table.actionDeleteText": "削除", - "xpack.idxMgmt.templateList.table.actionEditDecription": "このテンプレートを編集します", - "xpack.idxMgmt.templateList.table.actionEditText": "編集", - "xpack.idxMgmt.templateList.table.aliasesColumnTitle": "エイリアス", - "xpack.idxMgmt.templateList.table.createTemplatesButtonLabel": "テンプレートを作成", - "xpack.idxMgmt.templateList.table.deleteManagedTemplateTooltip": "管理されているテンプレートは削除できません。", - "xpack.idxMgmt.templateList.table.deleteTemplatesButtonLabel": "{count, plural, one {テンプレート} other {テンプレート} }を削除", "xpack.idxMgmt.templateList.table.ilmPolicyColumnDescription": "インデックスライフサイクルポリシー「{policyName}」", "xpack.idxMgmt.templateList.table.ilmPolicyColumnTitle": "ILM ポリシー", "xpack.idxMgmt.templateList.table.indexPatternsColumnTitle": "インデックスパターン", - "xpack.idxMgmt.templateList.table.mappingsColumnTitle": "マッピング", "xpack.idxMgmt.templateList.table.nameColumnTitle": "名前", "xpack.idxMgmt.templateList.table.noIndexTemplatesMessage": "インデックステンプレートが見つかりません", - "xpack.idxMgmt.templateList.table.orderColumnTitle": "順序", "xpack.idxMgmt.templateList.table.reloadTemplatesButtonLabel": "再読み込み", - "xpack.idxMgmt.templateList.table.settingsColumnTitle": "設定", "xpack.idxMgmt.templateValidation.indexPatternsRequiredError": "インデックスパターンが最低 1 つ必要です。", "xpack.idxMgmt.templateValidation.templateNameInvalidaCharacterError": "テンプレート名に「{invalidChar}」は使用できません", "xpack.idxMgmt.templateValidation.templateNameLowerCaseRequiredError": "テンプレート名は小文字でなければなりません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 86547a3729db4d..8c7fe2e0adbdeb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6582,7 +6582,6 @@ "xpack.idxMgmt.indexTemplatesList.emptyPrompt.noIndexTemplatesTitle": "您尚未有任何模板", "xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesDescription": "正在加载模板……", "xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesErrorMessage": "加载模板时出错", - "xpack.idxMgmt.indexTemplatesTable.systemIndexTemplatesSwitchLabel": "包括系统模板", "xpack.idxMgmt.licenseCheckErrorMessage": "许可证检查失败", "xpack.idxMgmt.mappingsEditor.addFieldButtonLabel": "添加字段", "xpack.idxMgmt.mappingsEditor.addMultiFieldTooltipLabel": "添加多字段以使用不同的方式索引相同的字段。", @@ -7081,27 +7080,13 @@ "xpack.idxMgmt.templateCreate.loadingTemplateToCloneDescription": "正在加载要克隆的模板……", "xpack.idxMgmt.templateCreate.loadingTemplateToCloneErrorMessage": "加载要克隆的模板时出错", "xpack.idxMgmt.templateDetails.aliasesTab.noAliasesTitle": "未定义任何别名。", - "xpack.idxMgmt.templateDetails.aliasesTabTitle": "别名", - "xpack.idxMgmt.templateDetails.cloneButtonLabel": "克隆", - "xpack.idxMgmt.templateDetails.closeButtonLabel": "关闭", - "xpack.idxMgmt.templateDetails.deleteButtonLabel": "删除", - "xpack.idxMgmt.templateDetails.editButtonLabel": "编辑", - "xpack.idxMgmt.templateDetails.loadingIndexTemplateDescription": "正在加载模板……", - "xpack.idxMgmt.templateDetails.loadingIndexTemplateErrorMessage": "加载模板时出错", - "xpack.idxMgmt.templateDetails.manageButtonLabel": "管理", - "xpack.idxMgmt.templateDetails.manageContextMenuPanelTitle": "模板选项", - "xpack.idxMgmt.templateDetails.managedTemplateInfoDescription": "托管模板对内部操作至关重要。", - "xpack.idxMgmt.templateDetails.managedTemplateInfoTitle": "不允许编辑托管模板", "xpack.idxMgmt.templateDetails.mappingsTab.noMappingsTitle": "未定义任何映射。", - "xpack.idxMgmt.templateDetails.mappingsTabTitle": "映射", "xpack.idxMgmt.templateDetails.settingsTab.noSettingsTitle": "未定义任何设置。", - "xpack.idxMgmt.templateDetails.settingsTabTitle": "设置", "xpack.idxMgmt.templateDetails.summaryTab.ilmPolicyDescriptionListTitle": "ILM 策略", "xpack.idxMgmt.templateDetails.summaryTab.indexPatternsDescriptionListTitle": "索引{numIndexPatterns, plural, one {模式} other {模式}}", "xpack.idxMgmt.templateDetails.summaryTab.noneDescriptionText": "无", "xpack.idxMgmt.templateDetails.summaryTab.orderDescriptionListTitle": "顺序", "xpack.idxMgmt.templateDetails.summaryTab.versionDescriptionListTitle": "版本", - "xpack.idxMgmt.templateDetails.summaryTabTitle": "总结", "xpack.idxMgmt.templateEdit.loadingIndexTemplateDescription": "正在加载模板……", "xpack.idxMgmt.templateEdit.loadingIndexTemplateErrorMessage": "加载模板时出错", "xpack.idxMgmt.templateEdit.managedTemplateWarningDescription": "托管模板对内部操作至关重要。", @@ -7165,26 +7150,12 @@ "xpack.idxMgmt.templateForm.stepSettings.settingsDescription": "定义索引的行为。", "xpack.idxMgmt.templateForm.stepSettings.settingsEditorHelpText": "使用 JSON 格式:{code}", "xpack.idxMgmt.templateForm.stepSettings.stepTitle": "索引设置(可选)", - "xpack.idxMgmt.templateList.table.actionCloneDescription": "克隆此模板", - "xpack.idxMgmt.templateList.table.actionCloneTitle": "克隆", - "xpack.idxMgmt.templateList.table.actionColumnTitle": "操作", - "xpack.idxMgmt.templateList.table.actionDeleteDecription": "删除此模板", - "xpack.idxMgmt.templateList.table.actionDeleteText": "删除", - "xpack.idxMgmt.templateList.table.actionEditDecription": "编辑此模板", - "xpack.idxMgmt.templateList.table.actionEditText": "编辑", - "xpack.idxMgmt.templateList.table.aliasesColumnTitle": "别名", - "xpack.idxMgmt.templateList.table.createTemplatesButtonLabel": "创建模板", - "xpack.idxMgmt.templateList.table.deleteManagedTemplateTooltip": "您无法删除托管模板。", - "xpack.idxMgmt.templateList.table.deleteTemplatesButtonLabel": "删除{count, plural, one {模板} other {模板} }", "xpack.idxMgmt.templateList.table.ilmPolicyColumnDescription": "“{policyName}”索引生命周期策略", "xpack.idxMgmt.templateList.table.ilmPolicyColumnTitle": "ILM 策略", "xpack.idxMgmt.templateList.table.indexPatternsColumnTitle": "索引模式", - "xpack.idxMgmt.templateList.table.mappingsColumnTitle": "映射", "xpack.idxMgmt.templateList.table.nameColumnTitle": "名称", "xpack.idxMgmt.templateList.table.noIndexTemplatesMessage": "未找到任何索引模板", - "xpack.idxMgmt.templateList.table.orderColumnTitle": "顺序", "xpack.idxMgmt.templateList.table.reloadTemplatesButtonLabel": "重新加载", - "xpack.idxMgmt.templateList.table.settingsColumnTitle": "设置", "xpack.idxMgmt.templateValidation.indexPatternsRequiredError": "至少需要一个索引模式。", "xpack.idxMgmt.templateValidation.templateNameInvalidaCharacterError": "模板名称不得包含字符“{invalidChar}”", "xpack.idxMgmt.templateValidation.templateNameLowerCaseRequiredError": "模板名称必须小写。", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 7ce952e9b3e0ad..7db6b5145f8950 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; +import React, { Fragment, lazy } from 'react'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { ReactWrapper } from 'enzyme'; @@ -18,6 +18,13 @@ jest.mock('../../lib/action_connector_api', () => ({ const actionTypeRegistry = actionTypeRegistryMock.create(); describe('action_form', () => { let deps: any; + + const mockedActionParamsFields = lazy(async () => ({ + default() { + return ; + }, + })); + const alertType = { id: 'my-alert-type', iconClass: 'test', @@ -41,7 +48,7 @@ describe('action_form', () => { return validationResult; }, actionConnectorFields: null, - actionParamsFields: null, + actionParamsFields: mockedActionParamsFields, }; const disabledByConfigActionType = { @@ -56,7 +63,7 @@ describe('action_form', () => { return validationResult; }, actionConnectorFields: null, - actionParamsFields: null, + actionParamsFields: mockedActionParamsFields, }; const disabledByLicenseActionType = { @@ -71,7 +78,7 @@ describe('action_form', () => { return validationResult; }, actionConnectorFields: null, - actionParamsFields: null, + actionParamsFields: mockedActionParamsFields, }; const preconfiguredOnly = { @@ -86,6 +93,21 @@ describe('action_form', () => { return validationResult; }, actionConnectorFields: null, + actionParamsFields: mockedActionParamsFields, + }; + + const actionTypeWithoutParams = { + id: 'my-action-type-without-params', + iconClass: 'test', + selectMessage: 'test', + validateConnector: (): ValidationResult => { + return { errors: {} }; + }, + validateParams: (): ValidationResult => { + const validationResult = { errors: {} }; + return validationResult; + }, + actionConnectorFields: null, actionParamsFields: null, }; @@ -153,6 +175,7 @@ describe('action_form', () => { disabledByConfigActionType, disabledByLicenseActionType, preconfiguredOnly, + actionTypeWithoutParams, ]); actionTypeRegistry.has.mockReturnValue(true); actionTypeRegistry.get.mockReturnValue(actionType); @@ -237,6 +260,14 @@ describe('action_form', () => { enabledInLicense: false, minimumLicenseRequired: 'gold', }, + { + id: actionTypeWithoutParams.id, + name: 'Action type without params', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, ]} toastNotifications={deps!.toastNotifications} docLinks={deps.docLinks} @@ -340,5 +371,13 @@ describe('action_form', () => { .exists() ).toBeTruthy(); }); + + it(`shouldn't render action types without params component`, async () => { + await setup(); + const actionOption = wrapper.find( + `[data-test-subj="${actionTypeWithoutParams.id}-ActionTypeSelectOption"]` + ); + expect(actionOption.exists()).toBeFalsy(); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 4c3a8d133922d3..201852ddeee488 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -547,6 +547,7 @@ export const ActionForm = ({ actionTypeNodes = actionTypeRegistry .list() .filter((item) => actionTypesIndex[item.id]) + .filter((item) => !!item.actionParamsFields) .sort((a, b) => actionTypeCompare(actionTypesIndex[a.id], actionTypesIndex[b.id], preconfiguredConnectors) ) diff --git a/x-pack/test/api_integration/apis/endpoint/resolver.ts b/x-pack/test/api_integration/apis/endpoint/resolver.ts index 31e8cc9a6426ce..43f42f700a4c85 100644 --- a/x-pack/test/api_integration/apis/endpoint/resolver.ts +++ b/x-pack/test/api_integration/apis/endpoint/resolver.ts @@ -14,6 +14,7 @@ import { ResolverChildren, ResolverTree, LegacyEndpointEvent, + ResolverNodeStats, } from '../../../../plugins/security_solution/common/endpoint/types'; import { parentEntityId } from '../../../../plugins/security_solution/common/endpoint/models/event'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -21,6 +22,9 @@ import { Event, Tree, TreeNode, + RelatedEventCategory, + RelatedEventInfo, + categoryMapping, } from '../../../../plugins/security_solution/common/endpoint/generate_data'; import { Options, GeneratedTrees } from '../../services/resolver'; @@ -141,16 +145,60 @@ const compareArrays = ( }); }; +/** + * Verifies that the stats received from ES for a node reflect the categories of events that the generator created. + * + * @param relatedEvents the related events received for a particular node + * @param categories the related event info used when generating the resolver tree + */ +const verifyStats = (stats: ResolverNodeStats | undefined, categories: RelatedEventInfo[]) => { + expect(stats).to.not.be(undefined); + let totalExpEvents = 0; + for (const cat of categories) { + const ecsCategories = categoryMapping[cat.category]; + if (Array.isArray(ecsCategories)) { + // if there are multiple ecs categories used to define a related event, the count for all of them should be the same + // and they should equal what is defined in the categories used to generate the related events + for (const ecsCat of ecsCategories) { + expect(stats?.events.byCategory[ecsCat]).to.be(cat.count); + } + } else { + expect(stats?.events.byCategory[ecsCategories]).to.be(cat.count); + } + + totalExpEvents += cat.count; + } + expect(stats?.events.total).to.be(totalExpEvents); +}; + +/** + * A helper function for verifying the stats information an array of nodes. + * + * @param nodes an array of lifecycle nodes that should have a stats field defined + * @param categories the related event info used when generating the resolver tree + */ +const verifyLifecycleStats = (nodes: LifecycleNode[], categories: RelatedEventInfo[]) => { + for (const node of nodes) { + verifyStats(node.stats, categories); + } +}; + export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const resolver = getService('resolverGenerator'); + const relatedEventsToGen = [ + { category: RelatedEventCategory.Driver, count: 2 }, + { category: RelatedEventCategory.File, count: 1 }, + { category: RelatedEventCategory.Registry, count: 1 }, + ]; + let resolverTrees: GeneratedTrees; let tree: Tree; const treeOptions: Options = { ancestors: 5, - relatedEvents: 4, + relatedEvents: relatedEventsToGen, children: 3, generations: 2, percentTerminated: 100, @@ -563,14 +611,17 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC expect(body.children.nextChild).to.equal(null); expect(body.children.childNodes.length).to.equal(12); verifyChildren(body.children.childNodes, tree, 4, 3); + verifyLifecycleStats(body.children.childNodes, relatedEventsToGen); expect(body.ancestry.nextAncestor).to.equal(null); verifyAncestry(body.ancestry.ancestors, tree, true); + verifyLifecycleStats(body.ancestry.ancestors, relatedEventsToGen); expect(body.relatedEvents.nextEvent).to.equal(null); compareArrays(tree.origin.relatedEvents, body.relatedEvents.events, true); compareArrays(tree.origin.lifecycle, body.lifecycle, true); + verifyStats(body.stats, relatedEventsToGen); }); }); }); diff --git a/x-pack/test/api_integration/apis/management/index_management/templates.helpers.js b/x-pack/test/api_integration/apis/management/index_management/templates.helpers.js index efe41ad47b019a..9f1fab40393972 100644 --- a/x-pack/test/api_integration/apis/management/index_management/templates.helpers.js +++ b/x-pack/test/api_integration/apis/management/index_management/templates.helpers.js @@ -7,12 +7,12 @@ import { API_BASE_PATH, INDEX_PATTERNS } from './constants'; export const registerHelpers = ({ supertest }) => { - const getAllTemplates = () => supertest.get(`${API_BASE_PATH}/templates`); + const getAllTemplates = () => supertest.get(`${API_BASE_PATH}/index-templates`); - const getOneTemplate = (name, formatVersion = 1) => - supertest.get(`${API_BASE_PATH}/templates/${name}?v=${formatVersion}`); + const getOneTemplate = (name, isLegacy = true) => + supertest.get(`${API_BASE_PATH}/index-templates/${name}?legacy=${isLegacy}`); - const getTemplatePayload = (name, formatVersion = 1) => ({ + const getTemplatePayload = (name, isLegacy = true) => ({ name, order: 1, indexPatterns: INDEX_PATTERNS, @@ -45,19 +45,22 @@ export const registerHelpers = ({ supertest }) => { }, }, _kbnMeta: { - formatVersion, + isLegacy, }, }); const createTemplate = (payload) => - supertest.put(`${API_BASE_PATH}/templates`).set('kbn-xsrf', 'xxx').send(payload); + supertest.post(`${API_BASE_PATH}/index-templates`).set('kbn-xsrf', 'xxx').send(payload); const deleteTemplates = (templates) => - supertest.post(`${API_BASE_PATH}/delete-templates`).set('kbn-xsrf', 'xxx').send({ templates }); + supertest + .post(`${API_BASE_PATH}/delete-index-templates`) + .set('kbn-xsrf', 'xxx') + .send({ templates }); const updateTemplate = (payload, templateName) => supertest - .put(`${API_BASE_PATH}/templates/${templateName}`) + .put(`${API_BASE_PATH}/index-templates/${templateName}`) .set('kbn-xsrf', 'xxx') .send(payload); diff --git a/x-pack/test/api_integration/apis/management/index_management/templates.js b/x-pack/test/api_integration/apis/management/index_management/templates.js index 625719443c56bc..cd7f5fb209eca9 100644 --- a/x-pack/test/api_integration/apis/management/index_management/templates.js +++ b/x-pack/test/api_integration/apis/management/index_management/templates.js @@ -35,10 +35,18 @@ export default function ({ getService }) { await createTemplate(payload).expect(200); }); + // TODO: When the "Create" API handler is added for V2 template, + // update this test to list composable templates. it('should list all the index templates with the expected parameters', async () => { - const { body: templates } = await getAllTemplates().expect(200); + const { body: allTemplates } = await getAllTemplates().expect(200); - const createdTemplate = templates.find((template) => template.name === payload.name); + // Composable templates + expect(allTemplates.templates).to.eql([]); + + // Legacy templates + const legacyTemplate = allTemplates.legacyTemplates.find( + (template) => template.name === payload.name + ); const expectedKeys = [ 'name', 'indexPatterns', @@ -46,13 +54,12 @@ export default function ({ getService }) { 'hasAliases', 'hasMappings', 'ilmPolicy', - 'isManaged', 'order', 'version', '_kbnMeta', ].sort(); - expect(Object.keys(createdTemplate).sort()).to.eql(expectedKeys); + expect(Object.keys(legacyTemplate).sort()).to.eql(expectedKeys); }); }); @@ -71,7 +78,6 @@ export default function ({ getService }) { 'indexPatterns', 'template', 'ilmPolicy', - 'isManaged', 'order', 'version', '_kbnMeta', @@ -155,7 +161,7 @@ export default function ({ getService }) { ).to.equal(templateName); const { body } = await deleteTemplates([ - { name: templateName, formatVersion: payload._kbnMeta.formatVersion }, + { name: templateName, isLegacy: payload._kbnMeta.isLegacy }, ]).expect(200); expect(body.errors).to.be.empty; diff --git a/x-pack/test_utils/stub_web_worker.ts b/x-pack/test_utils/stub_web_worker.ts index 2e7d5cf2098c8c..10d0ee6517384b 100644 --- a/x-pack/test_utils/stub_web_worker.ts +++ b/x-pack/test_utils/stub_web_worker.ts @@ -4,13 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -if (!window.Worker) { - // @ts-ignore we aren't honoring the real Worker spec here - window.Worker = function Worker() { - this.postMessage = jest.fn(); +function stubWebWorker() { + if (!window.Worker) { + // @ts-ignore we aren't honoring the real Worker spec here + window.Worker = function Worker() { + this.postMessage = jest.fn(); - // @ts-ignore TypeScript doesn't think this exists on the Worker interface - // https://developer.mozilla.org/en-US/docs/Web/API/Worker/terminate - this.terminate = jest.fn(); - }; + // @ts-ignore TypeScript doesn't think this exists on the Worker interface + // https://developer.mozilla.org/en-US/docs/Web/API/Worker/terminate + this.terminate = jest.fn(); + }; + } } + +stubWebWorker(); + +// Add an export to avoid TS complaining "stub_web_worker.ts" is not a module. +export { stubWebWorker };