diff --git a/.node-version b/.node-version index 5b7269c0a98f35..b61c07ffddbd16 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -10.19.0 +10.21.0 diff --git a/.nvmrc b/.nvmrc index 5b7269c0a98f35..b61c07ffddbd16 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -10.19.0 +10.21.0 diff --git a/.sass-lint.yml b/.sass-lint.yml index db895583eb8a7c..eb43af293c670e 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -4,7 +4,6 @@ files: - 'src/legacy/core_plugins/timelion/**/*.s+(a|c)ss' - 'src/plugins/vis_type_vislib/**/*.s+(a|c)ss' - 'src/plugins/vis_type_xy/**/*.s+(a|c)ss' - - 'x-pack/legacy/plugins/security/**/*.s+(a|c)ss' - 'x-pack/plugins/canvas/**/*.s+(a|c)ss' - 'x-pack/plugins/triggers_actions_ui/**/*.s+(a|c)ss' - 'x-pack/plugins/lens/**/*.s+(a|c)ss' @@ -12,6 +11,7 @@ files: - 'x-pack/legacy/plugins/maps/**/*.s+(a|c)ss' - 'x-pack/plugins/maps/**/*.s+(a|c)ss' - 'x-pack/plugins/spaces/**/*.s+(a|c)ss' + - 'x-pack/plugins/security/**/*.s+(a|c)ss' ignore: - 'x-pack/plugins/canvas/shareable_runtime/**/*.s+(a|c)ss' rules: diff --git a/Jenkinsfile b/Jenkinsfile index 11dca544f32261..b6a36c79f877dd 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -41,6 +41,7 @@ kibanaPipeline(timeoutMinutes: 155, checkPrChanges: true) { 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), + 'xpack-pageLoadMetrics': kibanaPipeline.functionalTestProcess('xpack-pageLoadMetrics', './test/scripts/jenkins_xpack_page_load_metrics.sh'), 'xpack-securitySolutionCypress': { processNumber -> whenChanged(['x-pack/plugins/security_solution/', 'x-pack/test/security_solution_cypress/']) { kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')(processNumber) diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 02cc34baf7c45d..75d3abefc74b90 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -114,6 +114,7 @@ | [SearchBar](./kibana-plugin-plugins-data-public.searchbar.md) | | | [SYNC\_SEARCH\_STRATEGY](./kibana-plugin-plugins-data-public.sync_search_strategy.md) | | | [syncQueryStateWithUrl](./kibana-plugin-plugins-data-public.syncquerystatewithurl.md) | Helper to setup syncing of global data with the URL | +| [UI\_SETTINGS](./kibana-plugin-plugins-data-public.ui_settings.md) | | ## Type Aliases diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md new file mode 100644 index 00000000000000..a48f4920b3d264 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md @@ -0,0 +1,39 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [UI\_SETTINGS](./kibana-plugin-plugins-data-public.ui_settings.md) + +## UI\_SETTINGS variable + +Signature: + +```typescript +UI_SETTINGS: { + META_FIELDS: string; + DOC_HIGHLIGHT: string; + QUERY_STRING_OPTIONS: string; + QUERY_ALLOW_LEADING_WILDCARDS: string; + SEARCH_QUERY_LANGUAGE: string; + SORT_OPTIONS: string; + COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string; + COURIER_SET_REQUEST_PREFERENCE: string; + COURIER_CUSTOM_REQUEST_PREFERENCE: string; + COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string; + COURIER_BATCH_SEARCHES: string; + SEARCH_INCLUDE_FROZEN: string; + HISTOGRAM_BAR_TARGET: string; + HISTOGRAM_MAX_BARS: string; + HISTORY_LIMIT: string; + SHORT_DOTS_ENABLE: string; + FORMAT_DEFAULT_TYPE_MAP: string; + FORMAT_NUMBER_DEFAULT_PATTERN: string; + FORMAT_PERCENT_DEFAULT_PATTERN: string; + FORMAT_BYTES_DEFAULT_PATTERN: string; + FORMAT_CURRENCY_DEFAULT_PATTERN: string; + FORMAT_NUMBER_DEFAULT_LOCALE: string; + TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string; + TIMEPICKER_QUICK_RANGES: string; + INDEXPATTERN_PLACEHOLDER: string; + FILTERS_PINNED_BY_DEFAULT: string; + FILTERS_EDITOR_SUGGEST_VALUES: string; +} +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 5f4bb8742c1d14..0efbe8ed4ed643 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -62,6 +62,7 @@ | [fieldFormats](./kibana-plugin-plugins-data-server.fieldformats.md) | | | [indexPatterns](./kibana-plugin-plugins-data-server.indexpatterns.md) | | | [search](./kibana-plugin-plugins-data-server.search.md) | | +| [UI\_SETTINGS](./kibana-plugin-plugins-data-server.ui_settings.md) | | ## Type Aliases diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md new file mode 100644 index 00000000000000..855cfd11d00ea8 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md @@ -0,0 +1,39 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [UI\_SETTINGS](./kibana-plugin-plugins-data-server.ui_settings.md) + +## UI\_SETTINGS variable + +Signature: + +```typescript +UI_SETTINGS: { + META_FIELDS: string; + DOC_HIGHLIGHT: string; + QUERY_STRING_OPTIONS: string; + QUERY_ALLOW_LEADING_WILDCARDS: string; + SEARCH_QUERY_LANGUAGE: string; + SORT_OPTIONS: string; + COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string; + COURIER_SET_REQUEST_PREFERENCE: string; + COURIER_CUSTOM_REQUEST_PREFERENCE: string; + COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string; + COURIER_BATCH_SEARCHES: string; + SEARCH_INCLUDE_FROZEN: string; + HISTOGRAM_BAR_TARGET: string; + HISTOGRAM_MAX_BARS: string; + HISTORY_LIMIT: string; + SHORT_DOTS_ENABLE: string; + FORMAT_DEFAULT_TYPE_MAP: string; + FORMAT_NUMBER_DEFAULT_PATTERN: string; + FORMAT_PERCENT_DEFAULT_PATTERN: string; + FORMAT_BYTES_DEFAULT_PATTERN: string; + FORMAT_CURRENCY_DEFAULT_PATTERN: string; + FORMAT_NUMBER_DEFAULT_LOCALE: string; + TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string; + TIMEPICKER_QUICK_RANGES: string; + INDEXPATTERN_PLACEHOLDER: string; + FILTERS_PINNED_BY_DEFAULT: string; + FILTERS_EDITOR_SUGGEST_VALUES: string; +} +``` diff --git a/package.json b/package.json index 1201a1773e6cd2..88ef07ba17cace 100644 --- a/package.json +++ b/package.json @@ -504,7 +504,7 @@ "zlib": "^1.0.5" }, "engines": { - "node": "10.19.0", + "node": "10.21.0", "yarn": "^1.21.1" } } 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/packages/kbn-test/package.json b/packages/kbn-test/package.json index 0ab0048619358b..8e2fd1c9182ff7 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -14,6 +14,7 @@ "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", "@types/parse-link-header": "^1.0.0", + "@types/puppeteer": "^3.0.0", "@types/strip-ansi": "^5.2.1", "@types/xml2js": "^0.4.5", "diff": "^4.0.1" @@ -25,6 +26,7 @@ "getopts": "^2.2.4", "glob": "^7.1.2", "parse-link-header": "^1.0.1", + "puppeteer": "^3.3.0", "strip-ansi": "^5.2.0", "rxjs": "^6.5.3", "tar-fs": "^1.16.3", diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index 585ce8181df5f4..0bc7cc664df68f 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -58,3 +58,5 @@ export { runFailedTestsReporterCli } from './failed_tests_reporter'; export { makeJunitReportPath } from './junit_report_path'; export { CI_PARALLEL_PROCESS_PREFIX } from './ci_parallel_process_prefix'; + +export * from './page_load_metrics'; diff --git a/packages/kbn-test/src/page_load_metrics/capture_page_load_metrics.ts b/packages/kbn-test/src/page_load_metrics/capture_page_load_metrics.ts new file mode 100644 index 00000000000000..013d49a29a51cf --- /dev/null +++ b/packages/kbn-test/src/page_load_metrics/capture_page_load_metrics.ts @@ -0,0 +1,81 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ToolingLog } from '@kbn/dev-utils'; +import { NavigationOptions, createUrl, navigateToApps } from './navigation'; + +export async function capturePageLoadMetrics(log: ToolingLog, options: NavigationOptions) { + const responsesByPageView = await navigateToApps(log, options); + + const assetSizeMeasurements = new Map(); + + const numberOfPagesVisited = responsesByPageView.size; + + for (const [, frameResponses] of responsesByPageView) { + for (const [, { url, dataLength }] of frameResponses) { + if (url.length === 0) { + throw new Error('navigateToApps(); failed to identify the url of the request'); + } + if (assetSizeMeasurements.has(url)) { + assetSizeMeasurements.set(url, [dataLength].concat(assetSizeMeasurements.get(url) || [])); + } else { + assetSizeMeasurements.set(url, [dataLength]); + } + } + } + + return Array.from(assetSizeMeasurements.entries()) + .map(([url, measurements]) => { + const baseUrl = createUrl('/', options.appConfig.url); + const relativeUrl = url + // remove the baseUrl (expect the trailing slash) to make url relative + .replace(baseUrl.slice(0, -1), '') + // strip the build number from asset urls + .replace(/^\/\d+\//, '/'); + return [relativeUrl, measurements] as const; + }) + .filter(([url, measurements]) => { + if (measurements.length !== numberOfPagesVisited) { + // ignore urls seen only on some pages + return false; + } + + if (url.startsWith('data:')) { + // ignore data urls since they are already counted by other assets + return false; + } + + if (url.startsWith('/api/') || url.startsWith('/internal/')) { + // ignore api requests since they don't have deterministic sizes + return false; + } + + const allMetricsAreEqual = measurements.every((x, i) => + i === 0 ? true : x === measurements[i - 1] + ); + if (!allMetricsAreEqual) { + throw new Error(`measurements for url [${url}] are not equal [${measurements.join(',')}]`); + } + + return true; + }) + .map(([url, measurements]) => { + return { group: 'page load asset size', id: url, value: measurements[0] }; + }); +} diff --git a/packages/kbn-test/src/page_load_metrics/cli.ts b/packages/kbn-test/src/page_load_metrics/cli.ts new file mode 100644 index 00000000000000..95421384c79cb0 --- /dev/null +++ b/packages/kbn-test/src/page_load_metrics/cli.ts @@ -0,0 +1,90 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Url from 'url'; + +import { run, createFlagError } from '@kbn/dev-utils'; +import { resolve, basename } from 'path'; +import { capturePageLoadMetrics } from './capture_page_load_metrics'; + +const defaultScreenshotsDir = resolve(__dirname, 'screenshots'); + +export function runPageLoadMetricsCli() { + run( + async ({ flags, log }) => { + const kibanaUrl = flags['kibana-url']; + if (!kibanaUrl || typeof kibanaUrl !== 'string') { + throw createFlagError('Expect --kibana-url to be a string'); + } + + const parsedUrl = Url.parse(kibanaUrl); + + const [username, password] = parsedUrl.auth + ? parsedUrl.auth.split(':') + : [flags.username, flags.password]; + + if (typeof username !== 'string' || typeof password !== 'string') { + throw createFlagError( + 'Mising username and/or password, either specify in --kibana-url or pass --username and --password' + ); + } + + const headless = !flags.head; + + const screenshotsDir = flags.screenshotsDir || defaultScreenshotsDir; + + if (typeof screenshotsDir !== 'string' || screenshotsDir === basename(screenshotsDir)) { + throw createFlagError('Expect screenshotsDir to be valid path string'); + } + + const metrics = await capturePageLoadMetrics(log, { + headless, + appConfig: { + url: kibanaUrl, + username, + password, + }, + screenshotsDir, + }); + for (const metric of metrics) { + log.info(`${metric.id}: ${metric.value}`); + } + }, + { + description: `Loads several pages with Puppeteer to capture the size of assets`, + flags: { + string: ['kibana-url', 'username', 'password', 'screenshotsDir'], + boolean: ['head'], + default: { + username: 'elastic', + password: 'changeme', + debug: true, + screenshotsDir: defaultScreenshotsDir, + }, + help: ` + --kibana-url Url for Kibana we should connect to, can include login info + --head Run puppeteer with graphical user interface + --username Set username, defaults to 'elastic' + --password Set password, defaults to 'changeme' + --screenshotsDir Set screenshots directory, defaults to '${defaultScreenshotsDir}' + `, + }, + } + ); +} diff --git a/packages/kbn-test/src/page_load_metrics/event.ts b/packages/kbn-test/src/page_load_metrics/event.ts new file mode 100644 index 00000000000000..481954bbf672e2 --- /dev/null +++ b/packages/kbn-test/src/page_load_metrics/event.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface ResponseReceivedEvent { + frameId: string; + loaderId: string; + requestId: string; + response: Record; + timestamp: number; + type: string; +} + +export interface DataReceivedEvent { + encodedDataLength: number; + dataLength: number; + requestId: string; + timestamp: number; +} diff --git a/packages/kbn-test/src/page_load_metrics/index.ts b/packages/kbn-test/src/page_load_metrics/index.ts new file mode 100644 index 00000000000000..4309d558518a60 --- /dev/null +++ b/packages/kbn-test/src/page_load_metrics/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './cli'; +export { capturePageLoadMetrics } from './capture_page_load_metrics'; diff --git a/packages/kbn-test/src/page_load_metrics/navigation.ts b/packages/kbn-test/src/page_load_metrics/navigation.ts new file mode 100644 index 00000000000000..21dc681951b212 --- /dev/null +++ b/packages/kbn-test/src/page_load_metrics/navigation.ts @@ -0,0 +1,165 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Fs from 'fs'; +import Url from 'url'; +import _ from 'lodash'; +import puppeteer from 'puppeteer'; +import { resolve } from 'path'; +import { ToolingLog } from '@kbn/dev-utils'; +import { ResponseReceivedEvent, DataReceivedEvent } from './event'; + +export interface NavigationOptions { + headless: boolean; + appConfig: { url: string; username: string; password: string }; + screenshotsDir: string; +} + +export type NavigationResults = Map>; + +interface FrameResponse { + url: string; + dataLength: number; +} + +function joinPath(pathA: string, pathB: string) { + return `${pathA.endsWith('/') ? pathA.slice(0, -1) : pathA}/${ + pathB.startsWith('/') ? pathB.slice(1) : pathB + }`; +} + +export function createUrl(path: string, url: string) { + const baseUrl = Url.parse(url); + return Url.format({ + protocol: baseUrl.protocol, + hostname: baseUrl.hostname, + port: baseUrl.port, + pathname: joinPath(baseUrl.pathname || '', path), + }); +} + +async function loginToKibana( + log: ToolingLog, + browser: puppeteer.Browser, + options: NavigationOptions +) { + log.debug(`log in to the app..`); + const page = await browser.newPage(); + const loginUrl = createUrl('/login', options.appConfig.url); + await page.goto(loginUrl, { + waitUntil: 'networkidle0', + }); + await page.type('[data-test-subj="loginUsername"]', options.appConfig.username); + await page.type('[data-test-subj="loginPassword"]', options.appConfig.password); + await page.click('[data-test-subj="loginSubmit"]'); + await page.waitForNavigation({ waitUntil: 'networkidle0' }); + await page.close(); +} + +export async function navigateToApps(log: ToolingLog, options: NavigationOptions) { + const browser = await puppeteer.launch({ headless: options.headless, args: ['--no-sandbox'] }); + const devToolsResponses: NavigationResults = new Map(); + const apps = [ + { path: '/app/discover', locator: '[data-test-subj="discover-sidebar"]' }, + { path: '/app/home', locator: '[data-test-subj="homeApp"]' }, + { path: '/app/canvas', locator: '[data-test-subj="create-workpad-button"]' }, + { path: '/app/maps', locator: '[title="Maps"]' }, + { path: '/app/apm', locator: '[data-test-subj="apmMainContainer"]' }, + ]; + + await loginToKibana(log, browser, options); + + await Promise.all( + apps.map(async (app) => { + const page = await browser.newPage(); + page.setCacheEnabled(false); + page.setDefaultNavigationTimeout(0); + const frameResponses = new Map(); + devToolsResponses.set(app.path, frameResponses); + + const client = await page.target().createCDPSession(); + await client.send('Network.enable'); + + function getRequestData(requestId: string) { + if (!frameResponses.has(requestId)) { + frameResponses.set(requestId, { url: '', dataLength: 0 }); + } + + return frameResponses.get(requestId)!; + } + + client.on('Network.responseReceived', (event: ResponseReceivedEvent) => { + getRequestData(event.requestId).url = event.response.url; + }); + + client.on('Network.dataReceived', (event: DataReceivedEvent) => { + getRequestData(event.requestId).dataLength += event.dataLength; + }); + + const url = createUrl(app.path, options.appConfig.url); + log.debug(`goto ${url}`); + await page.goto(url, { + waitUntil: 'networkidle0', + }); + + let readyAttempt = 0; + let selectorFound = false; + while (!selectorFound) { + readyAttempt += 1; + try { + await page.waitForSelector(app.locator, { timeout: 5000 }); + selectorFound = true; + } catch (error) { + log.error( + `Page '${app.path}' was not loaded properly, unable to find '${ + app.locator + }', url: ${page.url()}` + ); + + if (readyAttempt < 6) { + continue; + } + + const failureDir = resolve(options.screenshotsDir, 'failure'); + const screenshotPath = resolve( + failureDir, + `${app.path.slice(1).split('/').join('_')}_navigation.png` + ); + Fs.mkdirSync(failureDir, { recursive: true }); + + await page.bringToFront(); + await page.screenshot({ + path: screenshotPath, + type: 'png', + fullPage: true, + }); + log.debug(`Saving screenshot to ${screenshotPath}`); + + throw new Error(`Page load timeout: ${app.path} not loaded after 30 seconds`); + } + } + + await page.close(); + }) + ); + + await browser.close(); + + return devToolsResponses; +} diff --git a/scripts/page_load_metrics.js b/scripts/page_load_metrics.js new file mode 100644 index 00000000000000..37500c26e0b20b --- /dev/null +++ b/scripts/page_load_metrics.js @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('../src/setup_node_env'); +require('@kbn/test').runPageLoadMetricsCli(); diff --git a/src/dev/build/build_distributables.js b/src/dev/build/build_distributables.js index 3a8709893565df..66f0c0355c2d92 100644 --- a/src/dev/build/build_distributables.js +++ b/src/dev/build/build_distributables.js @@ -30,6 +30,7 @@ import { CleanTypescriptTask, CleanNodeBuildsTask, CleanTask, + CopyBinScriptsTask, CopySourceTask, CreateArchivesSourcesTask, CreateArchivesTask, @@ -110,6 +111,7 @@ export async function buildDistributables(options) { * run platform-generic build tasks */ await run(CopySourceTask); + await run(CopyBinScriptsTask); await run(CreateEmptyDirsAndFilesTask); await run(CreateReadmeTask); await run(TranspileBabelTask); diff --git a/src/dev/build/tasks/bin/copy_bin_scripts_task.js b/src/dev/build/tasks/bin/copy_bin_scripts_task.js new file mode 100644 index 00000000000000..f620f12b17d882 --- /dev/null +++ b/src/dev/build/tasks/bin/copy_bin_scripts_task.js @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { copyAll } from '../../lib'; + +export const CopyBinScriptsTask = { + description: 'Copying bin scripts into platform-generic build directory', + + async run(config, log, build) { + await copyAll( + config.resolveFromRepo('src/dev/build/tasks/bin/scripts'), + build.resolvePath('bin') + ); + }, +}; diff --git a/src/dev/build/tasks/bin/index.js b/src/dev/build/tasks/bin/index.js new file mode 100644 index 00000000000000..e970ac5ec044ba --- /dev/null +++ b/src/dev/build/tasks/bin/index.js @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { CopyBinScriptsTask } from './copy_bin_scripts_task'; diff --git a/bin/kibana b/src/dev/build/tasks/bin/scripts/kibana similarity index 100% rename from bin/kibana rename to src/dev/build/tasks/bin/scripts/kibana diff --git a/bin/kibana-keystore b/src/dev/build/tasks/bin/scripts/kibana-keystore similarity index 100% rename from bin/kibana-keystore rename to src/dev/build/tasks/bin/scripts/kibana-keystore diff --git a/bin/kibana-keystore.bat b/src/dev/build/tasks/bin/scripts/kibana-keystore.bat similarity index 100% rename from bin/kibana-keystore.bat rename to src/dev/build/tasks/bin/scripts/kibana-keystore.bat diff --git a/bin/kibana-plugin b/src/dev/build/tasks/bin/scripts/kibana-plugin similarity index 100% rename from bin/kibana-plugin rename to src/dev/build/tasks/bin/scripts/kibana-plugin diff --git a/bin/kibana-plugin.bat b/src/dev/build/tasks/bin/scripts/kibana-plugin.bat similarity index 100% rename from bin/kibana-plugin.bat rename to src/dev/build/tasks/bin/scripts/kibana-plugin.bat diff --git a/bin/kibana.bat b/src/dev/build/tasks/bin/scripts/kibana.bat similarity index 100% rename from bin/kibana.bat rename to src/dev/build/tasks/bin/scripts/kibana.bat diff --git a/src/dev/build/tasks/copy_source_task.js b/src/dev/build/tasks/copy_source_task.js index ee9dc159de47fa..ddc6d000bca194 100644 --- a/src/dev/build/tasks/copy_source_task.js +++ b/src/dev/build/tasks/copy_source_task.js @@ -42,7 +42,6 @@ export const CopySourceTask = { '!src/es_archiver/**', '!src/functional_test_runner/**', '!src/dev/**', - 'bin/**', 'typings/**', 'webpackShims/**', 'config/kibana.yml', diff --git a/src/dev/build/tasks/index.js b/src/dev/build/tasks/index.js index 8105fa8a7d5d47..bafb5a2fe115e4 100644 --- a/src/dev/build/tasks/index.js +++ b/src/dev/build/tasks/index.js @@ -17,6 +17,7 @@ * under the License. */ +export * from './bin'; export * from './build_packages_task'; export * from './clean_tasks'; export * from './copy_source_task'; diff --git a/src/dev/code_coverage/nyc_config/nyc.functional.config.js b/src/dev/code_coverage/nyc_config/nyc.functional.config.js new file mode 100644 index 00000000000000..20d266ab9e2c3f --- /dev/null +++ b/src/dev/code_coverage/nyc_config/nyc.functional.config.js @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const defaultExclude = require('@istanbuljs/schema/default-exclude'); +const extraExclude = ['data/optimize/**', 'src/core/server/**', '**/test/**']; +const path = require('path'); + +module.exports = { + 'temp-dir': process.env.COVERAGE_TEMP_DIR + ? path.resolve(process.env.COVERAGE_TEMP_DIR, 'functional') + : 'target/kibana-coverage/functional', + 'report-dir': 'target/kibana-coverage/functional-combined', + reporter: ['html', 'json-summary'], + exclude: extraExclude.concat(defaultExclude), +}; diff --git a/src/dev/code_coverage/nyc_config/nyc.jest.config.js b/src/dev/code_coverage/nyc_config/nyc.jest.config.js new file mode 100644 index 00000000000000..1f73347837ab30 --- /dev/null +++ b/src/dev/code_coverage/nyc_config/nyc.jest.config.js @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const path = require('path'); + +module.exports = { + 'temp-dir': process.env.COVERAGE_TEMP_DIR + ? path.resolve(process.env.COVERAGE_TEMP_DIR, 'jest') + : 'target/kibana-coverage/jest', + 'report-dir': 'target/kibana-coverage/jest-combined', + reporter: ['html', 'json-summary'], +}; diff --git a/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh b/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh index ff9cb36c894f82..707c6de3f88a08 100644 --- a/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh +++ b/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh @@ -1,10 +1,9 @@ #!/bin/bash -EXTRACT_START_DIR=tmp/extracted_coverage -EXTRACT_END_DIR=target/kibana-coverage -COMBINED_EXTRACT_DIR=/${EXTRACT_START_DIR}/${EXTRACT_END_DIR} +COVERAGE_TEMP_DIR=/tmp/extracted_coverage/target/kibana-coverage/ +export COVERAGE_TEMP_DIR echo "### Merge coverage reports" for x in jest functional; do - yarn nyc report --temp-dir $COMBINED_EXTRACT_DIR/${x} --report-dir $EXTRACT_END_DIR/${x}-combined --reporter=html --reporter=json-summary + yarn nyc report --nycrc-path src/dev/code_coverage/nyc_config/nyc.${x}.config.js done diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 4ed8f8e7db1903..2f785896da8d55 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -18,7 +18,6 @@ */ export const storybookAliases = { - advanced_ui_actions: 'x-pack/plugins/advanced_ui_actions/scripts/storybook.js', apm: 'x-pack/plugins/apm/scripts/storybook.js', canvas: 'x-pack/plugins/canvas/scripts/storybook_new.js', codeeditor: 'src/plugins/kibana_react/public/code_editor/scripts/storybook.ts', @@ -27,4 +26,5 @@ export const storybookAliases = { embeddable: 'src/plugins/embeddable/scripts/storybook.js', infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js', security_solution: 'x-pack/plugins/security_solution/scripts/storybook.js', + ui_actions_enhanced: 'x-pack/plugins/ui_actions_enhanced/scripts/storybook.js', }; diff --git a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js index 0d1b69778263c3..b7af6a73e1bc18 100644 --- a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js @@ -18,53 +18,32 @@ */ import moment from 'moment-timezone'; -import numeralLanguages from '@elastic/numeral/languages'; import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import { isRelativeUrl } from '../../../../core/server'; -import { DEFAULT_QUERY_LANGUAGE } from '../../../../plugins/data/common'; export function getUiSettingDefaults() { const weekdays = moment.weekdays().slice(); const [defaultWeekday] = weekdays; - // We add the `en` key manually here, since that's not a real numeral locale, but the - // default fallback in case the locale is not found. - const numeralLanguageIds = [ - 'en', - ...numeralLanguages.map(function (numeralLanguage) { - return numeralLanguage.id; - }), - ]; - - const luceneQueryLanguageLabel = i18n.translate( - 'kbn.advancedSettings.searchQueryLanguageLucene', - { - defaultMessage: 'Lucene', - } - ); - - const queryLanguageSettingName = i18n.translate('kbn.advancedSettings.searchQueryLanguageTitle', { - defaultMessage: 'Query language', - }); - - const requestPreferenceOptionLabels = { - sessionId: i18n.translate('kbn.advancedSettings.courier.requestPreferenceSessionId', { - defaultMessage: 'Session ID', - }), - custom: i18n.translate('kbn.advancedSettings.courier.requestPreferenceCustom', { - defaultMessage: 'Custom', - }), - none: i18n.translate('kbn.advancedSettings.courier.requestPreferenceNone', { - defaultMessage: 'None', - }), - }; // wrapped in provider so that a new instance is given to each app/test return { buildNum: { readonly: true, }, + 'state:storeInSessionStorage': { + name: i18n.translate('kbn.advancedSettings.storeUrlTitle', { + defaultMessage: 'Store URLs in session storage', + }), + value: false, + description: i18n.translate('kbn.advancedSettings.storeUrlText', { + defaultMessage: + 'The URL can sometimes grow to be too large for some browsers to handle. ' + + 'To counter-act this we are testing if storing parts of the URL in session storage could help. ' + + 'Please let us know how it goes!', + }), + }, defaultRoute: { name: i18n.translate('kbn.advancedSettings.defaultRoute.defaultRouteTitle', { defaultMessage: 'Default route', @@ -89,83 +68,6 @@ export function getUiSettingDefaults() { 'The route must be a relative URL.', }), }, - 'query:queryString:options': { - name: i18n.translate('kbn.advancedSettings.query.queryStringOptionsTitle', { - defaultMessage: 'Query string options', - }), - value: '{ "analyze_wildcard": true }', - description: i18n.translate('kbn.advancedSettings.query.queryStringOptionsText', { - defaultMessage: - '{optionsLink} for the lucene query string parser. Is only used when "{queryLanguage}" is set ' + - 'to {luceneLanguage}.', - description: - 'Part of composite text: kbn.advancedSettings.query.queryStringOptions.optionsLinkText + ' + - 'kbn.advancedSettings.query.queryStringOptionsText', - values: { - optionsLink: - '' + - i18n.translate('kbn.advancedSettings.query.queryStringOptions.optionsLinkText', { - defaultMessage: 'Options', - }) + - '', - luceneLanguage: luceneQueryLanguageLabel, - queryLanguage: queryLanguageSettingName, - }, - }), - type: 'json', - }, - 'query:allowLeadingWildcards': { - name: i18n.translate('kbn.advancedSettings.query.allowWildcardsTitle', { - defaultMessage: 'Allow leading wildcards in query', - }), - value: true, - description: i18n.translate('kbn.advancedSettings.query.allowWildcardsText', { - defaultMessage: - 'When set, * is allowed as the first character in a query clause. ' + - 'Currently only applies when experimental query features are enabled in the query bar. ' + - 'To disallow leading wildcards in basic lucene queries, use {queryStringOptionsPattern}.', - values: { - queryStringOptionsPattern: 'query:queryString:options', - }, - }), - }, - 'search:queryLanguage': { - name: queryLanguageSettingName, - value: DEFAULT_QUERY_LANGUAGE, - description: i18n.translate('kbn.advancedSettings.searchQueryLanguageText', { - defaultMessage: - 'Query language used by the query bar. KQL is a new language built specifically for Kibana.', - }), - type: 'select', - options: ['lucene', 'kuery'], - optionLabels: { - lucene: luceneQueryLanguageLabel, - kuery: i18n.translate('kbn.advancedSettings.searchQueryLanguageKql', { - defaultMessage: 'KQL', - }), - }, - }, - 'sort:options': { - name: i18n.translate('kbn.advancedSettings.sortOptionsTitle', { - defaultMessage: 'Sort options', - }), - value: '{ "unmapped_type": "boolean" }', - description: i18n.translate('kbn.advancedSettings.sortOptionsText', { - defaultMessage: '{optionsLink} for the Elasticsearch sort parameter', - description: - 'Part of composite text: kbn.advancedSettings.sortOptions.optionsLinkText + ' + - 'kbn.advancedSettings.sortOptionsText', - values: { - optionsLink: - '' + - i18n.translate('kbn.advancedSettings.sortOptions.optionsLinkText', { - defaultMessage: 'Options', - }) + - '', - }, - }), - type: 'json', - }, dateFormat: { name: i18n.translate('kbn.advancedSettings.dateFormatTitle', { defaultMessage: 'Date format', @@ -261,160 +163,6 @@ export function getUiSettingDefaults() { }, }), }, - defaultIndex: { - name: i18n.translate('kbn.advancedSettings.defaultIndexTitle', { - defaultMessage: 'Default index', - }), - value: null, - type: 'string', - description: i18n.translate('kbn.advancedSettings.defaultIndexText', { - defaultMessage: 'The index to access if no index is set', - }), - }, - 'courier:ignoreFilterIfFieldNotInIndex': { - name: i18n.translate('kbn.advancedSettings.courier.ignoreFilterTitle', { - defaultMessage: 'Ignore filter(s)', - }), - value: false, - description: i18n.translate('kbn.advancedSettings.courier.ignoreFilterText', { - defaultMessage: - 'This configuration enhances support for dashboards containing visualizations accessing dissimilar indexes. ' + - 'When disabled, all filters are applied to all visualizations. ' + - 'When enabled, filter(s) will be ignored for a visualization ' + - `when the visualization's index does not contain the filtering field.`, - }), - category: ['search'], - }, - 'courier:setRequestPreference': { - name: i18n.translate('kbn.advancedSettings.courier.requestPreferenceTitle', { - defaultMessage: 'Request preference', - }), - value: 'sessionId', - options: ['sessionId', 'custom', 'none'], - optionLabels: requestPreferenceOptionLabels, - type: 'select', - description: i18n.translate('kbn.advancedSettings.courier.requestPreferenceText', { - defaultMessage: `Allows you to set which shards handle your search requests. -

    -
  • {sessionId}: restricts operations to execute all search requests on the same shards. - This has the benefit of reusing shard caches across requests.
  • -
  • {custom}: allows you to define a your own preference. - Use courier:customRequestPreference to customize your preference value.
  • -
  • {none}: means do not set a preference. - This might provide better performance because requests can be spread across all shard copies. - However, results might be inconsistent because different shards might be in different refresh states.
  • -
`, - values: { - sessionId: requestPreferenceOptionLabels.sessionId, - custom: requestPreferenceOptionLabels.custom, - none: requestPreferenceOptionLabels.none, - }, - }), - category: ['search'], - }, - 'courier:customRequestPreference': { - name: i18n.translate('kbn.advancedSettings.courier.customRequestPreferenceTitle', { - defaultMessage: 'Custom request preference', - }), - value: '_local', - type: 'string', - description: i18n.translate('kbn.advancedSettings.courier.customRequestPreferenceText', { - defaultMessage: - '{requestPreferenceLink} used when {setRequestReferenceSetting} is set to {customSettingValue}.', - description: - 'Part of composite text: kbn.advancedSettings.courier.customRequestPreference.requestPreferenceLinkText + ' + - 'kbn.advancedSettings.courier.customRequestPreferenceText', - values: { - setRequestReferenceSetting: 'courier:setRequestPreference', - customSettingValue: '"custom"', - requestPreferenceLink: - '' + - i18n.translate( - 'kbn.advancedSettings.courier.customRequestPreference.requestPreferenceLinkText', - { - defaultMessage: 'Request Preference', - } - ) + - '', - }, - }), - category: ['search'], - }, - 'courier:maxConcurrentShardRequests': { - name: i18n.translate('kbn.advancedSettings.courier.maxRequestsTitle', { - defaultMessage: 'Max Concurrent Shard Requests', - }), - value: 0, - type: 'number', - description: i18n.translate('kbn.advancedSettings.courier.maxRequestsText', { - defaultMessage: - 'Controls the {maxRequestsLink} setting used for _msearch requests sent by Kibana. ' + - 'Set to 0 to disable this config and use the Elasticsearch default.', - values: { - maxRequestsLink: `max_concurrent_shard_requests`, - }, - }), - category: ['search'], - }, - 'courier:batchSearches': { - name: i18n.translate('kbn.advancedSettings.courier.batchSearchesTitle', { - defaultMessage: 'Batch concurrent searches', - }), - value: false, - type: 'boolean', - description: i18n.translate('kbn.advancedSettings.courier.batchSearchesText', { - defaultMessage: `When disabled, dashboard panels will load individually, and search requests will terminate when users navigate - away or update the query. When enabled, dashboard panels will load together when all of the data is loaded, and - searches will not terminate.`, - }), - deprecation: { - message: i18n.translate('kbn.advancedSettings.courier.batchSearchesTextDeprecation', { - defaultMessage: 'This setting is deprecated and will be removed in Kibana 8.0.', - }), - docLinksKey: 'kibanaSearchSettings', - }, - category: ['search'], - }, - 'search:includeFrozen': { - name: 'Search in frozen indices', - description: `Will include frozen indices in results if enabled. Searching through frozen indices - might increase the search time.`, - value: false, - category: ['search'], - }, - 'histogram:barTarget': { - name: i18n.translate('kbn.advancedSettings.histogram.barTargetTitle', { - defaultMessage: 'Target bars', - }), - value: 50, - description: i18n.translate('kbn.advancedSettings.histogram.barTargetText', { - defaultMessage: - 'Attempt to generate around this many bars when using "auto" interval in date histograms', - }), - }, - 'histogram:maxBars': { - name: i18n.translate('kbn.advancedSettings.histogram.maxBarsTitle', { - defaultMessage: 'Maximum bars', - }), - value: 100, - description: i18n.translate('kbn.advancedSettings.histogram.maxBarsText', { - defaultMessage: - 'Never show more than this many bars in date histograms, scale values if needed', - }), - }, - 'visualize:enableLabs': { - name: i18n.translate('kbn.advancedSettings.visualizeEnableLabsTitle', { - defaultMessage: 'Enable experimental visualizations', - }), - value: true, - description: i18n.translate('kbn.advancedSettings.visualizeEnableLabsText', { - defaultMessage: `Allows users to create, view, and edit experimental visualizations. If disabled, - only visualizations that are considered production-ready are available to the user.`, - }), - category: ['visualization'], - }, 'visualization:tileMap:maxPrecision': { name: i18n.translate('kbn.advancedSettings.visualization.tileMap.maxPrecisionTitle', { defaultMessage: 'Maximum tile map precision', @@ -493,43 +241,6 @@ export function getUiSettingDefaults() { }), category: ['visualization'], }, - 'csv:separator': { - name: i18n.translate('kbn.advancedSettings.csv.separatorTitle', { - defaultMessage: 'CSV separator', - }), - value: ',', - description: i18n.translate('kbn.advancedSettings.csv.separatorText', { - defaultMessage: 'Separate exported values with this string', - }), - }, - 'csv:quoteValues': { - name: i18n.translate('kbn.advancedSettings.csv.quoteValuesTitle', { - defaultMessage: 'Quote CSV values', - }), - value: true, - description: i18n.translate('kbn.advancedSettings.csv.quoteValuesText', { - defaultMessage: 'Should values be quoted in csv exports?', - }), - }, - 'history:limit': { - name: i18n.translate('kbn.advancedSettings.historyLimitTitle', { - defaultMessage: 'History limit', - }), - value: 10, - description: i18n.translate('kbn.advancedSettings.historyLimitText', { - defaultMessage: - 'In fields that have history (e.g. query inputs), show this many recent values', - }), - }, - 'shortDots:enable': { - name: i18n.translate('kbn.advancedSettings.shortenFieldsTitle', { - defaultMessage: 'Shorten fields', - }), - value: false, - description: i18n.translate('kbn.advancedSettings.shortenFieldsText', { - defaultMessage: 'Shorten long fields, for example, instead of foo.bar.baz, show f.b.baz', - }), - }, 'truncate:maxHeight': { name: i18n.translate('kbn.advancedSettings.maxCellHeightTitle', { defaultMessage: 'Maximum table cell height', @@ -540,138 +251,6 @@ export function getUiSettingDefaults() { 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation', }), }, - 'format:defaultTypeMap': { - name: i18n.translate('kbn.advancedSettings.format.defaultTypeMapTitle', { - defaultMessage: 'Field type format name', - }), - value: `{ - "ip": { "id": "ip", "params": {} }, - "date": { "id": "date", "params": {} }, - "date_nanos": { "id": "date_nanos", "params": {}, "es": true }, - "number": { "id": "number", "params": {} }, - "boolean": { "id": "boolean", "params": {} }, - "_source": { "id": "_source", "params": {} }, - "_default_": { "id": "string", "params": {} } -}`, - type: 'json', - description: i18n.translate('kbn.advancedSettings.format.defaultTypeMapText', { - defaultMessage: - 'Map of the format name to use by default for each field type. ' + - '{defaultFormat} is used if the field type is not mentioned explicitly', - values: { - defaultFormat: '"_default_"', - }, - }), - }, - 'format:number:defaultPattern': { - name: i18n.translate('kbn.advancedSettings.format.numberFormatTitle', { - defaultMessage: 'Number format', - }), - value: '0,0.[000]', - type: 'string', - description: i18n.translate('kbn.advancedSettings.format.numberFormatText', { - defaultMessage: 'Default {numeralFormatLink} for the "number" format', - description: - 'Part of composite text: kbn.advancedSettings.format.numberFormatText + ' + - 'kbn.advancedSettings.format.numberFormat.numeralFormatLinkText', - values: { - numeralFormatLink: - '' + - i18n.translate('kbn.advancedSettings.format.numberFormat.numeralFormatLinkText', { - defaultMessage: 'numeral format', - }) + - '', - }, - }), - }, - 'format:bytes:defaultPattern': { - name: i18n.translate('kbn.advancedSettings.format.bytesFormatTitle', { - defaultMessage: 'Bytes format', - }), - value: '0,0.[0]b', - type: 'string', - description: i18n.translate('kbn.advancedSettings.format.bytesFormatText', { - defaultMessage: 'Default {numeralFormatLink} for the "bytes" format', - description: - 'Part of composite text: kbn.advancedSettings.format.bytesFormatText + ' + - 'kbn.advancedSettings.format.bytesFormat.numeralFormatLinkText', - values: { - numeralFormatLink: - '' + - i18n.translate('kbn.advancedSettings.format.bytesFormat.numeralFormatLinkText', { - defaultMessage: 'numeral format', - }) + - '', - }, - }), - }, - 'format:percent:defaultPattern': { - name: i18n.translate('kbn.advancedSettings.format.percentFormatTitle', { - defaultMessage: 'Percent format', - }), - value: '0,0.[000]%', - type: 'string', - description: i18n.translate('kbn.advancedSettings.format.percentFormatText', { - defaultMessage: 'Default {numeralFormatLink} for the "percent" format', - description: - 'Part of composite text: kbn.advancedSettings.format.percentFormatText + ' + - 'kbn.advancedSettings.format.percentFormat.numeralFormatLinkText', - values: { - numeralFormatLink: - '' + - i18n.translate('kbn.advancedSettings.format.percentFormat.numeralFormatLinkText', { - defaultMessage: 'numeral format', - }) + - '', - }, - }), - }, - 'format:currency:defaultPattern': { - name: i18n.translate('kbn.advancedSettings.format.currencyFormatTitle', { - defaultMessage: 'Currency format', - }), - value: '($0,0.[00])', - type: 'string', - description: i18n.translate('kbn.advancedSettings.format.currencyFormatText', { - defaultMessage: 'Default {numeralFormatLink} for the "currency" format', - description: - 'Part of composite text: kbn.advancedSettings.format.currencyFormatText + ' + - 'kbn.advancedSettings.format.currencyFormat.numeralFormatLinkText', - values: { - numeralFormatLink: - '' + - i18n.translate('kbn.advancedSettings.format.currencyFormat.numeralFormatLinkText', { - defaultMessage: 'numeral format', - }) + - '', - }, - }), - }, - 'format:number:defaultLocale': { - name: i18n.translate('kbn.advancedSettings.format.formattingLocaleTitle', { - defaultMessage: 'Formatting locale', - }), - value: 'en', - type: 'select', - options: numeralLanguageIds, - optionLabels: Object.fromEntries( - numeralLanguages.map((language) => [language.id, language.name]) - ), - description: i18n.translate('kbn.advancedSettings.format.formattingLocaleText', { - defaultMessage: `{numeralLanguageLink} locale`, - description: - 'Part of composite text: kbn.advancedSettings.format.formattingLocale.numeralLanguageLinkText + ' + - 'kbn.advancedSettings.format.formattingLocaleText', - values: { - numeralLanguageLink: - '' + - i18n.translate('kbn.advancedSettings.format.formattingLocale.numeralLanguageLinkText', { - defaultMessage: 'Numeral language', - }) + - '', - }, - }), - }, 'timepicker:timeDefaults': { name: i18n.translate('kbn.advancedSettings.timepicker.timeDefaultsTitle', { defaultMessage: 'Time filter defaults', @@ -686,120 +265,6 @@ export function getUiSettingDefaults() { }), requiresPageReload: true, }, - 'timepicker:refreshIntervalDefaults': { - name: i18n.translate('kbn.advancedSettings.timepicker.refreshIntervalDefaultsTitle', { - defaultMessage: 'Time filter refresh interval', - }), - value: `{ - "pause": false, - "value": 0 -}`, - type: 'json', - description: i18n.translate('kbn.advancedSettings.timepicker.refreshIntervalDefaultsText', { - defaultMessage: `The timefilter's default refresh interval`, - }), - requiresPageReload: true, - }, - 'timepicker:quickRanges': { - name: i18n.translate('kbn.advancedSettings.timepicker.quickRangesTitle', { - defaultMessage: 'Time filter quick ranges', - }), - value: JSON.stringify( - [ - { - from: 'now/d', - to: 'now/d', - display: i18n.translate('kbn.advancedSettings.timepicker.today', { - defaultMessage: 'Today', - }), - }, - { - from: 'now/w', - to: 'now/w', - display: i18n.translate('kbn.advancedSettings.timepicker.thisWeek', { - defaultMessage: 'This week', - }), - }, - { - from: 'now-15m', - to: 'now', - display: i18n.translate('kbn.advancedSettings.timepicker.last15Minutes', { - defaultMessage: 'Last 15 minutes', - }), - }, - { - from: 'now-30m', - to: 'now', - display: i18n.translate('kbn.advancedSettings.timepicker.last30Minutes', { - defaultMessage: 'Last 30 minutes', - }), - }, - { - from: 'now-1h', - to: 'now', - display: i18n.translate('kbn.advancedSettings.timepicker.last1Hour', { - defaultMessage: 'Last 1 hour', - }), - }, - { - from: 'now-24h', - to: 'now', - display: i18n.translate('kbn.advancedSettings.timepicker.last24Hours', { - defaultMessage: 'Last 24 hours', - }), - }, - { - from: 'now-7d', - to: 'now', - display: i18n.translate('kbn.advancedSettings.timepicker.last7Days', { - defaultMessage: 'Last 7 days', - }), - }, - { - from: 'now-30d', - to: 'now', - display: i18n.translate('kbn.advancedSettings.timepicker.last30Days', { - defaultMessage: 'Last 30 days', - }), - }, - { - from: 'now-90d', - to: 'now', - display: i18n.translate('kbn.advancedSettings.timepicker.last90Days', { - defaultMessage: 'Last 90 days', - }), - }, - { - from: 'now-1y', - to: 'now', - display: i18n.translate('kbn.advancedSettings.timepicker.last1Year', { - defaultMessage: 'Last 1 year', - }), - }, - ], - null, - 2 - ), - type: 'json', - description: i18n.translate('kbn.advancedSettings.timepicker.quickRangesText', { - defaultMessage: - 'The list of ranges to show in the Quick section of the time filter. This should be an array of objects, ' + - 'with each object containing "from", "to" (see {acceptedFormatsLink}), and ' + - '"display" (the title to be displayed).', - description: - 'Part of composite text: kbn.advancedSettings.timepicker.quickRangesText + ' + - 'kbn.advancedSettings.timepicker.quickRanges.acceptedFormatsLinkText', - values: { - acceptedFormatsLink: - `` + - i18n.translate('kbn.advancedSettings.timepicker.quickRanges.acceptedFormatsLinkText', { - defaultMessage: 'accepted formats', - }) + - '', - }, - }), - }, 'theme:darkMode': { name: i18n.translate('kbn.advancedSettings.darkModeTitle', { defaultMessage: 'Dark mode', @@ -822,26 +287,6 @@ export function getUiSettingDefaults() { }), requiresPageReload: true, }, - 'filters:pinnedByDefault': { - name: i18n.translate('kbn.advancedSettings.pinFiltersTitle', { - defaultMessage: 'Pin filters by default', - }), - value: false, - description: i18n.translate('kbn.advancedSettings.pinFiltersText', { - defaultMessage: 'Whether the filters should have a global state (be pinned) by default', - }), - }, - 'filterEditor:suggestValues': { - name: i18n.translate('kbn.advancedSettings.suggestFilterValuesTitle', { - defaultMessage: 'Filter editor suggest values', - description: '"Filter editor" refers to the UI you create filters in.', - }), - value: true, - description: i18n.translate('kbn.advancedSettings.suggestFilterValuesText', { - defaultMessage: - 'Set this property to false to prevent the filter editor from suggesting values for fields.', - }), - }, 'notifications:banner': { name: i18n.translate('kbn.advancedSettings.notifications.bannerTitle', { defaultMessage: 'Custom banner notification', @@ -930,28 +375,6 @@ export function getUiSettingDefaults() { type: 'number', category: ['notifications'], }, - 'state:storeInSessionStorage': { - name: i18n.translate('kbn.advancedSettings.storeUrlTitle', { - defaultMessage: 'Store URLs in session storage', - }), - value: false, - description: i18n.translate('kbn.advancedSettings.storeUrlText', { - defaultMessage: - 'The URL can sometimes grow to be too large for some browsers to handle. ' + - 'To counter-act this we are testing if storing parts of the URL in session storage could help. ' + - 'Please let us know how it goes!', - }), - }, - 'indexPattern:placeholder': { - name: i18n.translate('kbn.advancedSettings.indexPatternPlaceholderTitle', { - defaultMessage: 'Index pattern placeholder', - }), - value: '', - description: i18n.translate('kbn.advancedSettings.indexPatternPlaceholderText', { - defaultMessage: - 'The placeholder for the "Index pattern name" field in "Management > Index Patterns > Create Index Pattern".', - }), - }, 'accessibility:disableAnimations': { name: i18n.translate('kbn.advancedSettings.disableAnimationsTitle', { defaultMessage: 'Disable Animations', diff --git a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js b/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js index 08a347fbf72959..879fab206b99df 100644 --- a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js +++ b/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js @@ -29,6 +29,7 @@ import { PaginateDirectiveProvider, } from '../../../../../plugins/kibana_legacy/public'; import { PER_PAGE_SETTING } from '../../../../../plugins/saved_objects/common'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../../../plugins/visualizations/public'; const module = uiModules.get('kibana'); @@ -294,7 +295,7 @@ module prevSearch = filter; - const isLabsEnabled = config.get('visualize:enableLabs'); + const isLabsEnabled = config.get(VISUALIZE_ENABLE_LABS_SETTING); self.service.find(filter).then(function (hits) { hits.hits = hits.hits.filter( (hit) => isLabsEnabled || _.get(hit, 'type.stage') !== 'experimental' diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 229bfb1978a4ef..d98770842a0f09 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -28,6 +28,11 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../src/plugins/data/public/search/aggs'; import { ComponentRegistry } from '../../../../../src/plugins/advanced_settings/public/'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/public/'; +import { + CSV_SEPARATOR_SETTING, + CSV_QUOTE_VALUES_SETTING, +} from '../../../../../src/plugins/share/public'; const mockObservable = () => { return { @@ -49,18 +54,31 @@ let isTimeRangeSelectorEnabled = true; let isAutoRefreshSelectorEnabled = true; export const mockUiSettings = { - get: (item) => { - return mockUiSettings[item]; + get: (item, defaultValue) => { + const defaultValues = { + dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', + 'dateFormat:tz': 'UTC', + [UI_SETTINGS.SHORT_DOTS_ENABLE]: true, + [UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX]: true, + [UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS]: true, + [UI_SETTINGS.QUERY_STRING_OPTIONS]: {}, + [UI_SETTINGS.FORMAT_CURRENCY_DEFAULT_PATTERN]: '($0,0.[00])', + [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN]: '0,0.[000]', + [UI_SETTINGS.FORMAT_PERCENT_DEFAULT_PATTERN]: '0,0.[000]%', + [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_LOCALE]: 'en', + [UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP]: {}, + [CSV_SEPARATOR_SETTING]: ',', + [CSV_QUOTE_VALUES_SETTING]: true, + [UI_SETTINGS.SEARCH_QUERY_LANGUAGE]: 'kuery', + 'state:storeInSessionStorage': false, + }; + + return defaultValues[item] || defaultValue; }, getUpdate$: () => ({ subscribe: sinon.fake(), }), isDefault: sinon.fake(), - 'query:allowLeadingWildcards': true, - 'query:queryString:options': {}, - 'courier:ignoreFilterIfFieldNotInIndex': true, - 'dateFormat:tz': 'Browser', - 'format:defaultTypeMap': {}, }; const mockCoreSetup = { @@ -236,6 +254,9 @@ export const npSetup = { }, share: { register: () => {}, + urlGenerators: { + registerUrlGenerator: () => {}, + }, }, devTools: { register: () => {}, @@ -524,6 +545,8 @@ export function __setup__(coreSetup) { // bootstrap an LP plugin outside of tests) npSetup.core.application.register = () => {}; + npSetup.core.uiSettings.get = mockUiSettings.get; + // Services that need to be set in the legacy platform since the legacy data // & vis plugins which previously provided them have been removed. setSetupServices(npSetup); @@ -532,6 +555,8 @@ export function __setup__(coreSetup) { export function __start__(coreStart) { npStart.core = coreStart; + npStart.core.uiSettings.get = mockUiSettings.get; + // Services that need to be set in the legacy platform since the legacy data // & vis plugins which previously provided them have been removed. setStartServices(npStart); 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/legacy/ui/public/timefilter/setup_router.ts b/src/legacy/ui/public/timefilter/setup_router.ts index a7492e538b3af1..7c25c6aa3166e3 100644 --- a/src/legacy/ui/public/timefilter/setup_router.ts +++ b/src/legacy/ui/public/timefilter/setup_router.ts @@ -21,10 +21,15 @@ import _ from 'lodash'; import { IScope } from 'angular'; import moment from 'moment'; import chrome from 'ui/chrome'; -import { RefreshInterval, TimeRange, TimefilterContract } from 'src/plugins/data/public'; import { Subscription } from 'rxjs'; import { fatalError } from 'ui/notify/fatal_error'; import { subscribeWithScope } from '../../../../plugins/kibana_legacy/public'; +import { + RefreshInterval, + TimeRange, + TimefilterContract, + UI_SETTINGS, +} from '../../../../plugins/data/public'; // TODO // remove everything underneath once globalState is no longer an angular service @@ -38,7 +43,7 @@ export function getTimefilterConfig() { const settings = chrome.getUiSettingsClient(); return { timeDefaults: settings.get('timepicker:timeDefaults'), - refreshIntervalDefaults: settings.get('timepicker:refreshIntervalDefaults'), + refreshIntervalDefaults: settings.get(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS), }; } diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 8dd0a766da97b7..a59d1e8c546d4b 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -41,6 +41,7 @@ import { QueryState, SavedQuery, syncQueryStateWithUrl, + UI_SETTINGS, } from '../../../data/public'; import { getSavedObjectFinder, SaveResult, showSaveModal } from '../../../saved_objects/public'; @@ -430,7 +431,8 @@ export class DashboardAppController { dashboardStateManager.getQuery() || { query: '', language: - localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), + localStorage.get('kibana.userQueryLanguage') || + uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE), }, queryFilter.getFilters() ); 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/panel/dashboard_panel_placement.ts b/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts index 62a39ee898d3a6..1b060c186db973 100644 --- a/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts +++ b/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts @@ -108,16 +108,32 @@ interface IplacementDirection { fits: boolean; } +/** + * Compare grid data by an ending y coordinate. Grid data with a smaller ending y coordinate + * comes first. + * @param a + * @param b + */ +function comparePanels(a: GridData, b: GridData): number { + if (a.y + a.h < b.y + b.h) { + return -1; + } + if (a.y + a.h > b.y + b.h) { + return 1; + } + // a.y === b.y + if (a.x + a.w <= b.x + b.w) { + return -1; + } + return 1; +} + export function placePanelBeside({ width, height, currentPanels, placeBesideId, }: IPanelPlacementBesideArgs): Omit { - // const clonedPanels = _.cloneDeep(currentPanels); - if (!placeBesideId) { - throw new Error('Place beside method called without placeBesideId'); - } const panelToPlaceBeside = currentPanels[placeBesideId]; if (!panelToPlaceBeside) { throw new PanelNotFoundError(); @@ -130,10 +146,11 @@ export function placePanelBeside({ const possiblePlacementDirections: IplacementDirection[] = [ { grid: { x: beside.x + beside.w, y: beside.y, w: width, h: height }, fits: true }, // right - { grid: { x: beside.x - width, y: beside.y, w: width, h: height }, fits: true }, // left + { grid: { x: 0, y: beside.y + beside.h, w: width, h: height }, fits: true }, // left side of next row { grid: { x: beside.x, y: beside.y + beside.h, w: width, h: height }, fits: true }, // bottom ]; + // first, we check if there is place around the current panel for (const direction of possiblePlacementDirections) { if ( direction.grid.x >= 0 && @@ -156,13 +173,32 @@ export function placePanelBeside({ } } // if we get here that means there is no blank space around the panel we are placing beside. This means it's time to mess up the dashboard's groove. Fun! - const [, , bottomPlacement] = possiblePlacementDirections; - for (const currentPanelGrid of otherPanels) { - if (bottomPlacement.grid.y <= currentPanelGrid.y) { - const movedPanel = _.cloneDeep(currentPanels[currentPanelGrid.i]); - movedPanel.gridData.y = movedPanel.gridData.y + bottomPlacement.grid.h; - currentPanels[currentPanelGrid.i] = movedPanel; + /** + * 1. sort the panels in the grid + * 2. place the cloned panel to the bottom + * 3. reposition the panels after the cloned panel in the grid + */ + const grid = otherPanels.sort(comparePanels); + + let position = 0; + for (position; position < grid.length; position++) { + if (beside.i === grid[position].i) { + break; } } + const bottomPlacement = possiblePlacementDirections[2]; + // place to the bottom and move all other panels + let originalPositionInTheGrid = grid[position + 1].i; + const diff = + bottomPlacement.grid.y + + bottomPlacement.grid.h - + currentPanels[originalPositionInTheGrid].gridData.y; + + for (let j = position + 1; j < grid.length; j++) { + originalPositionInTheGrid = grid[j].i; + const movedPanel = _.cloneDeep(currentPanels[originalPositionInTheGrid]); + movedPanel.gridData.y = movedPanel.gridData.y + diff; + currentPanels[originalPositionInTheGrid] = movedPanel; + } return bottomPlacement.grid; } 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/data/common/constants.ts b/src/plugins/data/common/constants.ts index 66a96e3e6e1293..8ec72dc1f9a744 100644 --- a/src/plugins/data/common/constants.ts +++ b/src/plugins/data/common/constants.ts @@ -18,5 +18,33 @@ */ export const DEFAULT_QUERY_LANGUAGE = 'kuery'; -export const META_FIELDS_SETTING = 'metaFields'; -export const DOC_HIGHLIGHT_SETTING = 'doc_table:highlight'; + +export const UI_SETTINGS = { + META_FIELDS: 'metaFields', + DOC_HIGHLIGHT: 'doc_table:highlight', + QUERY_STRING_OPTIONS: 'query:queryString:options', + QUERY_ALLOW_LEADING_WILDCARDS: 'query:allowLeadingWildcards', + SEARCH_QUERY_LANGUAGE: 'search:queryLanguage', + SORT_OPTIONS: 'sort:options', + COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: 'courier:ignoreFilterIfFieldNotInIndex', + COURIER_SET_REQUEST_PREFERENCE: 'courier:setRequestPreference', + COURIER_CUSTOM_REQUEST_PREFERENCE: 'courier:customRequestPreference', + COURIER_MAX_CONCURRENT_SHARD_REQUESTS: 'courier:maxConcurrentShardRequests', + COURIER_BATCH_SEARCHES: 'courier:batchSearches', + SEARCH_INCLUDE_FROZEN: 'search:includeFrozen', + HISTOGRAM_BAR_TARGET: 'histogram:barTarget', + HISTOGRAM_MAX_BARS: 'histogram:maxBars', + HISTORY_LIMIT: 'history:limit', + SHORT_DOTS_ENABLE: 'shortDots:enable', + FORMAT_DEFAULT_TYPE_MAP: 'format:defaultTypeMap', + FORMAT_NUMBER_DEFAULT_PATTERN: 'format:number:defaultPattern', + FORMAT_PERCENT_DEFAULT_PATTERN: 'format:percent:defaultPattern', + FORMAT_BYTES_DEFAULT_PATTERN: 'format:bytes:defaultPattern', + FORMAT_CURRENCY_DEFAULT_PATTERN: 'format:currency:defaultPattern', + FORMAT_NUMBER_DEFAULT_LOCALE: 'format:number:defaultLocale', + TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: 'timepicker:refreshIntervalDefaults', + TIMEPICKER_QUICK_RANGES: 'timepicker:quickRanges', + INDEXPATTERN_PLACEHOLDER: 'indexPattern:placeholder', + FILTERS_PINNED_BY_DEFAULT: 'filters:pinnedByDefault', + FILTERS_EDITOR_SUGGEST_VALUES: 'filterEditor:suggestValues', +}; diff --git a/src/plugins/data/common/es_query/es_query/get_es_query_config.test.ts b/src/plugins/data/common/es_query/es_query/get_es_query_config.test.ts index d146d81973d0d7..5fa3c67dea400c 100644 --- a/src/plugins/data/common/es_query/es_query/get_es_query_config.test.ts +++ b/src/plugins/data/common/es_query/es_query/get_es_query_config.test.ts @@ -19,18 +19,19 @@ import { get } from 'lodash'; import { getEsQueryConfig } from './get_es_query_config'; import { IUiSettingsClient } from 'kibana/public'; +import { UI_SETTINGS } from '../../'; const config = ({ get(item: string) { return get(config, item); }, - 'query:allowLeadingWildcards': { + [UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS]: { allowLeadingWildcards: true, }, - 'query:queryString:options': { + [UI_SETTINGS.QUERY_STRING_OPTIONS]: { queryStringOptions: {}, }, - 'courier:ignoreFilterIfFieldNotInIndex': { + [UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX]: { ignoreFilterIfFieldNotInIndex: true, }, 'dateFormat:tz': { diff --git a/src/plugins/data/common/es_query/es_query/get_es_query_config.ts b/src/plugins/data/common/es_query/es_query/get_es_query_config.ts index 0a82cf03bdb445..ff8fc5b11b26e8 100644 --- a/src/plugins/data/common/es_query/es_query/get_es_query_config.ts +++ b/src/plugins/data/common/es_query/es_query/get_es_query_config.ts @@ -18,15 +18,18 @@ */ import { EsQueryConfig } from './build_es_query'; +import { UI_SETTINGS } from '../../'; interface KibanaConfig { get(key: string): T; } export function getEsQueryConfig(config: KibanaConfig) { - const allowLeadingWildcards = config.get('query:allowLeadingWildcards'); - const queryStringOptions = config.get('query:queryString:options'); - const ignoreFilterIfFieldNotInIndex = config.get('courier:ignoreFilterIfFieldNotInIndex'); + const allowLeadingWildcards = config.get(UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS); + const queryStringOptions = config.get(UI_SETTINGS.QUERY_STRING_OPTIONS); + const ignoreFilterIfFieldNotInIndex = config.get( + UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX + ); const dateFormatTZ = config.get('dateFormat:tz'); return { diff --git a/src/plugins/data/common/field_formats/converters/bytes.test.ts b/src/plugins/data/common/field_formats/converters/bytes.test.ts index 8dad9fc206e721..e0c26170c29075 100644 --- a/src/plugins/data/common/field_formats/converters/bytes.test.ts +++ b/src/plugins/data/common/field_formats/converters/bytes.test.ts @@ -18,11 +18,12 @@ */ import { BytesFormat } from './bytes'; +import { UI_SETTINGS } from '../../constants'; describe('BytesFormat', () => { const config: Record = {}; - config['format:bytes:defaultPattern'] = '0,0.[000]b'; + config[UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN] = '0,0.[000]b'; const getConfig = (key: string) => config[key]; diff --git a/src/plugins/data/common/field_formats/converters/number.test.ts b/src/plugins/data/common/field_formats/converters/number.test.ts index fe36d5b12e8736..31c5ea41bf5afd 100644 --- a/src/plugins/data/common/field_formats/converters/number.test.ts +++ b/src/plugins/data/common/field_formats/converters/number.test.ts @@ -18,11 +18,12 @@ */ import { NumberFormat } from './number'; +import { UI_SETTINGS } from '../../constants'; describe('NumberFormat', () => { const config: Record = {}; - config['format:number:defaultPattern'] = '0,0.[000]'; + config[UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN] = '0,0.[000]'; const getConfig = (key: string) => config[key]; diff --git a/src/plugins/data/common/field_formats/converters/numeral.ts b/src/plugins/data/common/field_formats/converters/numeral.ts index a483b5a1e4f99a..1d844bca3f89ac 100644 --- a/src/plugins/data/common/field_formats/converters/numeral.ts +++ b/src/plugins/data/common/field_formats/converters/numeral.ts @@ -24,6 +24,7 @@ import numeralLanguages from '@elastic/numeral/languages'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; import { TextContextTypeConvert } from '../types'; +import { UI_SETTINGS } from '../../constants'; const numeralInst = numeral(); @@ -51,7 +52,8 @@ export abstract class NumeralFormat extends FieldFormat { if (isNaN(val)) return ''; const previousLocale = numeral.language(); - const defaultLocale = (this.getConfig && this.getConfig('format:number:defaultLocale')) || 'en'; + const defaultLocale = + (this.getConfig && this.getConfig(UI_SETTINGS.FORMAT_NUMBER_DEFAULT_LOCALE)) || 'en'; numeral.language(defaultLocale); const formatted = numeralInst.set(val).format(this.param('pattern')); diff --git a/src/plugins/data/common/field_formats/converters/percent.test.ts b/src/plugins/data/common/field_formats/converters/percent.test.ts index 8b26564814af3d..754234bdeb78b4 100644 --- a/src/plugins/data/common/field_formats/converters/percent.test.ts +++ b/src/plugins/data/common/field_formats/converters/percent.test.ts @@ -18,11 +18,12 @@ */ import { PercentFormat } from './percent'; +import { UI_SETTINGS } from '../../constants'; describe('PercentFormat', () => { const config: Record = {}; - config['format:percent:defaultPattern'] = '0,0.[000]%'; + config[UI_SETTINGS.FORMAT_PERCENT_DEFAULT_PATTERN] = '0,0.[000]%'; const getConfig = (key: string) => config[key]; diff --git a/src/plugins/data/common/field_formats/converters/percent.ts b/src/plugins/data/common/field_formats/converters/percent.ts index ef3b0a1503a981..ecf9c7d19108d9 100644 --- a/src/plugins/data/common/field_formats/converters/percent.ts +++ b/src/plugins/data/common/field_formats/converters/percent.ts @@ -20,6 +20,7 @@ import { i18n } from '@kbn/i18n'; import { NumeralFormat } from './numeral'; import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; +import { UI_SETTINGS } from '../../constants'; export class PercentFormat extends NumeralFormat { static id = FIELD_FORMAT_IDS.PERCENT; @@ -32,7 +33,7 @@ export class PercentFormat extends NumeralFormat { allowsNumericalAggregations = true; getParamDefaults = () => ({ - pattern: this.getConfig!('format:percent:defaultPattern'), + pattern: this.getConfig!(UI_SETTINGS.FORMAT_PERCENT_DEFAULT_PATTERN), fractional: true, }); diff --git a/src/plugins/data/common/field_formats/converters/source.ts b/src/plugins/data/common/field_formats/converters/source.ts index 9e50d47bb2624b..f00261e00971aa 100644 --- a/src/plugins/data/common/field_formats/converters/source.ts +++ b/src/plugins/data/common/field_formats/converters/source.ts @@ -22,6 +22,7 @@ import { shortenDottedString } from '../../utils'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; import { TextContextTypeConvert, HtmlContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; +import { UI_SETTINGS } from '../../'; /** * Remove all of the whitespace between html tags @@ -71,7 +72,7 @@ export class SourceFormat extends FieldFormat { const formatted = field.indexPattern.formatHit(hit); const highlightPairs: any[] = []; const sourcePairs: any[] = []; - const isShortDots = this.getConfig!('shortDots:enable'); + const isShortDots = this.getConfig!(UI_SETTINGS.SHORT_DOTS_ENABLE); keys(formatted).forEach((key) => { const pairs = highlights[key] ? highlightPairs : sourcePairs; diff --git a/src/plugins/data/common/field_formats/field_formats_registry.ts b/src/plugins/data/common/field_formats/field_formats_registry.ts index c04a371066de33..9325485bce75d6 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.ts @@ -33,6 +33,7 @@ import { baseFormatters } from './constants/base_formatters'; import { FieldFormat } from './field_format'; import { SerializedFieldFormat } from '../../../expressions/common/types'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../types'; +import { UI_SETTINGS } from '../'; export class FieldFormatsRegistry { protected fieldFormats: Map = new Map(); @@ -49,7 +50,7 @@ export class FieldFormatsRegistry { metaParamsOptions: Record = {}, defaultFieldConverters: FieldFormatInstanceType[] = baseFormatters ) { - const defaultTypeMap = getConfig('format:defaultTypeMap'); + const defaultTypeMap = getConfig(UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP); this.register(defaultFieldConverters); this.parseDefaultTypeMap(defaultTypeMap); this.getConfig = getConfig; diff --git a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts index ef7a0e8c3a5ad0..a6a45a26f06b39 100644 --- a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts +++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts @@ -19,7 +19,7 @@ import { memoize } from 'lodash'; import { CoreSetup } from 'src/core/public'; -import { IIndexPattern, IFieldType } from '../../../common'; +import { IIndexPattern, IFieldType, UI_SETTINGS } from '../../../common'; function resolver(title: string, field: IFieldType, query: string, boolFilter: any) { // Only cache results for a minute @@ -58,7 +58,9 @@ export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsG boolFilter, signal, }: ValueSuggestionsGetFnArgs): Promise => { - const shouldSuggestValues = core!.uiSettings.get('filterEditor:suggestValues'); + const shouldSuggestValues = core!.uiSettings.get( + UI_SETTINGS.FILTERS_EDITOR_SUGGEST_VALUES + ); const { title } = indexPattern; if (field.type === 'boolean') { diff --git a/src/plugins/data/public/field_formats/field_formats_service.ts b/src/plugins/data/public/field_formats/field_formats_service.ts index 22c7e90c06130b..3ddc8d0b68a5bb 100644 --- a/src/plugins/data/public/field_formats/field_formats_service.ts +++ b/src/plugins/data/public/field_formats/field_formats_service.ts @@ -18,7 +18,7 @@ */ import { CoreSetup } from 'src/core/public'; -import { FieldFormatsRegistry } from '../../common'; +import { FieldFormatsRegistry, UI_SETTINGS } from '../../common'; import { deserializeFieldFormat } from './utils/deserialize'; import { FormatFactory } from '../../common/field_formats/utils'; import { baseFormattersPublic } from './constants'; @@ -28,7 +28,7 @@ export class FieldFormatsService { public setup(core: CoreSetup) { core.uiSettings.getUpdate$().subscribe(({ key, newValue }) => { - if (key === 'format:defaultTypeMap') { + if (key === UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP) { this.fieldFormatsRegistry.parseDefaultTypeMap(newValue); } }); diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 0c946fc6e1858b..55400393237567 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -17,8 +17,6 @@ * under the License. */ -import './index.scss'; - import { PluginInitializerContext } from '../../../core/public'; import { ConfigSchema } from '../config'; @@ -267,6 +265,7 @@ export { ES_FIELD_TYPES, KBN_FIELD_TYPES, IndexPatternAttributes, + UI_SETTINGS, } from '../common'; /* diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/public/index_patterns/index_patterns/index_pattern.ts index 43404c32cb3d4f..3d54009d0fdcab 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_pattern.ts @@ -33,7 +33,7 @@ import { KBN_FIELD_TYPES, IIndexPattern, IFieldType, - META_FIELDS_SETTING, + UI_SETTINGS, } from '../../../common'; import { findByTitle } from '../utils'; import { IndexPatternMissingIndices } from '../lib'; @@ -108,8 +108,8 @@ export class IndexPattern implements IIndexPattern { // which cause problems when being consumed from angular this.getConfig = getConfig; - this.shortDotsEnable = this.getConfig('shortDots:enable'); - this.metaFields = this.getConfig(META_FIELDS_SETTING); + this.shortDotsEnable = this.getConfig(UI_SETTINGS.SHORT_DOTS_ENABLE); + this.metaFields = this.getConfig(UI_SETTINGS.META_FIELDS); this.createFieldList = getIndexPatternFieldListCreator({ fieldFormats: getFieldFormats(), @@ -117,8 +117,12 @@ export class IndexPattern implements IIndexPattern { }); this.fields = this.createFieldList(this, [], this.shortDotsEnable); - this.fieldsFetcher = createFieldsFetcher(this, apiClient, this.getConfig(META_FIELDS_SETTING)); - this.flattenHit = flattenHitWrapper(this, this.getConfig(META_FIELDS_SETTING)); + this.fieldsFetcher = createFieldsFetcher( + this, + apiClient, + this.getConfig(UI_SETTINGS.META_FIELDS) + ); + this.flattenHit = flattenHitWrapper(this, this.getConfig(UI_SETTINGS.META_FIELDS)); this.formatHit = formatHitProvider( this, getFieldFormats().getDefaultInstance(KBN_FIELD_TYPES.STRING) diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 66e8d5a6e739a1..06b5cbdfdfdfb5 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + import { PluginInitializerContext, CoreSetup, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index ebb4e9d583bc0e..dcdb528ac8b7d5 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1792,6 +1792,39 @@ export interface TimeRange { // @public export type TSearchStrategyProvider = (context: ISearchContext) => ISearchStrategy; +// Warning: (ae-missing-release-tag) "UI_SETTINGS" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const UI_SETTINGS: { + META_FIELDS: string; + DOC_HIGHLIGHT: string; + QUERY_STRING_OPTIONS: string; + QUERY_ALLOW_LEADING_WILDCARDS: string; + SEARCH_QUERY_LANGUAGE: string; + SORT_OPTIONS: string; + COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string; + COURIER_SET_REQUEST_PREFERENCE: string; + COURIER_CUSTOM_REQUEST_PREFERENCE: string; + COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string; + COURIER_BATCH_SEARCHES: string; + SEARCH_INCLUDE_FROZEN: string; + HISTOGRAM_BAR_TARGET: string; + HISTOGRAM_MAX_BARS: string; + HISTORY_LIMIT: string; + SHORT_DOTS_ENABLE: string; + FORMAT_DEFAULT_TYPE_MAP: string; + FORMAT_NUMBER_DEFAULT_PATTERN: string; + FORMAT_PERCENT_DEFAULT_PATTERN: string; + FORMAT_BYTES_DEFAULT_PATTERN: string; + FORMAT_CURRENCY_DEFAULT_PATTERN: string; + FORMAT_NUMBER_DEFAULT_LOCALE: string; + TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string; + TIMEPICKER_QUICK_RANGES: string; + INDEXPATTERN_PLACEHOLDER: string; + FILTERS_PINNED_BY_DEFAULT: string; + FILTERS_EDITOR_SUGGEST_VALUES: string; +}; + // Warnings were encountered during analysis: // @@ -1800,52 +1833,52 @@ export type TSearchStrategyProvider = (context: ISearc // src/plugins/data/common/es_query/filters/match_all_filter.ts:28:3 - (ae-forgotten-export) The symbol "MatchAllFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:67:23 - (ae-forgotten-export) The symbol "FilterLabel" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:67:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:67:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:67:23 - (ae-forgotten-export) The symbol "generateFilters" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:67:23 - (ae-forgotten-export) The symbol "changeTimeFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:67:23 - (ae-forgotten-export) The symbol "convertRangeFilterToTimeRangeString" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:67:23 - (ae-forgotten-export) The symbol "extractTimeFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:138:21 - (ae-forgotten-export) The symbol "buildEsQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:138:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:138:21 - (ae-forgotten-export) The symbol "luceneStringToDsl" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:138:21 - (ae-forgotten-export) The symbol "decorateQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:180:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:180:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:180:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:180:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:180:26 - (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:180:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:180:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:180:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:180:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:180:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:180:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:180:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:180:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:180:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:180:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:378:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:378:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:378:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:378:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:380:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:381:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:392:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:397:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:404:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "FilterLabel" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "generateFilters" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "changeTimeFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "convertRangeFilterToTimeRangeString" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "extractTimeFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:136:21 - (ae-forgotten-export) The symbol "buildEsQuery" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:136:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:136:21 - (ae-forgotten-export) The symbol "luceneStringToDsl" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:136:21 - (ae-forgotten-export) The symbol "decorateQuery" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:377:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:377:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:377:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:377:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:379:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:380:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:33:33 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:37:1 - (ae-forgotten-export) The symbol "QueryStateChange" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.test.ts b/src/plugins/data/public/query/filter_manager/filter_manager.test.ts index 3c69a498e74cd3..878142906f54bd 100644 --- a/src/plugins/data/public/query/filter_manager/filter_manager.test.ts +++ b/src/plugins/data/public/query/filter_manager/filter_manager.test.ts @@ -24,14 +24,14 @@ import { Subscription } from 'rxjs'; import { FilterManager } from './filter_manager'; import { getFilter } from './test_helpers/get_stub_filter'; import { getFiltersArray } from './test_helpers/get_filters_array'; -import { Filter, FilterStateStore } from '../../../common'; +import { Filter, FilterStateStore, UI_SETTINGS } from '../../../common'; import { coreMock } from '../../../../../core/public/mocks'; const setupMock = coreMock.createSetup(); const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { switch (key) { - case 'filters:pinnedByDefault': + case UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT: return pinnedByDefault; default: throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.ts b/src/plugins/data/public/query/filter_manager/filter_manager.ts index d58a0eb45c04f7..60a49a4bd50f4a 100644 --- a/src/plugins/data/public/query/filter_manager/filter_manager.ts +++ b/src/plugins/data/public/query/filter_manager/filter_manager.ts @@ -34,6 +34,7 @@ import { isFilterPinned, compareFilters, COMPARE_ALL_OPTIONS, + UI_SETTINGS, } from '../../../common'; export class FilterManager { @@ -129,7 +130,7 @@ export class FilterManager { public addFilters( filters: Filter[] | Filter, - pinFilterStatus: boolean = this.uiSettings.get('filters:pinnedByDefault') + pinFilterStatus: boolean = this.uiSettings.get(UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT) ) { if (!Array.isArray(filters)) { filters = [filters]; @@ -157,7 +158,7 @@ export class FilterManager { public setFilters( newFilters: Filter[], - pinFilterStatus: boolean = this.uiSettings.get('filters:pinnedByDefault') + pinFilterStatus: boolean = this.uiSettings.get(UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT) ) { const store = pinFilterStatus ? FilterStateStore.GLOBAL_STATE : FilterStateStore.APP_STATE; diff --git a/src/plugins/data/public/query/lib/get_query_log.ts b/src/plugins/data/public/query/lib/get_query_log.ts index a71eb7580cf07e..b7827d2c8de021 100644 --- a/src/plugins/data/public/query/lib/get_query_log.ts +++ b/src/plugins/data/public/query/lib/get_query_log.ts @@ -20,6 +20,7 @@ import { IUiSettingsClient } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { PersistedLog } from '../persisted_log'; +import { UI_SETTINGS } from '../../../common'; export function getQueryLog( uiSettings: IUiSettingsClient, @@ -30,7 +31,7 @@ export function getQueryLog( return new PersistedLog( `typeahead:${appName}-${language}`, { - maxLength: uiSettings.get('history:limit'), + maxLength: uiSettings.get(UI_SETTINGS.HISTORY_LIMIT), filterDuplicates: true, }, storage diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts index 06e4c1c8be6d5d..4e394445b75ae3 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts @@ -20,7 +20,7 @@ import { Subscription } from 'rxjs'; import { FilterManager } from '../filter_manager'; import { getFilter } from '../filter_manager/test_helpers/get_stub_filter'; -import { Filter, FilterStateStore } from '../../../common'; +import { Filter, FilterStateStore, UI_SETTINGS } from '../../../common'; import { coreMock } from '../../../../../core/public/mocks'; import { BaseStateContainer, createStateContainer, Storage } from '../../../../kibana_utils/public'; import { QueryService, QueryStart } from '../query_service'; @@ -46,11 +46,11 @@ const startMock = coreMock.createStart(); setupMock.uiSettings.get.mockImplementation((key: string) => { switch (key) { - case 'filters:pinnedByDefault': + case UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT: return true; case 'timepicker:timeDefaults': return { from: 'now-15m', to: 'now' }; - case 'timepicker:refreshIntervalDefaults': + case UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: return { pause: false, value: 0 }; default: throw new Error(`sync_query test: not mocked uiSetting: ${key}`); diff --git a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts index 50dc35ea955ee3..77271535372579 100644 --- a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts +++ b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts @@ -21,7 +21,7 @@ import { Subscription } from 'rxjs'; import { createBrowserHistory, History } from 'history'; import { FilterManager } from '../filter_manager'; import { getFilter } from '../filter_manager/test_helpers/get_stub_filter'; -import { Filter, FilterStateStore } from '../../../common'; +import { Filter, FilterStateStore, UI_SETTINGS } from '../../../common'; import { coreMock } from '../../../../../core/public/mocks'; import { createKbnUrlStateStorage, @@ -39,11 +39,11 @@ const startMock = coreMock.createStart(); setupMock.uiSettings.get.mockImplementation((key: string) => { switch (key) { - case 'filters:pinnedByDefault': + case UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT: return true; case 'timepicker:timeDefaults': return { from: 'now-15m', to: 'now' }; - case 'timepicker:refreshIntervalDefaults': + case UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: return { pause: false, value: 0 }; default: throw new Error(`sync_query test: not mocked uiSetting: ${key}`); diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.ts b/src/plugins/data/public/query/timefilter/timefilter_service.ts index 413163ed059ad4..df2fbc8e5a8f3e 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.ts @@ -20,6 +20,7 @@ import { IUiSettingsClient } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { TimeHistory, Timefilter, TimeHistoryContract, TimefilterContract } from './index'; +import { UI_SETTINGS } from '../../../common'; /** * Filter Service @@ -35,7 +36,7 @@ export class TimefilterService { public setup({ uiSettings, storage }: TimeFilterServiceDependencies): TimefilterSetup { const timefilterConfig = { timeDefaults: uiSettings.get('timepicker:timeDefaults'), - refreshIntervalDefaults: uiSettings.get('timepicker:refreshIntervalDefaults'), + refreshIntervalDefaults: uiSettings.get(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS), }; const history = new TimeHistory(storage); const timefilter = new Timefilter(timefilterConfig, history); diff --git a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts index d5c97d0c95c5cf..8a5596f669cb78 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts @@ -31,7 +31,7 @@ import { dateHistogramInterval, TimeRange } from '../../../../common'; import { writeParams } from '../agg_params'; import { isMetricAggType } from '../metrics/metric_agg_type'; -import { FIELD_FORMAT_IDS, KBN_FIELD_TYPES } from '../../../../common'; +import { FIELD_FORMAT_IDS, KBN_FIELD_TYPES, UI_SETTINGS } from '../../../../common'; import { TimefilterContract } from '../../../query'; import { QuerySetup } from '../../../query/query_service'; import { GetInternalStartServicesFn } from '../../../types'; @@ -125,8 +125,8 @@ export const getDateHistogramBucketAgg = ({ const { timefilter } = query.timefilter; buckets = new TimeBuckets({ - 'histogram:maxBars': uiSettings.get('histogram:maxBars'), - 'histogram:barTarget': uiSettings.get('histogram:barTarget'), + 'histogram:maxBars': uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), + 'histogram:barTarget': uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), dateFormat: uiSettings.get('dateFormat'), 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), }); diff --git a/src/plugins/data/public/search/aggs/buckets/filters.ts b/src/plugins/data/public/search/aggs/buckets/filters.ts index 3f3f13bb955c18..4052c0b3901556 100644 --- a/src/plugins/data/public/search/aggs/buckets/filters.ts +++ b/src/plugins/data/public/search/aggs/buckets/filters.ts @@ -26,7 +26,7 @@ import { toAngularJSON } from '../utils'; import { BucketAggType } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; import { Storage } from '../../../../../../plugins/kibana_utils/public'; -import { getEsQueryConfig, buildEsQuery, Query } from '../../../../common'; +import { getEsQueryConfig, buildEsQuery, Query, UI_SETTINGS } from '../../../../common'; import { getQueryLog } from '../../../query'; import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; @@ -69,7 +69,10 @@ export const getFiltersBucketAgg = ({ { name: 'filters', default: [ - { input: { query: '', language: uiSettings.get('search:queryLanguage') }, label: '' }, + { + input: { query: '', language: uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE) }, + label: '', + }, ], write(aggConfig, output) { const inFilters: FilterValue[] = aggConfig.params.filters; diff --git a/src/plugins/data/public/search/aggs/buckets/histogram.ts b/src/plugins/data/public/search/aggs/buckets/histogram.ts index d04df4f8aac6ba..c1fad17f488db2 100644 --- a/src/plugins/data/public/search/aggs/buckets/histogram.ts +++ b/src/plugins/data/public/search/aggs/buckets/histogram.ts @@ -24,7 +24,7 @@ import { IUiSettingsClient } from 'src/core/public'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { createFilterHistogram } from './create_filter/histogram'; import { BUCKET_TYPES } from './bucket_agg_types'; -import { KBN_FIELD_TYPES } from '../../../../common'; +import { KBN_FIELD_TYPES, UI_SETTINGS } from '../../../../common'; import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; import { ExtendedBounds } from './lib/extended_bounds'; @@ -155,8 +155,8 @@ export const getHistogramBucketAgg = ({ const range = autoBounds.max - autoBounds.min; const bars = range / interval; - if (bars > uiSettings.get('histogram:maxBars')) { - const minInterval = range / uiSettings.get('histogram:maxBars'); + if (bars > uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS)) { + const minInterval = range / uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS); // Round interval by order of magnitude to provide clean intervals // Always round interval up so there will always be less buckets than histogram:maxBars diff --git a/src/plugins/data/public/search/aggs/utils/calculate_auto_time_expression.ts b/src/plugins/data/public/search/aggs/utils/calculate_auto_time_expression.ts index 9d976784329ccf..30fcdd9d83a389 100644 --- a/src/plugins/data/public/search/aggs/utils/calculate_auto_time_expression.ts +++ b/src/plugins/data/public/search/aggs/utils/calculate_auto_time_expression.ts @@ -19,7 +19,7 @@ import moment from 'moment'; import { IUiSettingsClient } from 'src/core/public'; import { TimeBuckets } from '../buckets/lib/time_buckets'; -import { toAbsoluteDates, TimeRange } from '../../../../common'; +import { toAbsoluteDates, TimeRange, UI_SETTINGS } from '../../../../common'; export function getCalculateAutoTimeExpression(uiSettings: IUiSettingsClient) { return function calculateAutoTimeExpression(range: TimeRange) { @@ -29,8 +29,8 @@ export function getCalculateAutoTimeExpression(uiSettings: IUiSettingsClient) { } const buckets = new TimeBuckets({ - 'histogram:maxBars': uiSettings.get('histogram:maxBars'), - 'histogram:barTarget': uiSettings.get('histogram:barTarget'), + 'histogram:maxBars': uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), + 'histogram:barTarget': uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), dateFormat: uiSettings.get('dateFormat'), 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), }); diff --git a/src/plugins/data/public/search/es_search/get_es_preference.test.ts b/src/plugins/data/public/search/es_search/get_es_preference.test.ts index 8b8156b4519d64..05a74b3e6205ac 100644 --- a/src/plugins/data/public/search/es_search/get_es_preference.test.ts +++ b/src/plugins/data/public/search/es_search/get_es_preference.test.ts @@ -20,6 +20,7 @@ import { getEsPreference } from './get_es_preference'; import { CoreStart } from '../../../../../core/public'; import { coreMock } from '../../../../../core/public/mocks'; +import { UI_SETTINGS } from '../../../common'; describe('Get ES preference', () => { let mockCoreStart: MockedKeys; @@ -30,8 +31,8 @@ describe('Get ES preference', () => { test('returns the session ID if set to sessionId', () => { mockCoreStart.uiSettings.get.mockImplementation((key: string) => { - if (key === 'courier:setRequestPreference') return 'sessionId'; - if (key === 'courier:customRequestPreference') return 'foobar'; + if (key === UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE) return 'sessionId'; + if (key === UI_SETTINGS.COURIER_CUSTOM_REQUEST_PREFERENCE) return 'foobar'; }); const preference = getEsPreference(mockCoreStart.uiSettings, 'my_session_id'); expect(preference).toBe('my_session_id'); @@ -39,8 +40,8 @@ describe('Get ES preference', () => { test('returns the custom preference if set to custom', () => { mockCoreStart.uiSettings.get.mockImplementation((key: string) => { - if (key === 'courier:setRequestPreference') return 'custom'; - if (key === 'courier:customRequestPreference') return 'foobar'; + if (key === UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE) return 'custom'; + if (key === UI_SETTINGS.COURIER_CUSTOM_REQUEST_PREFERENCE) return 'foobar'; }); const preference = getEsPreference(mockCoreStart.uiSettings); expect(preference).toBe('foobar'); @@ -48,8 +49,8 @@ describe('Get ES preference', () => { test('returns undefined if set to none', () => { mockCoreStart.uiSettings.get.mockImplementation((key: string) => { - if (key === 'courier:setRequestPreference') return 'none'; - if (key === 'courier:customRequestPreference') return 'foobar'; + if (key === UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE) return 'none'; + if (key === UI_SETTINGS.COURIER_CUSTOM_REQUEST_PREFERENCE) return 'foobar'; }); const preference = getEsPreference(mockCoreStart.uiSettings); expect(preference).toBe(undefined); diff --git a/src/plugins/data/public/search/es_search/get_es_preference.ts b/src/plugins/data/public/search/es_search/get_es_preference.ts index 3f1c2b9b3b7369..5e40712067bb02 100644 --- a/src/plugins/data/public/search/es_search/get_es_preference.ts +++ b/src/plugins/data/public/search/es_search/get_es_preference.ts @@ -18,12 +18,13 @@ */ import { IUiSettingsClient } from '../../../../../core/public'; +import { UI_SETTINGS } from '../../../common'; const defaultSessionId = `${Date.now()}`; export function getEsPreference(uiSettings: IUiSettingsClient, sessionId = defaultSessionId) { - const setPreference = uiSettings.get('courier:setRequestPreference'); + const setPreference = uiSettings.get(UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE); if (setPreference === 'sessionId') return `${sessionId}`; - const customPreference = uiSettings.get('courier:customRequestPreference'); + const customPreference = uiSettings.get(UI_SETTINGS.COURIER_CUSTOM_REQUEST_PREFERENCE); return setPreference === 'custom' ? customPreference : undefined; } diff --git a/src/plugins/data/public/search/fetch/get_search_params.test.ts b/src/plugins/data/public/search/fetch/get_search_params.test.ts index 4809d76a46f59f..f9b62fdd4fc613 100644 --- a/src/plugins/data/public/search/fetch/get_search_params.test.ts +++ b/src/plugins/data/public/search/fetch/get_search_params.test.ts @@ -19,6 +19,7 @@ import { getSearchParams } from './get_search_params'; import { IUiSettingsClient } from 'kibana/public'; +import { UI_SETTINGS } from '../../../common'; function getConfigStub(config: any = {}) { return { @@ -40,21 +41,21 @@ describe('getSearchParams', () => { }); test('includes ignore_throttled according to search:includeFrozen', () => { - let config = getConfigStub({ 'search:includeFrozen': true }); + let config = getConfigStub({ [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: true }); let searchParams = getSearchParams(config); expect(searchParams.ignore_throttled).toBe(false); - config = getConfigStub({ 'search:includeFrozen': false }); + config = getConfigStub({ [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false }); searchParams = getSearchParams(config); expect(searchParams.ignore_throttled).toBe(true); }); test('includes max_concurrent_shard_requests according to courier:maxConcurrentShardRequests', () => { - let config = getConfigStub({ 'courier:maxConcurrentShardRequests': 0 }); + let config = getConfigStub({ [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 0 }); let searchParams = getSearchParams(config); expect(searchParams.max_concurrent_shard_requests).toBe(undefined); - config = getConfigStub({ 'courier:maxConcurrentShardRequests': 5 }); + config = getConfigStub({ [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 5 }); searchParams = getSearchParams(config); expect(searchParams.max_concurrent_shard_requests).toBe(5); }); diff --git a/src/plugins/data/public/search/fetch/get_search_params.ts b/src/plugins/data/public/search/fetch/get_search_params.ts index f0c43bd2e74cd1..60bdc9ed6473ad 100644 --- a/src/plugins/data/public/search/fetch/get_search_params.ts +++ b/src/plugins/data/public/search/fetch/get_search_params.ts @@ -18,6 +18,7 @@ */ import { IUiSettingsClient } from 'kibana/public'; +import { UI_SETTINGS } from '../../../common'; const sessionId = Date.now(); @@ -33,19 +34,19 @@ export function getSearchParams(config: IUiSettingsClient, esShardTimeout: numbe } export function getIgnoreThrottled(config: IUiSettingsClient) { - return !config.get('search:includeFrozen'); + return !config.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); } export function getMaxConcurrentShardRequests(config: IUiSettingsClient) { - const maxConcurrentShardRequests = config.get('courier:maxConcurrentShardRequests'); + const maxConcurrentShardRequests = config.get(UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS); return maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined; } export function getPreference(config: IUiSettingsClient) { - const setRequestPreference = config.get('courier:setRequestPreference'); + const setRequestPreference = config.get(UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE); if (setRequestPreference === 'sessionId') return sessionId; return setRequestPreference === 'custom' - ? config.get('courier:customRequestPreference') + ? config.get(UI_SETTINGS.COURIER_CUSTOM_REQUEST_PREFERENCE) : undefined; } diff --git a/src/plugins/data/public/search/legacy/default_search_strategy.test.ts b/src/plugins/data/public/search/legacy/default_search_strategy.test.ts index c619c9b17d9a84..436b522744622d 100644 --- a/src/plugins/data/public/search/legacy/default_search_strategy.test.ts +++ b/src/plugins/data/public/search/legacy/default_search_strategy.test.ts @@ -21,6 +21,7 @@ import { IUiSettingsClient } from 'kibana/public'; import { defaultSearchStrategy } from './default_search_strategy'; import { searchStartMock } from '../mocks'; import { SearchStrategySearchParams } from './types'; +import { UI_SETTINGS } from '../../../common'; const { search } = defaultSearchStrategy; @@ -69,30 +70,30 @@ describe('defaultSearchStrategy', function () { }); test('does not send max_concurrent_shard_requests by default', async () => { - const config = getConfigStub({ 'courier:batchSearches': true }); + const config = getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }); await search({ ...searchArgs, config }); expect(es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(undefined); }); test('allows configuration of max_concurrent_shard_requests', async () => { const config = getConfigStub({ - 'courier:batchSearches': true, - 'courier:maxConcurrentShardRequests': 42, + [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true, + [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 42, }); await search({ ...searchArgs, config }); expect(es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(42); }); test('should set rest_total_hits_as_int to true on a request', async () => { - const config = getConfigStub({ 'courier:batchSearches': true }); + const config = getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }); await search({ ...searchArgs, config }); expect(es.msearch.mock.calls[0][0]).toHaveProperty('rest_total_hits_as_int', true); }); test('should set ignore_throttled=false when including frozen indices', async () => { const config = getConfigStub({ - 'courier:batchSearches': true, - 'search:includeFrozen': true, + [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true, + [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: true, }); await search({ ...searchArgs, config }); expect(es.msearch.mock.calls[0][0]).toHaveProperty('ignore_throttled', false); @@ -100,7 +101,7 @@ describe('defaultSearchStrategy', function () { test('should properly call abort with msearch', () => { const config = getConfigStub({ - 'courier:batchSearches': true, + [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true, }); search({ ...searchArgs, config }).abort(); expect(msearchMockResponse.abort).toHaveBeenCalled(); diff --git a/src/plugins/data/public/search/legacy/fetch_soon.test.ts b/src/plugins/data/public/search/legacy/fetch_soon.test.ts index e99e13ba33d1a6..61d3568350b6b1 100644 --- a/src/plugins/data/public/search/legacy/fetch_soon.test.ts +++ b/src/plugins/data/public/search/legacy/fetch_soon.test.ts @@ -22,6 +22,7 @@ import { callClient } from './call_client'; import { IUiSettingsClient } from 'kibana/public'; import { FetchHandlers, FetchOptions } from '../fetch/types'; import { SearchRequest, SearchResponse } from '../index'; +import { UI_SETTINGS } from '../../../common'; function getConfigStub(config: any = {}) { return { @@ -60,7 +61,7 @@ describe('fetchSoon', () => { test('should execute asap if config is set to not batch searches', () => { const config = getConfigStub({ - 'courier:batchSearches': false, + [UI_SETTINGS.COURIER_BATCH_SEARCHES]: false, }); const request = {}; const options = {}; @@ -72,7 +73,7 @@ describe('fetchSoon', () => { test('should delay by 50ms if config is set to batch searches', () => { const config = getConfigStub({ - 'courier:batchSearches': true, + [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true, }); const request = {}; const options = {}; @@ -88,7 +89,7 @@ describe('fetchSoon', () => { test('should send a batch of requests to callClient', () => { const config = getConfigStub({ - 'courier:batchSearches': true, + [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true, }); const requests = [{ foo: 1 }, { foo: 2 }]; const options = [{ bar: 1 }, { bar: 2 }]; @@ -105,7 +106,7 @@ describe('fetchSoon', () => { test('should return the response to the corresponding call for multiple batched requests', async () => { const config = getConfigStub({ - 'courier:batchSearches': true, + [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true, }); const requests = [{ _mockResponseId: 'foo' }, { _mockResponseId: 'bar' }]; @@ -120,7 +121,7 @@ describe('fetchSoon', () => { test('should wait for the previous batch to start before starting a new batch', () => { const config = getConfigStub({ - 'courier:batchSearches': true, + [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true, }); const firstBatch = [{ foo: 1 }, { foo: 2 }]; const secondBatch = [{ bar: 1 }, { bar: 2 }]; diff --git a/src/plugins/data/public/search/legacy/fetch_soon.ts b/src/plugins/data/public/search/legacy/fetch_soon.ts index 304c1c4d63f5bb..fed2c52bc491f8 100644 --- a/src/plugins/data/public/search/legacy/fetch_soon.ts +++ b/src/plugins/data/public/search/legacy/fetch_soon.ts @@ -20,6 +20,7 @@ import { callClient } from './call_client'; import { FetchHandlers, FetchOptions } from '../fetch/types'; import { SearchRequest, SearchResponse } from '../index'; +import { UI_SETTINGS } from '../../../common'; /** * This function introduces a slight delay in the request process to allow multiple requests to queue @@ -30,7 +31,7 @@ export async function fetchSoon( options: FetchOptions, fetchHandlers: FetchHandlers ) { - const msToDelay = fetchHandlers.config.get('courier:batchSearches') ? 50 : 0; + const msToDelay = fetchHandlers.config.get(UI_SETTINGS.COURIER_BATCH_SEARCHES) ? 50 : 0; return delayedFetch(request, options, fetchHandlers, msToDelay); } diff --git a/src/plugins/data/public/search/legacy/get_msearch_params.test.ts b/src/plugins/data/public/search/legacy/get_msearch_params.test.ts index ce98f6ab2a7bbc..dc61e194066314 100644 --- a/src/plugins/data/public/search/legacy/get_msearch_params.test.ts +++ b/src/plugins/data/public/search/legacy/get_msearch_params.test.ts @@ -19,6 +19,7 @@ import { getMSearchParams } from './get_msearch_params'; import { IUiSettingsClient } from '../../../../../core/public'; +import { UI_SETTINGS } from '../../../common'; function getConfigStub(config: any = {}) { return { @@ -34,29 +35,29 @@ describe('getMSearchParams', () => { }); test('includes ignore_throttled according to search:includeFrozen', () => { - let config = getConfigStub({ 'search:includeFrozen': true }); + let config = getConfigStub({ [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: true }); let msearchParams = getMSearchParams(config); expect(msearchParams.ignore_throttled).toBe(false); - config = getConfigStub({ 'search:includeFrozen': false }); + config = getConfigStub({ [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false }); msearchParams = getMSearchParams(config); expect(msearchParams.ignore_throttled).toBe(true); }); test('includes max_concurrent_shard_requests according to courier:maxConcurrentShardRequests if greater than 0', () => { - let config = getConfigStub({ 'courier:maxConcurrentShardRequests': 0 }); + let config = getConfigStub({ [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 0 }); let msearchParams = getMSearchParams(config); expect(msearchParams.max_concurrent_shard_requests).toBe(undefined); - config = getConfigStub({ 'courier:maxConcurrentShardRequests': 5 }); + config = getConfigStub({ [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 5 }); msearchParams = getMSearchParams(config); expect(msearchParams.max_concurrent_shard_requests).toBe(5); }); test('does not include other search params that are included in the msearch header or body', () => { const config = getConfigStub({ - 'search:includeFrozen': false, - 'courier:maxConcurrentShardRequests': 5, + [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false, + [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 5, }); const msearchParams = getMSearchParams(config); expect(msearchParams.hasOwnProperty('ignore_unavailable')).toBe(false); diff --git a/src/plugins/data/public/search/search_source/search_source.ts b/src/plugins/data/public/search/search_source/search_source.ts index 38f4ce73713c88..b926739112e0e8 100644 --- a/src/plugins/data/public/search/search_source/search_source.ts +++ b/src/plugins/data/public/search/search_source/search_source.ts @@ -75,12 +75,11 @@ import { CoreStart } from 'kibana/public'; import { normalizeSortRequest } from './normalize_sort_request'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../kibana_utils/public'; -import { META_FIELDS_SETTING, DOC_HIGHLIGHT_SETTING } from '../../../common'; import { IIndexPattern, ISearchGeneric, SearchRequest } from '../..'; import { SearchSourceOptions, SearchSourceFields } from './types'; import { FetchOptions, RequestFailure, getSearchParams, handleResponse } from '../fetch'; -import { getEsQueryConfig, buildEsQuery, Filter } from '../../../common'; +import { getEsQueryConfig, buildEsQuery, Filter, UI_SETTINGS } from '../../../common'; import { getHighlightRequest } from '../../../common/field_formats'; import { fetchSoon } from '../legacy'; import { extractReferences } from './extract_references'; @@ -251,7 +250,7 @@ export class SearchSource { this.history = [searchRequest]; let response; - if (uiSettings.get('courier:batchSearches')) { + if (uiSettings.get(UI_SETTINGS.COURIER_BATCH_SEARCHES)) { response = await this.legacyFetch(searchRequest, options); } else { response = this.fetch$(searchRequest, options.abortSignal).toPromise(); @@ -365,7 +364,7 @@ export class SearchSource { const sort = normalizeSortRequest( val, this.getField('index'), - uiSettings.get('sort:options') + uiSettings.get(UI_SETTINGS.SORT_OPTIONS) ); return addToBody(key, sort); default: @@ -425,7 +424,7 @@ export class SearchSource { // exclude source fields for this index pattern specified by the user const filter = fieldWildcardFilter( body._source.excludes, - uiSettings.get(META_FIELDS_SETTING) + uiSettings.get(UI_SETTINGS.META_FIELDS) ); body.docvalue_fields = body.docvalue_fields.filter((docvalueField: any) => filter(docvalueField.field) @@ -448,7 +447,7 @@ export class SearchSource { body.query = buildEsQuery(index, query, filters, esQueryConfigs); if (highlightAll && body.query) { - body.highlight = getHighlightRequest(body.query, uiSettings.get(DOC_HIGHLIGHT_SETTING)); + body.highlight = getHighlightRequest(body.query, uiSettings.get(UI_SETTINGS.DOC_HIGHLIGHT)); delete searchRequest.highlightAll; } diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index a54a25acc59134..43dba150bf8d44 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -36,6 +36,7 @@ import { toggleFilterDisabled, toggleFilterNegated, unpinFilter, + UI_SETTINGS, } from '../../../common'; interface Props { @@ -76,7 +77,7 @@ function FilterBarUI(props: Props) { } function renderAddFilter() { - const isPinned = uiSettings!.get('filters:pinnedByDefault'); + const isPinned = uiSettings!.get(UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT); const [indexPattern] = props.indexPatterns; const index = indexPattern && indexPattern.id; const newFilter = buildEmptyFilter(isPinned, index); diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx index 546365b89d9be1..94138f60b52b1f 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx @@ -22,6 +22,7 @@ import { debounce } from 'lodash'; import { withKibana, KibanaReactContextValue } from '../../../../../kibana_react/public'; import { IDataPluginServices, IIndexPattern, IFieldType } from '../../..'; +import { UI_SETTINGS } from '../../../../common'; export interface PhraseSuggestorProps { kibana: KibanaReactContextValue; @@ -54,7 +55,9 @@ export class PhraseSuggestorUI extends React.Com } protected isSuggestingValues() { - const shouldSuggestValues = this.services.uiSettings.get('filterEditor:suggestValues'); + const shouldSuggestValues = this.services.uiSettings.get( + UI_SETTINGS.FILTERS_EDITOR_SUGGEST_VALUES + ); const { field } = this.props; return shouldSuggestValues && field && field.aggregatable && field.type === 'string'; } diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index c44e1faeb8e7f4..053fca7d5773bc 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -74,7 +74,8 @@ export function FilterItem(props: Props) { setIndexPatternExists(false); }); } else { - setIndexPatternExists(false); + // Allow filters without an index pattern and don't validate them. + setIndexPatternExists(true); } }, [props.filter.meta.index]); @@ -244,6 +245,9 @@ export function FilterItem(props: Props) { * This function makes this behavior explicit, but it needs to be revised. */ function isFilterApplicable() { + // Any filter is applicable if no index patterns were provided to FilterBar. + if (!props.indexPatterns.length) return true; + const ip = getIndexPatternFromFilter(filter, indexPatterns); if (ip) return true; diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx index f579adbc0c7e27..5f2d4c00cd6b6f 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx @@ -28,6 +28,7 @@ import { dataPluginMock } from '../../mocks'; import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; import { I18nProvider } from '@kbn/i18n/react'; import { stubIndexPatternWithFields } from '../../stubs'; +import { UI_SETTINGS } from '../../../common'; const startMock = coreMock.createStart(); const mockTimeHistory = { @@ -38,7 +39,7 @@ const mockTimeHistory = { startMock.uiSettings.get.mockImplementation((key: string) => { switch (key) { - case 'timepicker:quickRanges': + case UI_SETTINGS.TIMEPICKER_QUICK_RANGES: return [ { from: 'now/d', @@ -48,7 +49,7 @@ startMock.uiSettings.get.mockImplementation((key: string) => { ]; case 'dateFormat': return 'MMM D, YYYY @ HH:mm:ss.SSS'; - case 'history:limit': + case UI_SETTINGS.HISTORY_LIMIT: return 10; case 'timepicker:timeDefaults': return { diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index 433cb652ee5ce6..f65bf97e391e22 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -38,7 +38,7 @@ import { Toast } from 'src/core/public'; import { IDataPluginServices, IIndexPattern, TimeRange, TimeHistoryContract, Query } from '../..'; import { useKibana, toMountPoint } from '../../../../kibana_react/public'; import { QueryStringInput } from './query_string_input'; -import { doesKueryExpressionHaveLuceneSyntaxError } from '../../../common'; +import { doesKueryExpressionHaveLuceneSyntaxError, UI_SETTINGS } from '../../../common'; import { PersistedLog, getQueryLog } from '../../query'; interface Props { @@ -255,7 +255,7 @@ export function QueryBarTopRow(props: Props) { } const commonlyUsedRanges = uiSettings! - .get('timepicker:quickRanges') + .get(UI_SETTINGS.TIMEPICKER_QUICK_RANGES) .map(({ from, to, display }: { from: string; to: string; display: string }) => { return { start: from, diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 7723254f3aa51e..18ed632e0a8ecc 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -27,7 +27,7 @@ import { useFilterManager } from './lib/use_filter_manager'; import { useTimefilter } from './lib/use_timefilter'; import { useSavedQuery } from './lib/use_saved_query'; import { DataPublicPluginStart } from '../../types'; -import { Filter, Query, TimeRange } from '../../../common'; +import { Filter, Query, TimeRange, UI_SETTINGS } from '../../../common'; interface StatefulSearchBarDeps { core: CoreStart; @@ -125,7 +125,8 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) const defaultQuery = { query: '', language: - storage.get('kibana.userQueryLanguage') || core.uiSettings.get('search:queryLanguage'), + storage.get('kibana.userQueryLanguage') || + core.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE), }; const [query, setQuery] = useState(props.query || defaultQuery); diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 72a29e377ac54e..831d23864d2287 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -146,6 +146,7 @@ export { ES_FIELD_TYPES, KBN_FIELD_TYPES, IndexPatternAttributes, + UI_SETTINGS, } from '../common'; /** diff --git a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts index 446320b09757a5..81e352fea51b26 100644 --- a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts +++ b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts @@ -22,6 +22,9 @@ import { APICaller } from 'kibana/server'; jest.mock('../../../common', () => ({ DEFAULT_QUERY_LANGUAGE: 'lucene', + UI_SETTINGS: { + SEARCH_QUERY_LANGUAGE: 'search:queryLanguage', + }, })); let fetch: ReturnType; diff --git a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts index 9f3437161541f1..157716b38f5234 100644 --- a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts +++ b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts @@ -19,7 +19,7 @@ import { get } from 'lodash'; import { APICaller } from 'kibana/server'; -import { DEFAULT_QUERY_LANGUAGE } from '../../../common'; +import { DEFAULT_QUERY_LANGUAGE, UI_SETTINGS } from '../../../common'; const defaultSearchQueryLanguageSetting = DEFAULT_QUERY_LANGUAGE; @@ -40,7 +40,7 @@ export function fetchProvider(index: string) { const queryLanguageConfigValue = get( config, - 'hits.hits[0]._source.config.search:queryLanguage' + `hits.hits[0]._source.config.${UI_SETTINGS.SEARCH_QUERY_LANGUAGE}` ); // search:queryLanguage can potentially be in four states in the .kibana index: diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index df7a7b9cf4d011..8c9d0df2ed8947 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -28,7 +28,7 @@ import { KqlTelemetryService } from './kql_telemetry'; import { UsageCollectionSetup } from '../../usage_collection/server'; import { AutocompleteService } from './autocomplete'; import { FieldFormatsService, FieldFormatsSetup, FieldFormatsStart } from './field_formats'; -import { uiSettings } from './ui_settings'; +import { getUiSettings } from './ui_settings'; export interface DataPluginSetup { search: ISearchSetup; @@ -65,7 +65,8 @@ export class DataServerPlugin implements Plugin = (context: ISearchContext, caller: APICaller_2, search: ISearchGeneric) => ISearchStrategy; +// Warning: (ae-missing-release-tag) "UI_SETTINGS" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const UI_SETTINGS: { + META_FIELDS: string; + DOC_HIGHLIGHT: string; + QUERY_STRING_OPTIONS: string; + QUERY_ALLOW_LEADING_WILDCARDS: string; + SEARCH_QUERY_LANGUAGE: string; + SORT_OPTIONS: string; + COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string; + COURIER_SET_REQUEST_PREFERENCE: string; + COURIER_CUSTOM_REQUEST_PREFERENCE: string; + COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string; + COURIER_BATCH_SEARCHES: string; + SEARCH_INCLUDE_FROZEN: string; + HISTOGRAM_BAR_TARGET: string; + HISTOGRAM_MAX_BARS: string; + HISTORY_LIMIT: string; + SHORT_DOTS_ENABLE: string; + FORMAT_DEFAULT_TYPE_MAP: string; + FORMAT_NUMBER_DEFAULT_PATTERN: string; + FORMAT_PERCENT_DEFAULT_PATTERN: string; + FORMAT_BYTES_DEFAULT_PATTERN: string; + FORMAT_CURRENCY_DEFAULT_PATTERN: string; + FORMAT_NUMBER_DEFAULT_LOCALE: string; + TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string; + TIMEPICKER_QUICK_RANGES: string; + INDEXPATTERN_PLACEHOLDER: string; + FILTERS_PINNED_BY_DEFAULT: string; + FILTERS_EDITOR_SUGGEST_VALUES: string; +}; + // Warnings were encountered during analysis: // @@ -745,12 +778,12 @@ export type TSearchStrategyProvider = (context: ISearc // src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:131:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:131:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:183:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:184:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:185:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:186:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:187:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:190:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:184:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:185:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:186:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:187:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:188:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:191:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:66:14 - (ae-forgotten-export) The symbol "ISearchSetup" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index 5af62be2952011..de978c7968aee5 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -19,33 +19,652 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; - import { UiSettingsParams } from 'kibana/server'; -import { META_FIELDS_SETTING, DOC_HIGHLIGHT_SETTING } from '../common'; +// @ts-ignore untyped module +import numeralLanguages from '@elastic/numeral/languages'; +import { DEFAULT_QUERY_LANGUAGE, UI_SETTINGS } from '../common'; + +const luceneQueryLanguageLabel = i18n.translate('data.advancedSettings.searchQueryLanguageLucene', { + defaultMessage: 'Lucene', +}); + +const queryLanguageSettingName = i18n.translate('data.advancedSettings.searchQueryLanguageTitle', { + defaultMessage: 'Query language', +}); -export const uiSettings: Record = { - [META_FIELDS_SETTING]: { - name: i18n.translate('data.advancedSettings.metaFieldsTitle', { - defaultMessage: 'Meta fields', - }), - value: ['_source', '_id', '_type', '_index', '_score'], - description: i18n.translate('data.advancedSettings.metaFieldsText', { - defaultMessage: - 'Fields that exist outside of _source to merge into our document when displaying it', - }), - schema: schema.arrayOf(schema.string()), - }, - [DOC_HIGHLIGHT_SETTING]: { - name: i18n.translate('data.advancedSettings.docTableHighlightTitle', { - defaultMessage: 'Highlight results', - }), - value: true, - description: i18n.translate('data.advancedSettings.docTableHighlightText', { - defaultMessage: - 'Highlight results in Discover and Saved Searches Dashboard. ' + - 'Highlighting makes requests slow when working on big documents.', - }), - category: ['discover'], - schema: schema.boolean(), - }, +const requestPreferenceOptionLabels = { + sessionId: i18n.translate('data.advancedSettings.courier.requestPreferenceSessionId', { + defaultMessage: 'Session ID', + }), + custom: i18n.translate('data.advancedSettings.courier.requestPreferenceCustom', { + defaultMessage: 'Custom', + }), + none: i18n.translate('data.advancedSettings.courier.requestPreferenceNone', { + defaultMessage: 'None', + }), }; + +// We add the `en` key manually here, since that's not a real numeral locale, but the +// default fallback in case the locale is not found. +const numeralLanguageIds = [ + 'en', + ...numeralLanguages.map((numeralLanguage: any) => { + return numeralLanguage.id; + }), +]; + +export function getUiSettings(): Record> { + return { + [UI_SETTINGS.META_FIELDS]: { + name: i18n.translate('data.advancedSettings.metaFieldsTitle', { + defaultMessage: 'Meta fields', + }), + value: ['_source', '_id', '_type', '_index', '_score'], + description: i18n.translate('data.advancedSettings.metaFieldsText', { + defaultMessage: + 'Fields that exist outside of _source to merge into our document when displaying it', + }), + schema: schema.arrayOf(schema.string()), + }, + [UI_SETTINGS.DOC_HIGHLIGHT]: { + name: i18n.translate('data.advancedSettings.docTableHighlightTitle', { + defaultMessage: 'Highlight results', + }), + value: true, + description: i18n.translate('data.advancedSettings.docTableHighlightText', { + defaultMessage: + 'Highlight results in Discover and Saved Searches Dashboard. ' + + 'Highlighting makes requests slow when working on big documents.', + }), + category: ['discover'], + schema: schema.boolean(), + }, + [UI_SETTINGS.QUERY_STRING_OPTIONS]: { + name: i18n.translate('data.advancedSettings.query.queryStringOptionsTitle', { + defaultMessage: 'Query string options', + }), + value: '{ "analyze_wildcard": true }', + description: i18n.translate('data.advancedSettings.query.queryStringOptionsText', { + defaultMessage: + '{optionsLink} for the lucene query string parser. Is only used when "{queryLanguage}" is set ' + + 'to {luceneLanguage}.', + description: + 'Part of composite text: data.advancedSettings.query.queryStringOptions.optionsLinkText + ' + + 'data.advancedSettings.query.queryStringOptionsText', + values: { + optionsLink: + '' + + i18n.translate('data.advancedSettings.query.queryStringOptions.optionsLinkText', { + defaultMessage: 'Options', + }) + + '', + luceneLanguage: luceneQueryLanguageLabel, + queryLanguage: queryLanguageSettingName, + }, + }), + type: 'json', + schema: schema.object({ + analyze_wildcard: schema.boolean(), + }), + }, + [UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS]: { + name: i18n.translate('data.advancedSettings.query.allowWildcardsTitle', { + defaultMessage: 'Allow leading wildcards in query', + }), + value: true, + description: i18n.translate('data.advancedSettings.query.allowWildcardsText', { + defaultMessage: + 'When set, * is allowed as the first character in a query clause. ' + + 'Currently only applies when experimental query features are enabled in the query bar. ' + + 'To disallow leading wildcards in basic lucene queries, use {queryStringOptionsPattern}.', + values: { + queryStringOptionsPattern: UI_SETTINGS.QUERY_STRING_OPTIONS, + }, + }), + schema: schema.boolean(), + }, + [UI_SETTINGS.SEARCH_QUERY_LANGUAGE]: { + name: queryLanguageSettingName, + value: DEFAULT_QUERY_LANGUAGE, + description: i18n.translate('data.advancedSettings.searchQueryLanguageText', { + defaultMessage: + 'Query language used by the query bar. KQL is a new language built specifically for Kibana.', + }), + type: 'select', + options: ['lucene', 'kuery'], + optionLabels: { + lucene: luceneQueryLanguageLabel, + kuery: i18n.translate('data.advancedSettings.searchQueryLanguageKql', { + defaultMessage: 'KQL', + }), + }, + schema: schema.string(), + }, + [UI_SETTINGS.SORT_OPTIONS]: { + name: i18n.translate('data.advancedSettings.sortOptionsTitle', { + defaultMessage: 'Sort options', + }), + value: '{ "unmapped_type": "boolean" }', + description: i18n.translate('data.advancedSettings.sortOptionsText', { + defaultMessage: '{optionsLink} for the Elasticsearch sort parameter', + description: + 'Part of composite text: data.advancedSettings.sortOptions.optionsLinkText + ' + + 'data.advancedSettings.sortOptionsText', + values: { + optionsLink: + '' + + i18n.translate('data.advancedSettings.sortOptions.optionsLinkText', { + defaultMessage: 'Options', + }) + + '', + }, + }), + type: 'json', + schema: schema.object({ + unmapped_type: schema.string(), + }), + }, + defaultIndex: { + name: i18n.translate('data.advancedSettings.defaultIndexTitle', { + defaultMessage: 'Default index', + }), + value: null, + type: 'string', + description: i18n.translate('data.advancedSettings.defaultIndexText', { + defaultMessage: 'The index to access if no index is set', + }), + schema: schema.nullable(schema.string()), + }, + [UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX]: { + name: i18n.translate('data.advancedSettings.courier.ignoreFilterTitle', { + defaultMessage: 'Ignore filter(s)', + }), + value: false, + description: i18n.translate('data.advancedSettings.courier.ignoreFilterText', { + defaultMessage: + 'This configuration enhances support for dashboards containing visualizations accessing dissimilar indexes. ' + + 'When disabled, all filters are applied to all visualizations. ' + + 'When enabled, filter(s) will be ignored for a visualization ' + + `when the visualization's index does not contain the filtering field.`, + }), + category: ['search'], + schema: schema.boolean(), + }, + [UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE]: { + name: i18n.translate('data.advancedSettings.courier.requestPreferenceTitle', { + defaultMessage: 'Request preference', + }), + value: 'sessionId', + options: ['sessionId', 'custom', 'none'], + optionLabels: requestPreferenceOptionLabels, + type: 'select', + description: i18n.translate('data.advancedSettings.courier.requestPreferenceText', { + defaultMessage: `Allows you to set which shards handle your search requests. +
    +
  • {sessionId}: restricts operations to execute all search requests on the same shards. + This has the benefit of reusing shard caches across requests.
  • +
  • {custom}: allows you to define a your own preference. + Use 'courier:customRequestPreference' to customize your preference value.
  • +
  • {none}: means do not set a preference. + This might provide better performance because requests can be spread across all shard copies. + However, results might be inconsistent because different shards might be in different refresh states.
  • +
`, + values: { + sessionId: requestPreferenceOptionLabels.sessionId, + custom: requestPreferenceOptionLabels.custom, + none: requestPreferenceOptionLabels.none, + }, + }), + category: ['search'], + schema: schema.string(), + }, + [UI_SETTINGS.COURIER_CUSTOM_REQUEST_PREFERENCE]: { + name: i18n.translate('data.advancedSettings.courier.customRequestPreferenceTitle', { + defaultMessage: 'Custom request preference', + }), + value: '_local', + type: 'string', + description: i18n.translate('data.advancedSettings.courier.customRequestPreferenceText', { + defaultMessage: + '{requestPreferenceLink} used when {setRequestReferenceSetting} is set to {customSettingValue}.', + description: + 'Part of composite text: data.advancedSettings.courier.customRequestPreference.requestPreferenceLinkText + ' + + 'data.advancedSettings.courier.customRequestPreferenceText', + values: { + setRequestReferenceSetting: `${UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE}`, + customSettingValue: '"custom"', + requestPreferenceLink: + '' + + i18n.translate( + 'data.advancedSettings.courier.customRequestPreference.requestPreferenceLinkText', + { + defaultMessage: 'Request Preference', + } + ) + + '', + }, + }), + category: ['search'], + schema: schema.string(), + }, + [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: { + name: i18n.translate('data.advancedSettings.courier.maxRequestsTitle', { + defaultMessage: 'Max Concurrent Shard Requests', + }), + value: 0, + type: 'number', + description: i18n.translate('data.advancedSettings.courier.maxRequestsText', { + defaultMessage: + 'Controls the {maxRequestsLink} setting used for _msearch requests sent by Kibana. ' + + 'Set to 0 to disable this config and use the Elasticsearch default.', + values: { + maxRequestsLink: `max_concurrent_shard_requests`, + }, + }), + category: ['search'], + schema: schema.number(), + }, + [UI_SETTINGS.COURIER_BATCH_SEARCHES]: { + name: i18n.translate('data.advancedSettings.courier.batchSearchesTitle', { + defaultMessage: 'Batch concurrent searches', + }), + value: false, + type: 'boolean', + description: i18n.translate('data.advancedSettings.courier.batchSearchesText', { + defaultMessage: `When disabled, dashboard panels will load individually, and search requests will terminate when users navigate + away or update the query. When enabled, dashboard panels will load together when all of the data is loaded, and + searches will not terminate.`, + }), + deprecation: { + message: i18n.translate('data.advancedSettings.courier.batchSearchesTextDeprecation', { + defaultMessage: 'This setting is deprecated and will be removed in Kibana 8.0.', + }), + docLinksKey: 'kibanaSearchSettings', + }, + category: ['search'], + schema: schema.boolean(), + }, + [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: { + name: 'Search in frozen indices', + description: `Will include frozen indices in results if enabled. Searching through frozen indices + might increase the search time.`, + value: false, + category: ['search'], + schema: schema.boolean(), + }, + [UI_SETTINGS.HISTOGRAM_BAR_TARGET]: { + name: i18n.translate('data.advancedSettings.histogram.barTargetTitle', { + defaultMessage: 'Target bars', + }), + value: 50, + description: i18n.translate('data.advancedSettings.histogram.barTargetText', { + defaultMessage: + 'Attempt to generate around this many bars when using "auto" interval in date histograms', + }), + schema: schema.number(), + }, + [UI_SETTINGS.HISTOGRAM_MAX_BARS]: { + name: i18n.translate('data.advancedSettings.histogram.maxBarsTitle', { + defaultMessage: 'Maximum bars', + }), + value: 100, + description: i18n.translate('data.advancedSettings.histogram.maxBarsText', { + defaultMessage: + 'Never show more than this many bars in date histograms, scale values if needed', + }), + schema: schema.number(), + }, + [UI_SETTINGS.HISTORY_LIMIT]: { + name: i18n.translate('data.advancedSettings.historyLimitTitle', { + defaultMessage: 'History limit', + }), + value: 10, + description: i18n.translate('data.advancedSettings.historyLimitText', { + defaultMessage: + 'In fields that have history (e.g. query inputs), show this many recent values', + }), + schema: schema.number(), + }, + [UI_SETTINGS.SHORT_DOTS_ENABLE]: { + name: i18n.translate('data.advancedSettings.shortenFieldsTitle', { + defaultMessage: 'Shorten fields', + }), + value: false, + description: i18n.translate('data.advancedSettings.shortenFieldsText', { + defaultMessage: 'Shorten long fields, for example, instead of foo.bar.baz, show f.b.baz', + }), + schema: schema.boolean(), + }, + [UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP]: { + name: i18n.translate('data.advancedSettings.format.defaultTypeMapTitle', { + defaultMessage: 'Field type format name', + }), + value: `{ + "ip": { "id": "ip", "params": {} }, + "date": { "id": "date", "params": {} }, + "date_nanos": { "id": "date_nanos", "params": {}, "es": true }, + "number": { "id": "number", "params": {} }, + "boolean": { "id": "boolean", "params": {} }, + "_source": { "id": "_source", "params": {} }, + "_default_": { "id": "string", "params": {} } +}`, + type: 'json', + description: i18n.translate('data.advancedSettings.format.defaultTypeMapText', { + defaultMessage: + 'Map of the format name to use by default for each field type. ' + + '{defaultFormat} is used if the field type is not mentioned explicitly', + values: { + defaultFormat: '"_default_"', + }, + }), + schema: schema.object({ + ip: schema.object({ + id: schema.string(), + params: schema.object({}), + }), + date: schema.object({ + id: schema.string(), + params: schema.object({}), + }), + date_nanos: schema.object({ + id: schema.string(), + params: schema.object({}), + es: schema.boolean(), + }), + number: schema.object({ + id: schema.string(), + params: schema.object({}), + }), + boolean: schema.object({ + id: schema.string(), + params: schema.object({}), + }), + _source: schema.object({ + id: schema.string(), + params: schema.object({}), + }), + _default_: schema.object({ + id: schema.string(), + params: schema.object({}), + }), + }), + }, + [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN]: { + name: i18n.translate('data.advancedSettings.format.numberFormatTitle', { + defaultMessage: 'Number format', + }), + value: '0,0.[000]', + type: 'string', + description: i18n.translate('data.advancedSettings.format.numberFormatText', { + defaultMessage: 'Default {numeralFormatLink} for the "number" format', + description: + 'Part of composite text: data.advancedSettings.format.numberFormatText + ' + + 'data.advancedSettings.format.numberFormat.numeralFormatLinkText', + values: { + numeralFormatLink: + '' + + i18n.translate('data.advancedSettings.format.numberFormat.numeralFormatLinkText', { + defaultMessage: 'numeral format', + }) + + '', + }, + }), + schema: schema.string(), + }, + [UI_SETTINGS.FORMAT_PERCENT_DEFAULT_PATTERN]: { + name: i18n.translate('data.advancedSettings.format.percentFormatTitle', { + defaultMessage: 'Percent format', + }), + value: '0,0.[000]%', + type: 'string', + description: i18n.translate('data.advancedSettings.format.percentFormatText', { + defaultMessage: 'Default {numeralFormatLink} for the "percent" format', + description: + 'Part of composite text: data.advancedSettings.format.percentFormatText + ' + + 'data.advancedSettings.format.percentFormat.numeralFormatLinkText', + values: { + numeralFormatLink: + '' + + i18n.translate('data.advancedSettings.format.percentFormat.numeralFormatLinkText', { + defaultMessage: 'numeral format', + }) + + '', + }, + }), + schema: schema.string(), + }, + [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: { + name: i18n.translate('data.advancedSettings.format.bytesFormatTitle', { + defaultMessage: 'Bytes format', + }), + value: '0,0.[0]b', + type: 'string', + description: i18n.translate('data.advancedSettings.format.bytesFormatText', { + defaultMessage: 'Default {numeralFormatLink} for the "bytes" format', + description: + 'Part of composite text: data.advancedSettings.format.bytesFormatText + ' + + 'data.advancedSettings.format.bytesFormat.numeralFormatLinkText', + values: { + numeralFormatLink: + '' + + i18n.translate('data.advancedSettings.format.bytesFormat.numeralFormatLinkText', { + defaultMessage: 'numeral format', + }) + + '', + }, + }), + schema: schema.string(), + }, + [UI_SETTINGS.FORMAT_CURRENCY_DEFAULT_PATTERN]: { + name: i18n.translate('data.advancedSettings.format.currencyFormatTitle', { + defaultMessage: 'Currency format', + }), + value: '($0,0.[00])', + type: 'string', + description: i18n.translate('data.advancedSettings.format.currencyFormatText', { + defaultMessage: 'Default {numeralFormatLink} for the "currency" format', + description: + 'Part of composite text: data.advancedSettings.format.currencyFormatText + ' + + 'data.advancedSettings.format.currencyFormat.numeralFormatLinkText', + values: { + numeralFormatLink: + '' + + i18n.translate('data.advancedSettings.format.currencyFormat.numeralFormatLinkText', { + defaultMessage: 'numeral format', + }) + + '', + }, + }), + schema: schema.string(), + }, + [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_LOCALE]: { + name: i18n.translate('data.advancedSettings.format.formattingLocaleTitle', { + defaultMessage: 'Formatting locale', + }), + value: 'en', + type: 'select', + options: numeralLanguageIds, + optionLabels: Object.fromEntries( + numeralLanguages.map((language: Record) => [language.id, language.name]) + ), + description: i18n.translate('data.advancedSettings.format.formattingLocaleText', { + defaultMessage: `{numeralLanguageLink} locale`, + description: + 'Part of composite text: data.advancedSettings.format.formattingLocale.numeralLanguageLinkText + ' + + 'data.advancedSettings.format.formattingLocaleText', + values: { + numeralLanguageLink: + '' + + i18n.translate( + 'data.advancedSettings.format.formattingLocale.numeralLanguageLinkText', + { + defaultMessage: 'Numeral language', + } + ) + + '', + }, + }), + schema: schema.string(), + }, + [UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS]: { + name: i18n.translate('data.advancedSettings.timepicker.refreshIntervalDefaultsTitle', { + defaultMessage: 'Time filter refresh interval', + }), + value: `{ + "pause": false, + "value": 0 +}`, + type: 'json', + description: i18n.translate('data.advancedSettings.timepicker.refreshIntervalDefaultsText', { + defaultMessage: `The timefilter's default refresh interval`, + }), + requiresPageReload: true, + schema: schema.object({ + pause: schema.boolean(), + value: schema.number(), + }), + }, + [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: { + name: i18n.translate('data.advancedSettings.timepicker.quickRangesTitle', { + defaultMessage: 'Time filter quick ranges', + }), + value: JSON.stringify( + [ + { + from: 'now/d', + to: 'now/d', + display: i18n.translate('data.advancedSettings.timepicker.today', { + defaultMessage: 'Today', + }), + }, + { + from: 'now/w', + to: 'now/w', + display: i18n.translate('data.advancedSettings.timepicker.thisWeek', { + defaultMessage: 'This week', + }), + }, + { + from: 'now-15m', + to: 'now', + display: i18n.translate('data.advancedSettings.timepicker.last15Minutes', { + defaultMessage: 'Last 15 minutes', + }), + }, + { + from: 'now-30m', + to: 'now', + display: i18n.translate('data.advancedSettings.timepicker.last30Minutes', { + defaultMessage: 'Last 30 minutes', + }), + }, + { + from: 'now-1h', + to: 'now', + display: i18n.translate('data.advancedSettings.timepicker.last1Hour', { + defaultMessage: 'Last 1 hour', + }), + }, + { + from: 'now-24h', + to: 'now', + display: i18n.translate('data.advancedSettings.timepicker.last24Hours', { + defaultMessage: 'Last 24 hours', + }), + }, + { + from: 'now-7d', + to: 'now', + display: i18n.translate('data.advancedSettings.timepicker.last7Days', { + defaultMessage: 'Last 7 days', + }), + }, + { + from: 'now-30d', + to: 'now', + display: i18n.translate('data.advancedSettings.timepicker.last30Days', { + defaultMessage: 'Last 30 days', + }), + }, + { + from: 'now-90d', + to: 'now', + display: i18n.translate('data.advancedSettings.timepicker.last90Days', { + defaultMessage: 'Last 90 days', + }), + }, + { + from: 'now-1y', + to: 'now', + display: i18n.translate('data.advancedSettings.timepicker.last1Year', { + defaultMessage: 'Last 1 year', + }), + }, + ], + null, + 2 + ), + type: 'json', + description: i18n.translate('data.advancedSettings.timepicker.quickRangesText', { + defaultMessage: + 'The list of ranges to show in the Quick section of the time filter. This should be an array of objects, ' + + 'with each object containing "from", "to" (see {acceptedFormatsLink}), and ' + + '"display" (the title to be displayed).', + description: + 'Part of composite text: data.advancedSettings.timepicker.quickRangesText + ' + + 'data.advancedSettings.timepicker.quickRanges.acceptedFormatsLinkText', + values: { + acceptedFormatsLink: + `` + + i18n.translate('data.advancedSettings.timepicker.quickRanges.acceptedFormatsLinkText', { + defaultMessage: 'accepted formats', + }) + + '', + }, + }), + schema: schema.arrayOf( + schema.object({ + from: schema.string(), + to: schema.string(), + display: schema.string(), + }) + ), + }, + [UI_SETTINGS.INDEXPATTERN_PLACEHOLDER]: { + name: i18n.translate('data.advancedSettings.indexPatternPlaceholderTitle', { + defaultMessage: 'Index pattern placeholder', + }), + value: '', + description: i18n.translate('data.advancedSettings.indexPatternPlaceholderText', { + defaultMessage: + 'The placeholder for the "Index pattern name" field in "Management > Index Patterns > Create Index Pattern".', + }), + schema: schema.string(), + }, + [UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT]: { + name: i18n.translate('data.advancedSettings.pinFiltersTitle', { + defaultMessage: 'Pin filters by default', + }), + value: false, + description: i18n.translate('data.advancedSettings.pinFiltersText', { + defaultMessage: 'Whether the filters should have a global state (be pinned) by default', + }), + schema: schema.boolean(), + }, + [UI_SETTINGS.FILTERS_EDITOR_SUGGEST_VALUES]: { + name: i18n.translate('data.advancedSettings.suggestFilterValuesTitle', { + defaultMessage: 'Filter editor suggest values', + description: '"Filter editor" refers to the UI you create filters in.', + }), + value: true, + description: i18n.translate('data.advancedSettings.suggestFilterValuesText', { + defaultMessage: + 'Set this property to false to prevent the filter editor from suggesting values for fields.', + }), + schema: schema.boolean(), + }, + }; +} diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 0b3a07e98624ef..14dd399697b568 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -1,6 +1,7 @@ { "id": "discover", "version": "kibana", + "optionalPlugins": ["share"], "server": true, "ui": true, "requiredPlugins": [ diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index caba094bd0984e..88885b3eb211ba 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -72,6 +72,7 @@ import { syncQueryStateWithUrl, getDefaultQuery, search, + UI_SETTINGS, } from '../../../../data/public'; import { getIndexPatternId } from '../helpers/get_index_pattern_id'; import { addFatalError } from '../../../../kibana_legacy/public'; @@ -592,7 +593,8 @@ function discoverController( const query = $scope.searchSource.getField('query') || getDefaultQuery( - localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage') + localStorage.get('kibana.userQueryLanguage') || + config.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE) ); return { query, diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts index 60dfb69e85e74b..82bfcc8bc42f97 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts @@ -19,6 +19,7 @@ import { TableHeader } from './table_header/table_header'; import { getServices } from '../../../../kibana_services'; import { SORT_DEFAULT_ORDER_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../../common'; +import { UI_SETTINGS } from '../../../../../../data/public'; export function createTableHeaderDirective(reactDirective: any) { const { uiSettings: config } = getServices(); @@ -38,7 +39,7 @@ export function createTableHeaderDirective(reactDirective: any) { { restrict: 'A' }, { hideTimeColumn: config.get(DOC_HIDE_TIME_COLUMN_SETTING, false), - isShortDots: config.get('shortDots:enable'), + isShortDots: config.get(UI_SETTINGS.SHORT_DOTS_ENABLE), defaultSortOrder: config.get(SORT_DEFAULT_ORDER_SETTING, 'desc'), } ); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index 99a5547ed0760b..5a319d30b2515a 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -33,6 +33,7 @@ import { IIndexPatternFieldList, IndexPatternField, IndexPattern, + UI_SETTINGS, } from '../../../../../data/public'; import { AppState } from '../../angular/discover_state'; import { getDetails } from './lib/get_details'; @@ -133,7 +134,7 @@ export function DiscoverSidebar({ ); const popularLimit = services.uiSettings.get(FIELDS_LIMIT_SETTING); - const useShortDots = services.uiSettings.get('shortDots:enable'); + const useShortDots = services.uiSettings.get(UI_SETTINGS.SHORT_DOTS_ENABLE); const { selected: selectedFields, diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index 359d91325f064e..4154fdfeb3ff48 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -27,3 +27,4 @@ export function plugin(initializerContext: PluginInitializerContext) { export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches'; export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable'; +export { DISCOVER_APP_URL_GENERATOR } from './url_generator'; diff --git a/src/plugins/discover/public/mocks.ts b/src/plugins/discover/public/mocks.ts index c394fe2c11a714..e4314426bfce53 100644 --- a/src/plugins/discover/public/mocks.ts +++ b/src/plugins/discover/public/mocks.ts @@ -34,6 +34,9 @@ const createSetupContract = (): Setup => { const createStartContract = (): Start => { const startContract: Start = { savedSearchLoader: {} as any, + urlGenerator: { + createUrl: jest.fn(), + } as any, }; return startContract; }; diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index 4323e3d8deda46..091288e3e65aa4 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -34,7 +34,7 @@ import { UiActionsStart, UiActionsSetup } from 'src/plugins/ui_actions/public'; import { EmbeddableStart, EmbeddableSetup } from 'src/plugins/embeddable/public'; import { ChartsPluginStart } from 'src/plugins/charts/public'; import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public'; -import { SharePluginStart } from 'src/plugins/share/public'; +import { SharePluginStart, SharePluginSetup, UrlGeneratorContract } from 'src/plugins/share/public'; import { VisualizationsStart, VisualizationsSetup } from 'src/plugins/visualizations/public'; import { KibanaLegacySetup } from 'src/plugins/kibana_legacy/public'; import { HomePublicPluginSetup } from 'src/plugins/home/public'; @@ -43,7 +43,7 @@ import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../d import { SavedObjectLoader } from '../../saved_objects/public'; import { createKbnUrlTracker } from '../../kibana_utils/public'; import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; - +import { UrlGeneratorState } from '../../share/public'; import { DocViewInput, DocViewInputFn } from './application/doc_views/doc_views_types'; import { DocViewsRegistry } from './application/doc_views/doc_views_registry'; import { DocViewTable } from './application/components/table/table'; @@ -59,6 +59,17 @@ import { import { createSavedSearchesLoader } from './saved_searches'; import { registerFeature } from './register_feature'; import { buildServices } from './build_services'; +import { + DiscoverUrlGeneratorState, + DISCOVER_APP_URL_GENERATOR, + DiscoverUrlGenerator, +} from './url_generator'; + +declare module '../../share/public' { + export interface UrlGeneratorStateMapping { + [DISCOVER_APP_URL_GENERATOR]: UrlGeneratorState; + } +} /** * @public @@ -76,12 +87,31 @@ export interface DiscoverSetup { export interface DiscoverStart { savedSearchLoader: SavedObjectLoader; + + /** + * `share` plugin URL generator for Discover app. Use it to generate links into + * Discover application, example: + * + * ```ts + * const url = await plugins.discover.urlGenerator.createUrl({ + * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d', + * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002', + * timeRange: { + * to: 'now', + * from: 'now-15m', + * mode: 'relative', + * }, + * }); + * ``` + */ + readonly urlGenerator: undefined | UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; } /** * @internal */ export interface DiscoverSetupPlugins { + share?: SharePluginSetup; uiActions: UiActionsSetup; embeddable: EmbeddableSetup; kibanaLegacy: KibanaLegacySetup; @@ -122,6 +152,7 @@ export class DiscoverPlugin private stopUrlTracking: (() => void) | undefined = undefined; private servicesInitialized: boolean = false; private innerAngularInitialized: boolean = false; + private urlGenerator?: DiscoverStart['urlGenerator']; /** * why are those functions public? they are needed for some mocha tests @@ -131,6 +162,17 @@ export class DiscoverPlugin public initializeServices?: () => Promise<{ core: CoreStart; plugins: DiscoverStartPlugins }>; setup(core: CoreSetup, plugins: DiscoverSetupPlugins) { + const baseUrl = core.http.basePath.prepend('/app/discover'); + + if (plugins.share) { + this.urlGenerator = plugins.share.urlGenerators.registerUrlGenerator( + new DiscoverUrlGenerator({ + appBasePath: baseUrl, + useHash: core.uiSettings.get('state:storeInSessionStorage'), + }) + ); + } + this.docViewsRegistry = new DocViewsRegistry(); setDocViewsRegistry(this.docViewsRegistry); this.docViewsRegistry.addDocView({ @@ -158,7 +200,7 @@ export class DiscoverPlugin // so history is lazily created (when app is mounted) // this prevents redundant `#` when not in discover app getHistory: getScopedHistory, - baseUrl: core.http.basePath.prepend('/app/discover'), + baseUrl, defaultSubUrl: '#/', storageKey: `lastUrl:${core.http.basePath.get()}:discover`, navLinkUpdater$: this.appStateUpdater, @@ -266,6 +308,7 @@ export class DiscoverPlugin }; return { + urlGenerator: this.urlGenerator, savedSearchLoader: createSavedSearchesLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns: plugins.data.indexPatterns, diff --git a/src/plugins/discover/public/url_generator.test.ts b/src/plugins/discover/public/url_generator.test.ts new file mode 100644 index 00000000000000..cf9beb246fea29 --- /dev/null +++ b/src/plugins/discover/public/url_generator.test.ts @@ -0,0 +1,259 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DiscoverUrlGenerator } from './url_generator'; +import { hashedItemStore, getStatesFromKbnUrl } from '../../kibana_utils/public'; +// eslint-disable-next-line +import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; +import { FilterStateStore } from '../../data/common'; + +const appBasePath: string = 'xyz/app/discover'; +const indexPatternId: string = 'c367b774-a4c2-11ea-bb37-0242ac130002'; +const savedSearchId: string = '571aaf70-4c88-11e8-b3d7-01146121b73d'; + +interface SetupParams { + useHash?: boolean; +} + +const setup = async ({ useHash = false }: SetupParams = {}) => { + const generator = new DiscoverUrlGenerator({ + appBasePath, + useHash, + }); + + return { + generator, + }; +}; + +beforeEach(() => { + // @ts-ignore + hashedItemStore.storage = mockStorage; +}); + +describe('Discover url generator', () => { + test('can create a link to Discover with no state and no saved search', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({}); + const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']); + + expect(url.startsWith(appBasePath)).toBe(true); + expect(_a).toEqual({}); + expect(_g).toEqual({}); + }); + + test('can create a link to a saved search in Discover', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ savedSearchId }); + const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']); + + expect(url.startsWith(`${appBasePath}#/${savedSearchId}`)).toBe(true); + expect(_a).toEqual({}); + expect(_g).toEqual({}); + }); + + test('can specify specific index pattern', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + indexPatternId, + }); + const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']); + + expect(_a).toEqual({ + index: indexPatternId, + }); + expect(_g).toEqual({}); + }); + + test('can specify specific time range', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, + }); + const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']); + + expect(_a).toEqual({}); + expect(_g).toEqual({ + time: { + from: 'now-15m', + mode: 'relative', + to: 'now', + }, + }); + }); + + test('can specify query', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + query: { + language: 'kuery', + query: 'foo', + }, + }); + const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']); + + expect(_a).toEqual({ + query: { + language: 'kuery', + query: 'foo', + }, + }); + expect(_g).toEqual({}); + }); + + test('can specify local and global filters', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + filters: [ + { + meta: { + alias: 'foo', + disabled: false, + negate: false, + }, + $state: { + store: FilterStateStore.APP_STATE, + }, + }, + { + meta: { + alias: 'bar', + disabled: false, + negate: false, + }, + $state: { + store: FilterStateStore.GLOBAL_STATE, + }, + }, + ], + }); + const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']); + + expect(_a).toEqual({ + filters: [ + { + $state: { + store: 'appState', + }, + meta: { + alias: 'foo', + disabled: false, + negate: false, + }, + }, + ], + }); + expect(_g).toEqual({ + filters: [ + { + $state: { + store: 'globalState', + }, + meta: { + alias: 'bar', + disabled: false, + negate: false, + }, + }, + ], + }); + }); + + test('can set refresh interval', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + refreshInterval: { + pause: false, + value: 666, + }, + }); + const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']); + + expect(_a).toEqual({}); + expect(_g).toEqual({ + refreshInterval: { + pause: false, + value: 666, + }, + }); + }); + + test('can set time range', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + timeRange: { + from: 'now-3h', + to: 'now', + }, + }); + const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']); + + expect(_a).toEqual({}); + expect(_g).toEqual({ + time: { + from: 'now-3h', + to: 'now', + }, + }); + }); + + describe('useHash property', () => { + describe('when default useHash is set to false', () => { + test('when using default, sets index pattern ID in the generated URL', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + indexPatternId, + }); + + expect(url.indexOf(indexPatternId) > -1).toBe(true); + }); + + test('when enabling useHash, does not set index pattern ID in the generated URL', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + useHash: true, + indexPatternId, + }); + + expect(url.indexOf(indexPatternId) > -1).toBe(false); + }); + }); + + describe('when default useHash is set to true', () => { + test('when using default, does not set index pattern ID in the generated URL', async () => { + const { generator } = await setup({ useHash: true }); + const url = await generator.createUrl({ + indexPatternId, + }); + + expect(url.indexOf(indexPatternId) > -1).toBe(false); + }); + + test('when disabling useHash, sets index pattern ID in the generated URL', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + useHash: false, + indexPatternId, + }); + + expect(url.indexOf(indexPatternId) > -1).toBe(true); + }); + }); + }); +}); diff --git a/src/plugins/discover/public/url_generator.ts b/src/plugins/discover/public/url_generator.ts new file mode 100644 index 00000000000000..42d689050d5ad4 --- /dev/null +++ b/src/plugins/discover/public/url_generator.ts @@ -0,0 +1,114 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + TimeRange, + Filter, + Query, + esFilters, + QueryState, + RefreshInterval, +} from '../../data/public'; +import { setStateToKbnUrl } from '../../kibana_utils/public'; +import { UrlGeneratorsDefinition } from '../../share/public'; + +export const DISCOVER_APP_URL_GENERATOR = 'DISCOVER_APP_URL_GENERATOR'; + +export interface DiscoverUrlGeneratorState { + /** + * Optionally set saved search ID. + */ + savedSearchId?: string; + + /** + * Optionally set index pattern ID. + */ + indexPatternId?: string; + + /** + * Optionally set the time range in the time picker. + */ + timeRange?: TimeRange; + + /** + * Optionally set the refresh interval. + */ + refreshInterval?: RefreshInterval; + + /** + * Optionally apply filers. + */ + filters?: Filter[]; + + /** + * Optionally set a query. NOTE: if given and used in conjunction with `dashboardId`, and the + * saved dashboard has a query saved with it, this will _replace_ that query. + */ + query?: Query; + + /** + * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines + * whether to hash the data in the url to avoid url length issues. + */ + useHash?: boolean; +} + +interface Params { + appBasePath: string; + useHash: boolean; +} + +export class DiscoverUrlGenerator + implements UrlGeneratorsDefinition { + constructor(private readonly params: Params) {} + + public readonly id = DISCOVER_APP_URL_GENERATOR; + + public readonly createUrl = async ({ + filters, + indexPatternId, + query, + refreshInterval, + savedSearchId, + timeRange, + useHash = this.params.useHash, + }: DiscoverUrlGeneratorState): Promise => { + const savedSearchPath = savedSearchId ? encodeURIComponent(savedSearchId) : ''; + const appState: { + query?: Query; + filters?: Filter[]; + index?: string; + } = {}; + const queryState: QueryState = {}; + + if (query) appState.query = query; + if (filters) appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f)); + if (indexPatternId) appState.index = indexPatternId; + + if (timeRange) queryState.time = timeRange; + if (filters) queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f)); + if (refreshInterval) queryState.refreshInterval = refreshInterval; + + let url = `${this.params.appBasePath}#/${savedSearchPath}`; + url = setStateToKbnUrl('_g', queryState, { useHash }, url); + url = setStateToKbnUrl('_a', appState, { useHash }, url); + + return url; + }; +} 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/home/public/application/components/__snapshots__/add_data.test.js.snap b/src/plugins/home/public/application/components/__snapshots__/add_data.test.js.snap index 3b3f86e579f1ad..2545bbcb5114d6 100644 --- a/src/plugins/home/public/application/components/__snapshots__/add_data.test.js.snap +++ b/src/plugins/home/public/application/components/__snapshots__/add_data.test.js.snap @@ -202,17 +202,17 @@ exports[`apmUiEnabled 1`] = ` } textAlign="left" - title="SIEM" + title="Security" titleSize="xs" /> @@ -468,17 +468,17 @@ exports[`isNewKibanaInstance 1`] = ` } textAlign="left" - title="SIEM" + title="Security" titleSize="xs" /> @@ -765,17 +765,17 @@ exports[`mlEnabled 1`] = ` } textAlign="left" - title="SIEM" + title="Security" titleSize="xs" /> @@ -1067,17 +1067,17 @@ exports[`render 1`] = ` } textAlign="left" - title="SIEM" + title="Security" titleSize="xs" /> diff --git a/src/plugins/home/public/application/components/add_data.js b/src/plugins/home/public/application/components/add_data.js index 2f7f07a0e4549d..fa1327b3fcd080 100644 --- a/src/plugins/home/public/application/components/add_data.js +++ b/src/plugins/home/public/application/components/add_data.js @@ -80,11 +80,11 @@ const AddDataUi = ({ apmUiEnabled, isNewKibanaInstance, intl, mlEnabled }) => { }; const siemData = { title: intl.formatMessage({ - id: 'home.addData.siem.nameTitle', - defaultMessage: 'SIEM', + id: 'home.addData.securitySolution.nameTitle', + defaultMessage: 'Security', }), description: intl.formatMessage({ - id: 'home.addData.siem.nameDescription', + id: 'home.addData.securitySolution.nameDescription', defaultMessage: 'Centralize security events for interactive investigation in ready-to-go visualizations.', }), @@ -221,11 +221,11 @@ const AddDataUi = ({ apmUiEnabled, isNewKibanaInstance, intl, mlEnabled }) => { footer={ diff --git a/src/plugins/home/public/application/components/tutorial_directory.js b/src/plugins/home/public/application/components/tutorial_directory.js index 4d2cec158f63e5..774b23af11ac85 100644 --- a/src/plugins/home/public/application/components/tutorial_directory.js +++ b/src/plugins/home/public/application/components/tutorial_directory.js @@ -75,10 +75,10 @@ class TutorialDirectoryUi extends React.Component { }), }, { - id: 'siem', + id: 'security', name: this.props.intl.formatMessage({ - id: 'home.tutorial.tabs.siemTitle', - defaultMessage: 'SIEM', + id: 'home.tutorial.tabs.securitySolutionTitle', + defaultMessage: 'Security', }), }, { diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx index edb96f119385ec..b6205a8731dfa9 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx @@ -21,7 +21,11 @@ import React, { Component } from 'react'; import { EuiPanel, EuiSpacer, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { indexPatterns, IndexPatternAttributes } from '../../../../../../../plugins/data/public'; +import { + indexPatterns, + IndexPatternAttributes, + UI_SETTINGS, +} from '../../../../../../../plugins/data/public'; import { MAX_SEARCH_SIZE } from '../../constants'; import { getIndices, @@ -82,7 +86,8 @@ export class StepIndexPattern extends Component fieldWildcardMatcher(filters, uiSettings.get('metaFields')), + (filters: string[]) => fieldWildcardMatcher(filters, uiSettings.get(UI_SETTINGS.META_FIELDS)), [uiSettings] ); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.tsx index a1b7289efee210..c97f19f59d340d 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.tsx @@ -35,7 +35,12 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { esQuery, IndexPattern, Query } from '../../../../../../../plugins/data/public'; +import { + esQuery, + IndexPattern, + Query, + UI_SETTINGS, +} from '../../../../../../../plugins/data/public'; import { context as contextType } from '../../../../../../kibana_react/public'; import { IndexPatternManagmentContextValue } from '../../../../types'; import { ExecuteScript } from '../../types'; @@ -244,7 +249,7 @@ export class TestScript extends Component { showDatePicker={false} showQueryInput={true} query={{ - language: this.context.services.uiSettings.get('search:queryLanguage'), + language: this.context.services.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE), query: '', }} onQuerySubmit={this.previewScript} diff --git a/src/plugins/inspector/public/views/data/components/data_table.tsx b/src/plugins/inspector/public/views/data/components/data_table.tsx index 69be069272f79c..0fdf3d9b13e333 100644 --- a/src/plugins/inspector/public/views/data/components/data_table.tsx +++ b/src/plugins/inspector/public/views/data/components/data_table.tsx @@ -37,6 +37,7 @@ import { DataDownloadOptions } from './download_options'; import { DataViewRow, DataViewColumn } from '../types'; import { TabularData } from '../../../../common/adapters/data/types'; import { IUiSettingsClient } from '../../../../../../core/public'; +import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../../../share/public'; interface DataTableFormatState { columns: DataViewColumn[]; @@ -58,8 +59,8 @@ export class DataTableFormat extends Component { +export interface StartServices { plugins: Plugins; self: OwnContract; - core: CoreStart; + core: Core; } -export type StartServicesGetter = () => StartServices< - Plugins, - OwnContract ->; +export type StartServicesGetter< + Plugins = unknown, + OwnContract = unknown, + Core = CoreStart +> = () => StartServices; /** * Use this utility to create a synchronous *start* service getter in *setup* diff --git a/src/plugins/share/common/constants.ts b/src/plugins/share/common/constants.ts new file mode 100644 index 00000000000000..7ad8e39c279d33 --- /dev/null +++ b/src/plugins/share/common/constants.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const CSV_SEPARATOR_SETTING = 'csv:separator'; +export const CSV_QUOTE_VALUES_SETTING = 'csv:quoteValues'; diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index 183219645467e1..e3d6c41a278cd9 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +export { CSV_QUOTE_VALUES_SETTING, CSV_SEPARATOR_SETTING } from '../common/constants'; + export { UrlGeneratorStateMapping } from './url_generators/url_generator_definition'; export { SharePluginSetup, SharePluginStart } from './plugin'; diff --git a/src/plugins/share/public/url_generators/url_generator_service.ts b/src/plugins/share/public/url_generators/url_generator_service.ts index 13c1b94acdd07e..b63e2a45d6812c 100644 --- a/src/plugins/share/public/url_generators/url_generator_service.ts +++ b/src/plugins/share/public/url_generators/url_generator_service.ts @@ -24,7 +24,7 @@ import { UrlGeneratorInternal } from './url_generator_internal'; import { UrlGeneratorContract } from './url_generator_contract'; export interface UrlGeneratorsStart { - getUrlGenerator: (urlGeneratorId: UrlGeneratorId) => UrlGeneratorContract; + getUrlGenerator: (urlGeneratorId: T) => UrlGeneratorContract; } export interface UrlGeneratorsSetup { diff --git a/src/plugins/share/server/index.ts b/src/plugins/share/server/index.ts index 9e574314f80000..ff419ce68d46b0 100644 --- a/src/plugins/share/server/index.ts +++ b/src/plugins/share/server/index.ts @@ -20,6 +20,8 @@ import { PluginInitializerContext } from '../../../core/server'; import { SharePlugin } from './plugin'; +export { CSV_QUOTE_VALUES_SETTING, CSV_SEPARATOR_SETTING } from '../common/constants'; + export function plugin(initializerContext: PluginInitializerContext) { return new SharePlugin(initializerContext); } diff --git a/src/plugins/share/server/plugin.ts b/src/plugins/share/server/plugin.ts index 0d9f183d13404b..e444cb1658d957 100644 --- a/src/plugins/share/server/plugin.ts +++ b/src/plugins/share/server/plugin.ts @@ -17,9 +17,12 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; import { createRoutes } from './routes/create_routes'; import { url } from './saved_objects'; +import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../common/constants'; export class SharePlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} @@ -27,6 +30,28 @@ export class SharePlugin implements Plugin { public async setup(core: CoreSetup) { createRoutes(core, this.initializerContext.logger.get()); core.savedObjects.registerType(url); + core.uiSettings.register({ + [CSV_SEPARATOR_SETTING]: { + name: i18n.translate('share.advancedSettings.csv.separatorTitle', { + defaultMessage: 'CSV separator', + }), + value: ',', + description: i18n.translate('share.advancedSettings.csv.separatorText', { + defaultMessage: 'Separate exported values with this string', + }), + schema: schema.string(), + }, + [CSV_QUOTE_VALUES_SETTING]: { + name: i18n.translate('share.advancedSettings.csv.quoteValuesTitle', { + defaultMessage: 'Quote CSV values', + }), + value: true, + description: i18n.translate('share.advancedSettings.csv.quoteValuesText', { + defaultMessage: 'Should values be quoted in csv exports?', + }), + schema: schema.boolean(), + }, + }); } public start() { diff --git a/src/plugins/vis_default_editor/public/components/controls/filters.tsx b/src/plugins/vis_default_editor/public/components/controls/filters.tsx index 7a655b935a45e6..9a9933b5e1e833 100644 --- a/src/plugins/vis_default_editor/public/components/controls/filters.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/filters.tsx @@ -23,7 +23,7 @@ import { htmlIdGenerator, EuiButton, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useMount } from 'react-use'; -import { Query } from 'src/plugins/data/public'; +import { Query, UI_SETTINGS } from '../../../../data/public'; import { useKibana } from '../../../../kibana_react/public'; import { FilterRow } from './filter'; import { AggParamEditorProps } from '../agg_param_props'; @@ -68,7 +68,7 @@ function FiltersParamEditor({ agg, value = [], setValue }: AggParamEditorProps } type="questionInCircle" diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table.js b/src/plugins/vis_type_table/public/agg_table/agg_table.js index f67dcf42adff6c..bd7626a493338e 100644 --- a/src/plugins/vis_type_table/public/agg_table/agg_table.js +++ b/src/plugins/vis_type_table/public/agg_table/agg_table.js @@ -17,6 +17,7 @@ * under the License. */ import _ from 'lodash'; +import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../share/public'; import aggTableTemplate from './agg_table.html'; import { getFormatService } from '../services'; import { i18n } from '@kbn/i18n'; @@ -47,8 +48,8 @@ export function KbnAggTable(config, RecursionHelper) { self._saveAs = require('@elastic/filesaver').saveAs; self.csv = { - separator: config.get('csv:separator'), - quoteValues: config.get('csv:quoteValues'), + separator: config.get(CSV_SEPARATOR_SETTING), + quoteValues: config.get(CSV_QUOTE_VALUES_SETTING), }; self.exportAsCsv = function (formatted) { diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js index 1bc979399b1b16..a624ff72ead699 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js +++ b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js @@ -30,6 +30,7 @@ import _ from 'lodash'; import { expect } from 'chai'; import sinon from 'sinon'; import invoke from '../helpers/invoke_series_fn.js'; +import { UI_SETTINGS } from '../../../../data/server'; function stubRequestAndServer(response, indexPatternSavedObjects = []) { return { @@ -216,14 +217,14 @@ describe('es', () => { it('sets ignore_throttled=true on the request', () => { config.index = 'beer'; - tlConfig.settings['search:includeFrozen'] = false; + tlConfig.settings[UI_SETTINGS.SEARCH_INCLUDE_FROZEN] = false; const request = fn(config, tlConfig, emptyScriptedFields); expect(request.ignore_throttled).to.equal(true); }); it('sets no timeout if elasticsearch.shardTimeout is set to 0', () => { - tlConfig.settings['search:includeFrozen'] = true; + tlConfig.settings[UI_SETTINGS.SEARCH_INCLUDE_FROZEN] = true; config.index = 'beer'; const request = fn(config, tlConfig, emptyScriptedFields); diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/lib/build_request.js b/src/plugins/vis_type_timelion/server/series_functions/es/lib/build_request.js index 65b28fb833279a..bc0e368fbdab17 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/es/lib/build_request.js +++ b/src/plugins/vis_type_timelion/server/series_functions/es/lib/build_request.js @@ -21,6 +21,7 @@ import _ from 'lodash'; import moment from 'moment'; import { buildAggBody } from './agg_body'; import createDateAgg from './create_date_agg'; +import { UI_SETTINGS } from '../../../../../data/server'; export default function buildRequest(config, tlConfig, scriptedFields, timeout) { const bool = { must: [] }; @@ -78,7 +79,7 @@ export default function buildRequest(config, tlConfig, scriptedFields, timeout) const request = { index: config.index, - ignore_throttled: !tlConfig.settings['search:includeFrozen'], + ignore_throttled: !tlConfig.settings[UI_SETTINGS.SEARCH_INCLUDE_FROZEN], body: { query: { bool: bool, diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_default_query_language.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_default_query_language.js index 972f937ad109d5..84da28718e3235 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_default_query_language.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_default_query_language.js @@ -18,7 +18,8 @@ */ import { getUISettings } from '../../../services'; +import { UI_SETTINGS } from '../../../../../data/public'; export function getDefaultQueryLanguage() { - return getUISettings().get('search:queryLanguage'); + return getUISettings().get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE); } diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.test.js index 71e82770bfa03e..308579126eeb1d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.test.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.test.js @@ -20,6 +20,7 @@ import { createTickFormatter } from './tick_formatter'; import { getFieldFormatsRegistry } from '../../../../../../test_utils/public/stub_field_formats'; import { setFieldFormats } from '../../../services'; +import { UI_SETTINGS } from '../../../../../data/public'; const mockUiSettings = { get: (item) => { @@ -28,11 +29,11 @@ const mockUiSettings = { getUpdate$: () => ({ subscribe: jest.fn(), }), - 'query:allowLeadingWildcards': true, - 'query:queryString:options': {}, - 'courier:ignoreFilterIfFieldNotInIndex': true, + [UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS]: true, + [UI_SETTINGS.QUERY_STRING_OPTIONS]: {}, + [UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX]: true, 'dateFormat:tz': 'Browser', - 'format:defaultTypeMap': {}, + [UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP]: {}, }; const mockCore = { @@ -55,7 +56,7 @@ describe('createTickFormatter(format, template)', () => { test('returns a percent with percent formatter', () => { const config = { - 'format:percent:defaultPattern': '0.[00]%', + [UI_SETTINGS.FORMAT_PERCENT_DEFAULT_PATTERN]: '0.[00]%', }; const fn = createTickFormatter('percent', null, (key) => config[key]); expect(fn(0.5556)).toEqual('55.56%'); @@ -63,7 +64,7 @@ describe('createTickFormatter(format, template)', () => { test('returns a byte formatted string with byte formatter', () => { const config = { - 'format:bytes:defaultPattern': '0.0b', + [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0.0b', }; const fn = createTickFormatter('bytes', null, (key) => config[key]); expect(fn(1500 ^ 10)).toEqual('1.5KB'); @@ -76,7 +77,7 @@ describe('createTickFormatter(format, template)', () => { test('returns a located string with custom locale setting', () => { const config = { - 'format:number:defaultLocale': 'fr', + [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_LOCALE]: 'fr', }; const fn = createTickFormatter('0,0.0', null, (key) => config[key]); expect(fn(1500)).toEqual('1 500,0'); @@ -99,7 +100,7 @@ describe('createTickFormatter(format, template)', () => { test('returns formatted value if passed a bad template', () => { const config = { - 'format:number:defaultPattern': '0,0.[00]', + [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN]: '0,0.[00]', }; const fn = createTickFormatter('number', '{{value', (key) => config[key]); expect(fn(1.5556)).toEqual('1.56'); diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js index 0c53ddd3f0ba82..a96890d4d15029 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js @@ -29,7 +29,7 @@ import { PanelConfig } from './panel_config'; import { createBrushHandler } from '../lib/create_brush_handler'; import { fetchFields } from '../lib/fetch_fields'; import { extractIndexPatterns } from '../../../../../plugins/vis_type_timeseries/common/extract_index_patterns'; -import { esKuery } from '../../../../../plugins/data/public'; +import { esKuery, UI_SETTINGS } from '../../../../../plugins/data/public'; import { getSavedObjectsClient, getUISettings, getDataStart, getCoreStart } from '../../services'; import { CoreStartContextProvider } from '../contexts/query_input_bar_context'; @@ -89,7 +89,9 @@ export class VisEditor extends Component { isValidKueryQuery = (filterQuery) => { if (filterQuery && filterQuery.language === 'kuery') { try { - const queryOptions = this.coreContext.uiSettings.get('query:allowLeadingWildcards'); + const queryOptions = this.coreContext.uiSettings.get( + UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS + ); esKuery.fromKueryExpression(filterQuery.query, { allowLeadingWildcards: queryOptions }); } catch (error) { return false; diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.js index 93a4eaba4ad9ec..9ada39e3595890 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.js @@ -17,12 +17,15 @@ * under the License. */ import { AbstractSearchRequest } from './abstract_request'; +import { UI_SETTINGS } from '../../../../../data/server'; const SEARCH_METHOD = 'msearch'; export class MultiSearchRequest extends AbstractSearchRequest { async search(searches) { - const includeFrozen = await this.req.getUiSettingsService().get('search:includeFrozen'); + const includeFrozen = await this.req + .getUiSettingsService() + .get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); const multiSearchBody = searches.reduce( (acc, { body, index }) => [ ...acc, diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.test.js index 1e28965a35793f..c113db76332b76 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.test.js @@ -17,6 +17,7 @@ * under the License. */ import { MultiSearchRequest } from './multi_search_request'; +import { UI_SETTINGS } from '../../../../../data/server'; describe('MultiSearchRequest', () => { let searchRequest; @@ -51,7 +52,7 @@ describe('MultiSearchRequest', () => { expect(responses).toEqual([]); expect(req.getUiSettingsService).toHaveBeenCalled(); - expect(getServiceMock).toHaveBeenCalledWith('search:includeFrozen'); + expect(getServiceMock).toHaveBeenCalledWith(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); expect(callWithRequest).toHaveBeenCalledWith(req, 'msearch', { body: [ { ignoreUnavailable: true, index: 'index' }, diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.js index 110deb6a9bc1b6..7d8b60a7e45957 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.js @@ -17,12 +17,15 @@ * under the License. */ import { AbstractSearchRequest } from './abstract_request'; +import { UI_SETTINGS } from '../../../../../data/server'; const SEARCH_METHOD = 'search'; export class SingleSearchRequest extends AbstractSearchRequest { async search([{ body, index }]) { - const includeFrozen = await this.req.getUiSettingsService().get('search:includeFrozen'); + const includeFrozen = await this.req + .getUiSettingsService() + .get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); const resp = await this.callWithRequest(this.req, SEARCH_METHOD, { ignore_throttled: !includeFrozen, body, diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.test.js index 043bd52d87aad5..b899814f2fe133 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.test.js @@ -17,6 +17,7 @@ * under the License. */ import { SingleSearchRequest } from './single_search_request'; +import { UI_SETTINGS } from '../../../../../data/server'; describe('SingleSearchRequest', () => { let searchRequest; @@ -48,7 +49,7 @@ describe('SingleSearchRequest', () => { expect(responses).toEqual([{}]); expect(req.getUiSettingsService).toHaveBeenCalled(); - expect(getServiceMock).toHaveBeenCalledWith('search:includeFrozen'); + expect(getServiceMock).toHaveBeenCalledWith(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); expect(callWithRequest).toHaveBeenCalledWith(req, 'search', { body: 'body', index: 'index', diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_es_query_uisettings.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_es_query_uisettings.js index 42b8681f142e05..b427e5f12cadc9 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_es_query_uisettings.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_es_query_uisettings.js @@ -17,12 +17,14 @@ * under the License. */ +import { UI_SETTINGS } from '../../../../../data/server'; + export async function getEsQueryConfig(req) { const uiSettings = req.getUiSettingsService(); - const allowLeadingWildcards = await uiSettings.get('query:allowLeadingWildcards'); - const queryStringOptions = await uiSettings.get('query:queryString:options'); + const allowLeadingWildcards = await uiSettings.get(UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS); + const queryStringOptions = await uiSettings.get(UI_SETTINGS.QUERY_STRING_OPTIONS); const ignoreFilterIfFieldNotInIndex = await uiSettings.get( - 'courier:ignoreFilterIfFieldNotInIndex' + UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX ); return { allowLeadingWildcards, diff --git a/src/plugins/visualizations/common/constants.ts b/src/plugins/visualizations/common/constants.ts new file mode 100644 index 00000000000000..9129f060c5eef4 --- /dev/null +++ b/src/plugins/visualizations/common/constants.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const VISUALIZE_ENABLE_LABS_SETTING = 'visualize:enableLabs'; diff --git a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts index 81794c31527adb..45c750de05ae17 100644 --- a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts +++ b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts @@ -29,6 +29,7 @@ import { getCapabilities, } from '../services'; import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDeps) => async ( vis: Vis, @@ -44,7 +45,7 @@ export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDe const editUrl = visId ? getHttp().basePath.prepend(`/app/visualize${savedVisualizations.urlFor(visId)}`) : ''; - const isLabsEnabled = getUISettings().get('visualize:enableLabs'); + const isLabsEnabled = getUISettings().get(VISUALIZE_ENABLE_LABS_SETTING); if (!isLabsEnabled && vis.type.stage === 'experimental') { return new DisabledLabEmbeddable(vis.title, input); diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index c4aa4c262edb06..c4267c9a36f784 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -43,6 +43,7 @@ import { convertToSerializedVis } from '../saved_visualizations/_saved_vis'; import { createVisEmbeddableFromObject } from './create_vis_embeddable_from_object'; import { StartServicesGetter } from '../../../kibana_utils/public'; import { VisualizationsStartDeps } from '../plugin'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; interface VisualizationAttributes extends SavedObjectAttributes { visState: string; @@ -82,7 +83,7 @@ export class VisualizeEmbeddableFactory if (!visType) { return false; } - if (getUISettings().get('visualize:enableLabs')) { + if (getUISettings().get(VISUALIZE_ENABLE_LABS_SETTING)) { return true; } return visType.stage !== 'experimental'; diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index e475684ed5934d..0bbf862216ed5b 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -17,8 +17,6 @@ * under the License. */ -import './index.scss'; - import { PublicContract } from '@kbn/utility-types'; import { PluginInitializerContext } from 'src/core/public'; import { VisualizationsPlugin, VisualizationsSetup, VisualizationsStart } from './plugin'; @@ -53,3 +51,4 @@ export { VisSavedObject, VisResponseValue, } from './types'; +export { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants'; 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..3546fa40564915 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + import { PluginInitializerContext, CoreSetup, @@ -45,6 +47,7 @@ import { setChrome, setOverlays, setSavedSearchLoader, + setEmbeddable, } from './services'; import { VISUALIZE_EMBEDDABLE_TYPE, @@ -52,7 +55,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 +105,7 @@ export interface VisualizationsSetupDeps { export interface VisualizationsStartDeps { data: DataPublicPluginStart; expressions: ExpressionsStart; + embeddable: EmbeddableStart; inspector: InspectorStart; uiActions: UiActionsStart; application: ApplicationStart; @@ -151,11 +155,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/visualizations/public/wizard/new_vis_modal.tsx b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx index cea92b1db93aad..1a970e505b7c71 100644 --- a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx +++ b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx @@ -29,6 +29,7 @@ import { TypeSelection } from './type_selection'; import { TypesStart, VisType, VisTypeAlias } from '../vis_types'; import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public'; import { EMBEDDABLE_ORIGINATING_APP_PARAM } from '../../../embeddable/public'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; interface TypeSelectionProps { isOpen: boolean; @@ -65,7 +66,7 @@ class NewVisModal extends React.Component originatingApp; const visStateToEditorState = () => { diff --git a/src/plugins/visualize/public/application/listing/visualize_listing.js b/src/plugins/visualize/public/application/listing/visualize_listing.js index 228cfa1e9e4922..e8e8d92034113a 100644 --- a/src/plugins/visualize/public/application/listing/visualize_listing.js +++ b/src/plugins/visualize/public/application/listing/visualize_listing.js @@ -25,6 +25,8 @@ import { i18n } from '@kbn/i18n'; import { getServices } from '../../kibana_services'; import { syncQueryStateWithUrl } from '../../../../data/public'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../../visualizations/public'; + import { EuiLink } from '@elastic/eui'; import React from 'react'; @@ -120,7 +122,7 @@ export function VisualizeListingController($scope, createNewVis, kbnUrlStateStor } this.fetchItems = (filter) => { - const isLabsEnabled = uiSettings.get('visualize:enableLabs'); + const isLabsEnabled = uiSettings.get(VISUALIZE_ENABLE_LABS_SETTING); return savedVisualizations .findListItems(filter, savedObjectsPublic.settings.getListingLimit()) .then((result) => { diff --git a/test/functional/apps/bundles/index.js b/test/functional/apps/bundles/index.js index 503517a98c69e8..ead6412564751f 100644 --- a/test/functional/apps/bundles/index.js +++ b/test/functional/apps/bundles/index.js @@ -25,7 +25,7 @@ export default function ({ getService }) { const supertest = getService('supertest'); describe('bundle compression', function () { - this.tags('ciGroup12'); + this.tags(['ciGroup12', 'skipCoverage']); let buildNum; before(async () => { diff --git a/test/functional/apps/context/_filters.js b/test/functional/apps/context/_filters.js index 470ef462b9d9d2..66888d441954e5 100644 --- a/test/functional/apps/context/_filters.js +++ b/test/functional/apps/context/_filters.js @@ -17,8 +17,6 @@ * under the License. */ -import expect from '@kbn/expect'; - const TEST_INDEX_PATTERN = 'logstash-*'; const TEST_ANCHOR_ID = 'AU_x3_BrGFA8no6QjjaI'; const TEST_ANCHOR_FILTER_FIELD = 'geo.src'; @@ -40,20 +38,19 @@ export default function ({ getService, getPageObjects }) { }); it('inclusive filter should be addable via expanded doc table rows', async function () { - await docTable.toggleRowExpanded({ isAnchorRow: true }); - - await retry.try(async () => { + await retry.waitFor(`filter ${TEST_ANCHOR_FILTER_FIELD} in filterbar`, async () => { + await docTable.toggleRowExpanded({ isAnchorRow: true }); const anchorDetailsRow = await docTable.getAnchorDetailsRow(); await docTable.addInclusiveFilter(anchorDetailsRow, TEST_ANCHOR_FILTER_FIELD); await PageObjects.context.waitUntilContextLoadingHasFinished(); - expect( - await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, true) - ).to.be(true); + + return await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, true); + }); + await retry.waitFor(`filter matching docs in docTable`, async () => { const fields = await docTable.getFields(); - const hasOnlyFilteredRows = fields + return fields .map((row) => row[2]) .every((fieldContent) => fieldContent === TEST_ANCHOR_FILTER_VALUE); - expect(hasOnlyFilteredRows).to.be(true); }); }); @@ -64,26 +61,27 @@ export default function ({ getService, getPageObjects }) { await filterBar.toggleFilterEnabled(TEST_ANCHOR_FILTER_FIELD); await PageObjects.context.waitUntilContextLoadingHasFinished(); - await retry.try(async () => { - expect( - await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, false) - ).to.be(true); + await retry.waitFor(`a disabled filter in filterbar`, async () => { + return await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, false); + }); + + await retry.waitFor('filters are disabled', async () => { const fields = await docTable.getFields(); const hasOnlyFilteredRows = fields .map((row) => row[2]) .every((fieldContent) => fieldContent === TEST_ANCHOR_FILTER_VALUE); - expect(hasOnlyFilteredRows).to.be(false); + return hasOnlyFilteredRows === false; }); }); it('filter for presence should be addable via expanded doc table rows', async function () { await docTable.toggleRowExpanded({ isAnchorRow: true }); - await retry.try(async () => { + await retry.waitFor('an exists filter in the filterbar', async () => { const anchorDetailsRow = await docTable.getAnchorDetailsRow(); await docTable.addExistsFilter(anchorDetailsRow, TEST_ANCHOR_FILTER_FIELD); await PageObjects.context.waitUntilContextLoadingHasFinished(); - expect(await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, 'exists', true)).to.be(true); + return await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, 'exists', true); }); }); }); diff --git a/test/functional/apps/context/_size.js b/test/functional/apps/context/_size.js index 3beb070b50deb1..067a23daacb4a6 100644 --- a/test/functional/apps/context/_size.js +++ b/test/functional/apps/context/_size.js @@ -16,69 +16,69 @@ * specific language governing permissions and limitations * under the License. */ - -import expect from '@kbn/expect'; - const TEST_INDEX_PATTERN = 'logstash-*'; const TEST_ANCHOR_ID = 'AU_x3_BrGFA8no6QjjaI'; -const TEST_DEFAULT_CONTEXT_SIZE = 7; -const TEST_STEP_SIZE = 3; +const TEST_DEFAULT_CONTEXT_SIZE = 2; +const TEST_STEP_SIZE = 2; export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); const docTable = getService('docTable'); const PageObjects = getPageObjects(['context']); + let expectedRowLength = 2 * TEST_DEFAULT_CONTEXT_SIZE + 1; - // FLAKY: https://github.com/elastic/kibana/issues/53888 - describe.skip('context size', function contextSize() { + describe('context size', function contextSize() { before(async function () { await kibanaServer.uiSettings.update({ 'context:defaultSize': `${TEST_DEFAULT_CONTEXT_SIZE}`, 'context:step': `${TEST_STEP_SIZE}`, }); + await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_ID); }); it('should default to the `context:defaultSize` setting', async function () { - await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_ID); - - await retry.try(async function () { - expect(await docTable.getRowsText()).to.have.length(2 * TEST_DEFAULT_CONTEXT_SIZE + 1); - }); - await retry.try(async function () { - const predecessorCountPicker = await PageObjects.context.getPredecessorCountPicker(); - expect(await predecessorCountPicker.getAttribute('value')).to.equal( - `${TEST_DEFAULT_CONTEXT_SIZE}` - ); - }); - await retry.try(async function () { - const successorCountPicker = await PageObjects.context.getSuccessorCountPicker(); - expect(await successorCountPicker.getAttribute('value')).to.equal( - `${TEST_DEFAULT_CONTEXT_SIZE}` - ); - }); + await retry.waitFor( + `number of rows displayed initially is ${expectedRowLength}`, + async function () { + const rows = await docTable.getRowsText(); + return rows.length === expectedRowLength; + } + ); + await retry.waitFor( + `predecessor count picker is set to ${TEST_DEFAULT_CONTEXT_SIZE}`, + async function () { + const predecessorCountPicker = await PageObjects.context.getPredecessorCountPicker(); + const value = await predecessorCountPicker.getAttribute('value'); + return value === String(TEST_DEFAULT_CONTEXT_SIZE); + } + ); }); it('should increase according to the `context:step` setting when clicking the `load newer` button', async function () { - await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_ID); await PageObjects.context.clickPredecessorLoadMoreButton(); + expectedRowLength += TEST_STEP_SIZE; - await retry.try(async function () { - expect(await docTable.getRowsText()).to.have.length( - 2 * TEST_DEFAULT_CONTEXT_SIZE + TEST_STEP_SIZE + 1 - ); - }); + await retry.waitFor( + `number of rows displayed after clicking load more predecessors is ${expectedRowLength}`, + async function () { + const rows = await docTable.getRowsText(); + return rows.length === expectedRowLength; + } + ); }); it('should increase according to the `context:step` setting when clicking the `load older` button', async function () { - await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_ID); await PageObjects.context.clickSuccessorLoadMoreButton(); + expectedRowLength += TEST_STEP_SIZE; - await retry.try(async function () { - expect(await docTable.getRowsText()).to.have.length( - 2 * TEST_DEFAULT_CONTEXT_SIZE + TEST_STEP_SIZE + 1 - ); - }); + await retry.waitFor( + `number of rows displayed after clicking load more successors is ${expectedRowLength}`, + async function () { + const rows = await docTable.getRowsText(); + return rows.length === expectedRowLength; + } + ); }); }); } diff --git a/test/functional/apps/dashboard/create_and_add_embeddables.js b/test/functional/apps/dashboard/create_and_add_embeddables.js index ba715f3472b98d..f5c2496a9a5aaf 100644 --- a/test/functional/apps/dashboard/create_and_add_embeddables.js +++ b/test/functional/apps/dashboard/create_and_add_embeddables.js @@ -20,6 +20,7 @@ import expect from '@kbn/expect'; import { VisualizeConstants } from '../../../../src/plugins/visualize/public/application/visualize_constants'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../../src/plugins/visualizations/common/constants'; export default function ({ getService, getPageObjects }) { const retry = getService('retry'); @@ -102,7 +103,7 @@ export default function ({ getService, getPageObjects }) { before(async () => { await PageObjects.header.clickStackManagement(); await PageObjects.settings.clickKibanaSettings(); - await PageObjects.settings.toggleAdvancedSettingCheckbox('visualize:enableLabs'); + await PageObjects.settings.toggleAdvancedSettingCheckbox(VISUALIZE_ENABLE_LABS_SETTING); }); it('should not display lab visualizations in add panel', async () => { @@ -117,7 +118,7 @@ export default function ({ getService, getPageObjects }) { after(async () => { await PageObjects.header.clickStackManagement(); await PageObjects.settings.clickKibanaSettings(); - await PageObjects.settings.clearAdvancedSettings('visualize:enableLabs'); + await PageObjects.settings.clearAdvancedSettings(VISUALIZE_ENABLE_LABS_SETTING); await PageObjects.header.clickDashboard(); }); }); diff --git a/test/functional/apps/dashboard/dashboard_filter_bar.js b/test/functional/apps/dashboard/dashboard_filter_bar.js index 6bc34a8b998a49..c931e6763f4830 100644 --- a/test/functional/apps/dashboard/dashboard_filter_bar.js +++ b/test/functional/apps/dashboard/dashboard_filter_bar.js @@ -217,6 +217,11 @@ export default function ({ getService, getPageObjects }) { const hasWarningFieldFilter = await filterBar.hasFilter('extension', 'warn', true); expect(hasWarningFieldFilter).to.be(true); }); + + it('filter without an index pattern is rendred as a warning, if the dashboard has an index pattern', async function () { + const noIndexPatternFilter = await filterBar.hasFilter('banana', 'warn', true); + expect(noIndexPatternFilter).to.be(true); + }); }); }); } diff --git a/test/functional/apps/visualize/_lab_mode.js b/test/functional/apps/visualize/_lab_mode.js index b356d01cdb63bc..27c149b9e0e0a1 100644 --- a/test/functional/apps/visualize/_lab_mode.js +++ b/test/functional/apps/visualize/_lab_mode.js @@ -18,6 +18,7 @@ */ import expect from '@kbn/expect'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../../src/plugins/visualizations/common/constants'; export default function ({ getService, getPageObjects }) { const log = getService('log'); @@ -37,7 +38,7 @@ export default function ({ getService, getPageObjects }) { // Navigate to advanced setting and disable lab mode await PageObjects.header.clickStackManagement(); await PageObjects.settings.clickKibanaSettings(); - await PageObjects.settings.toggleAdvancedSettingCheckbox('visualize:enableLabs'); + await PageObjects.settings.toggleAdvancedSettingCheckbox(VISUALIZE_ENABLE_LABS_SETTING); // Expect the discover still to list that saved visualization in the open list await PageObjects.header.clickDiscover(); @@ -51,7 +52,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.discover.closeLoadSaveSearchPanel(); await PageObjects.header.clickStackManagement(); await PageObjects.settings.clickKibanaSettings(); - await PageObjects.settings.clearAdvancedSettings('visualize:enableLabs'); + await PageObjects.settings.clearAdvancedSettings(VISUALIZE_ENABLE_LABS_SETTING); }); }); } diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index bd427577cd7877..42b82486dc13f7 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -18,6 +18,7 @@ */ import { FtrProviderContext } from '../../ftr_provider_context.d'; +import { UI_SETTINGS } from '../../../../src/plugins/data/common'; // eslint-disable-next-line @typescript-eslint/no-namespace, import/no-default-export export default function ({ getService, getPageObjects, loadTestFile }: FtrProviderContext) { @@ -37,7 +38,7 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid await esArchiver.load('visualize'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', - 'format:bytes:defaultPattern': '0,0.[000]b', + [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', }); isOss = await PageObjects.common.isOss(); }); diff --git a/test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json.gz b/test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json.gz index a052aad9450f56..ae78761fef0d34 100644 Binary files a/test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json.gz and b/test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json.gz differ diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 91e9c020a0e7c1..fe5694efc35da6 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -67,17 +67,17 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo * @param appUrl Kibana URL */ private async loginIfPrompted(appUrl: string, insertTimestamp: boolean) { + // Disable the welcome screen. This is relevant for environments + // which don't allow to use the yml setting, e.g. cloud production. + // It is done here so it applies to logins but also to a login re-use. + await browser.setLocalStorageItem('home:welcome:show', 'false'); + let currentUrl = await browser.getCurrentUrl(); log.debug(`currentUrl = ${currentUrl}\n appUrl = ${appUrl}`); await testSubjects.find('kibanaChrome', 6 * defaultFindTimeout); // 60 sec waiting const loginPage = currentUrl.includes('/login'); const wantedLoginPage = appUrl.includes('/login') || appUrl.includes('/logout'); - // Disable the welcome screen. This is relevant for environments - // which don't allow to use the yml setting, e.g. cloud production. - // It is done here so it applies to logins but also to a login re-use. - await browser.setLocalStorageItem('home:welcome:show', 'false'); - if (loginPage && !wantedLoginPage) { log.debug('Found login page'); if (config.get('security.disableTestUser')) { diff --git a/test/scripts/jenkins_ci_group.sh b/test/scripts/jenkins_ci_group.sh index 778142d95e4b4f..60d7f0406f4c9f 100755 --- a/test/scripts/jenkins_ci_group.sh +++ b/test/scripts/jenkins_ci_group.sh @@ -31,4 +31,12 @@ else mkdir -p ../kibana/target/kibana-coverage/functional mv target/kibana-coverage/functional/* ../kibana/target/kibana-coverage/functional/ fi + + echo " -> moving junit output, silently fail in case of no report" + mkdir -p ../kibana/target/junit + mv target/junit/* ../kibana/target/junit/ || echo "copying junit failed" + + echo " -> copying screenshots and html for failures" + cp -r test/functional/screenshots/* ../kibana/test/functional/screenshots/ || echo "copying screenshots failed" + cp -r test/functional/failure_debug ../kibana/test/functional/ || echo "copying html failed" fi diff --git a/test/scripts/jenkins_xpack_ci_group.sh b/test/scripts/jenkins_xpack_ci_group.sh index a6e600630364eb..648605135b3595 100755 --- a/test/scripts/jenkins_xpack_ci_group.sh +++ b/test/scripts/jenkins_xpack_ci_group.sh @@ -32,4 +32,12 @@ else mkdir -p ../../kibana/target/kibana-coverage/functional mv ../target/kibana-coverage/functional/* ../../kibana/target/kibana-coverage/functional/ fi + + echo " -> moving junit output, silently fail in case of no report" + mkdir -p ../../kibana/target/junit + mv ../target/junit/* ../../kibana/target/junit/ || echo "copying junit failed" + + echo " -> copying screenshots and html for failures" + cp -r test/functional/screenshots/* ../../kibana/x-pack/test/functional/screenshots/ || echo "copying screenshots failed" + cp -r test/functional/failure_debug ../../kibana/x-pack/test/functional/ || echo "copying html failed" fi \ No newline at end of file diff --git a/test/scripts/jenkins_xpack_page_load_metrics.sh b/test/scripts/jenkins_xpack_page_load_metrics.sh new file mode 100644 index 00000000000000..679f0b8d2ddc59 --- /dev/null +++ b/test/scripts/jenkins_xpack_page_load_metrics.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +source test/scripts/jenkins_test_setup_xpack.sh + +checks-reporter-with-killswitch "Capture Kibana page load metrics" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$installDir" \ + --config test/page_load_metrics/config.ts; diff --git a/x-pack/.gitignore b/x-pack/.gitignore index 92597a101c03d5..68262c4bf734ba 100644 --- a/x-pack/.gitignore +++ b/x-pack/.gitignore @@ -3,8 +3,11 @@ /target /test/functional/failure_debug /test/functional/screenshots +/test/page_load_metrics/screenshots /test/functional/apps/reporting/reports/session /test/reporting/configs/failure_debug/ +/legacy/plugins/reporting/.chromium/ +/legacy/plugins/reporting/.phantom/ /plugins/reporting/.chromium/ /plugins/reporting/.phantom/ /.aws-config.json diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index e453abb3a0f98b..d17e5d1a74a300 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -2,8 +2,7 @@ "prefix": "xpack", "paths": { "xpack.actions": "plugins/actions", - "xpack.advancedUiActions": "plugins/advanced_ui_actions", - "xpack.uiActionsEnhanced": "examples/ui_actions_enhanced_examples", + "xpack.uiActionsEnhanced": ["plugins/ui_actions_enhanced", "examples/ui_actions_enhanced_examples"], "xpack.alerts": "plugins/alerts", "xpack.alertingBuiltins": "plugins/alerting_builtins", "xpack.apm": ["legacy/plugins/apm", "plugins/apm"], @@ -39,7 +38,7 @@ "xpack.reporting": ["plugins/reporting"], "xpack.rollupJobs": ["legacy/plugins/rollup", "plugins/rollup"], "xpack.searchProfiler": "plugins/searchprofiler", - "xpack.security": ["legacy/plugins/security", "plugins/security"], + "xpack.security": "plugins/security", "xpack.server": "legacy/server", "xpack.securitySolution": "plugins/security_solution", "xpack.snapshotRestore": "plugins/snapshot_restore", diff --git a/x-pack/README.md b/x-pack/README.md index 744d97ca02c75e..03d2e3287c0f0b 100644 --- a/x-pack/README.md +++ b/x-pack/README.md @@ -25,8 +25,8 @@ Examples: - Run the jest test case whose description matches 'filtering should skip values of null': `cd x-pack && yarn test:jest -t 'filtering should skip values of null' plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js` - Run the x-pack api integration test case whose description matches the given string: - `node scripts/functional_tests_server --config x-pack/test/api_integration/config.js` - `node scripts/functional_test_runner --config x-pack/test/api_integration/config.js --grep='apis Monitoring Beats list with restarted beat instance should load multiple clusters'` + `node scripts/functional_tests_server --config x-pack/test/api_integration/config.ts` + `node scripts/functional_test_runner --config x-pack/test/api_integration/config.ts --grep='apis Monitoring Beats list with restarted beat instance should load multiple clusters'` In addition to to providing a regular expression argument, specific tests can also be run by appeding `.only` to an `it` or `describe` function block. E.g. `describe(` to `describe.only(`. @@ -63,7 +63,7 @@ yarn test:mocha For more info, see [the Elastic functional test development guide](https://www.elastic.co/guide/en/kibana/current/development-functional-tests.html). -The functional UI tests, the API integration tests, and the SAML API integration tests are all run against a live browser, Kibana, and Elasticsearch install. Each set of tests is specified with a unique config that describes how to start the Elasticsearch server, the Kibana server, and what tests to run against them. The sets of tests that exist today are *functional UI tests* ([specified by this config](test/functional/config.js)), *API integration tests* ([specified by this config](test/api_integration/config.js)), and *SAML API integration tests* ([specified by this config](test/saml_api_integration/config.js)). +The functional UI tests, the API integration tests, and the SAML API integration tests are all run against a live browser, Kibana, and Elasticsearch install. Each set of tests is specified with a unique config that describes how to start the Elasticsearch server, the Kibana server, and what tests to run against them. The sets of tests that exist today are *functional UI tests* ([specified by this config](test/functional/config.js)), *API integration tests* ([specified by this config](test/api_integration/config.ts)), and *SAML API integration tests* ([specified by this config](test/saml_api_integration/config.ts)). The script runs all sets of tests sequentially like so: * builds Elasticsearch and X-Pack diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index 01b966ebe359b5..74553bbde0cd6e 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -43,6 +43,7 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector '!**/scripts/**', '!**/mocks/**', '!**/plugins/apm/e2e/**', + '!**/plugins/siem/cypress/**', ], coveragePathIgnorePatterns: ['.*\\.d\\.ts'], coverageDirectory: `${kibanaDirectory}/target/kibana-coverage/jest`, diff --git a/x-pack/examples/ui_actions_enhanced_examples/kibana.json b/x-pack/examples/ui_actions_enhanced_examples/kibana.json index e220cdd5cd297b..a1cd895bb3cd62 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/kibana.json +++ b/x-pack/examples/ui_actions_enhanced_examples/kibana.json @@ -5,6 +5,6 @@ "configPath": ["ui_actions_enhanced_examples"], "server": false, "ui": true, - "requiredPlugins": ["advancedUiActions", "data"], + "requiredPlugins": ["uiActionsEnhanced", "data", "discover"], "optionalPlugins": [] } diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx index 847035403da024..bfe853241ae1dc 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiFormRow, EuiFieldText } from '@elastic/eui'; import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public'; -import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/advanced_ui_actions/public'; +import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public'; import { RangeSelectTriggerContext, ValueClickTriggerContext, diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/discover_drilldown_config.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/discover_drilldown_config.tsx index 0237e128c5a2fc..da9b0e921fb1cb 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/discover_drilldown_config.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/discover_drilldown_config.tsx @@ -31,9 +31,7 @@ export const DiscoverDrilldownConfig: React.FC = ( onIndexPatternSelect, customIndexPattern, onCustomIndexPatternToggle, - carryFiltersAndQuery, onCarryFiltersAndQueryToggle, - carryTimeRange, onCarryTimeRangeToggle, }) => { return ( @@ -82,9 +80,10 @@ export const DiscoverDrilldownConfig: React.FC = ( {!!onCarryFiltersAndQueryToggle && ( @@ -92,9 +91,10 @@ export const DiscoverDrilldownConfig: React.FC = ( {!!onCarryTimeRangeToggle && ( diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/drilldown.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/drilldown.tsx index fef01c9640f0d9..ba88f49861ffef 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/drilldown.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/drilldown.tsx @@ -11,7 +11,7 @@ import { StartServicesGetter } from '../../../../../src/plugins/kibana_utils/pub import { ActionContext, Config, CollectConfigProps } from './types'; import { CollectConfigContainer } from './collect_config_container'; import { SAMPLE_DASHBOARD_TO_DISCOVER_DRILLDOWN } from './constants'; -import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/advanced_ui_actions/public'; +import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public'; import { txtGoToDiscover } from './i18n'; const isOutputWithIndexPatterns = ( @@ -22,7 +22,7 @@ const isOutputWithIndexPatterns = ( }; export interface Params { - start: StartServicesGetter>; + start: StartServicesGetter>; } export class DashboardToDiscoverDrilldown implements Drilldown { @@ -54,6 +54,10 @@ export class DashboardToDiscoverDrilldown implements Drilldown => { + const { urlGenerator } = this.params.start().plugins.discover; + + if (!urlGenerator) throw new Error('Discover URL generator not available.'); + let indexPatternId = !!config.customIndexPattern && !!config.indexPatternId ? config.indexPatternId : ''; @@ -64,8 +68,9 @@ export class DashboardToDiscoverDrilldown implements Drilldown => { diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx index 20267a8b7292bf..4810fb2d6ad8da 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiFormRow, EuiSwitch, EuiFieldText, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public'; -import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/advanced_ui_actions/public'; +import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public'; import { RangeSelectTriggerContext, ValueClickTriggerContext, diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts b/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts index 0d4f274caf57ff..8034c378cc64f7 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts +++ b/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts @@ -9,27 +9,30 @@ import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/pl import { AdvancedUiActionsSetup, AdvancedUiActionsStart, -} from '../../../../x-pack/plugins/advanced_ui_actions/public'; +} from '../../../../x-pack/plugins/ui_actions_enhanced/public'; import { DashboardHelloWorldDrilldown } from './dashboard_hello_world_drilldown'; import { DashboardToUrlDrilldown } from './dashboard_to_url_drilldown'; import { DashboardToDiscoverDrilldown } from './dashboard_to_discover_drilldown'; import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; +import { DiscoverSetup, DiscoverStart } from '../../../../src/plugins/discover/public'; export interface SetupDependencies { data: DataPublicPluginSetup; - advancedUiActions: AdvancedUiActionsSetup; + discover: DiscoverSetup; + uiActionsEnhanced: AdvancedUiActionsSetup; } export interface StartDependencies { data: DataPublicPluginStart; - advancedUiActions: AdvancedUiActionsStart; + discover: DiscoverStart; + uiActionsEnhanced: AdvancedUiActionsStart; } export class UiActionsEnhancedExamplesPlugin implements Plugin { public setup( core: CoreSetup, - { advancedUiActions: uiActions }: SetupDependencies + { uiActionsEnhanced: uiActions }: SetupDependencies ) { const start = createStartServicesGetter(core.getStartServices); diff --git a/x-pack/legacy/plugins/beats_management/readme.md b/x-pack/legacy/plugins/beats_management/readme.md index 301caad683dd57..3414f09deed463 100644 --- a/x-pack/legacy/plugins/beats_management/readme.md +++ b/x-pack/legacy/plugins/beats_management/readme.md @@ -15,7 +15,7 @@ In one shell, from **~/kibana/x-pack**: `node scripts/functional_tests-server.js` In another shell, from **~kibana/x-pack**: -`node ../scripts/functional_test_runner.js --config test/api_integration/config.js`. +`node ../scripts/functional_test_runner.js --config test/api_integration/config.ts`. ### Manual e2e testing diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts index bb1f68e1c03b37..80599f38d982ab 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts @@ -8,6 +8,7 @@ import { Lifecycle, ResponseToolkit } from 'hapi'; import * as t from 'io-ts'; +import { SecurityPluginSetup } from '../../../../../../../plugins/security/server'; import { LicenseType } from '../../../../common/constants/security'; export const internalAuthData = Symbol('internalAuthData'); @@ -39,6 +40,11 @@ export interface BackendFrameworkAdapter { } export interface KibanaLegacyServer { + newPlatform: { + setup: { + plugins: { security: SecurityPluginSetup }; + }; + }; plugins: { xpack_main: { status: { @@ -53,9 +59,6 @@ export interface KibanaLegacyServer { }; }; }; - security: { - getUser: (request: KibanaServerRequest) => any; - }; elasticsearch: { status: { on: (status: 'green' | 'yellow' | 'red', callback: () => void) => void; diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts index 589f34ac746014..1bf9bbb22b3525 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -8,6 +8,7 @@ import { ResponseToolkit } from 'hapi'; import { PathReporter } from 'io-ts/lib/PathReporter'; import { get } from 'lodash'; import { isLeft } from 'fp-ts/lib/Either'; +import { KibanaRequest, LegacyRequest } from '../../../../../../../../src/core/server'; // @ts-ignore import { mirrorPluginStatus } from '../../../../../../server/lib/mirror_plugin_status'; import { @@ -128,13 +129,10 @@ export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { } private async getUser(request: KibanaServerRequest): Promise { - let user; - try { - user = await this.server.plugins.security.getUser(request); - } catch (e) { - return null; - } - if (user === null) { + const user = this.server.newPlatform.setup.plugins.security?.authc.getCurrentUser( + KibanaRequest.from((request as unknown) as LegacyRequest) + ); + if (!user) { return null; } const assertKibanaUser = RuntimeKibanaUser.decode(user); diff --git a/x-pack/legacy/plugins/security/index.ts b/x-pack/legacy/plugins/security/index.ts index 41371fcbc4c652..addeef34f63bfe 100644 --- a/x-pack/legacy/plugins/security/index.ts +++ b/x-pack/legacy/plugins/security/index.ts @@ -6,64 +6,17 @@ import { Root } from 'joi'; import { resolve } from 'path'; -import { Server } from 'src/legacy/server/kbn_server'; -import { KibanaRequest, LegacyRequest } from '../../../../src/core/server'; -// @ts-ignore -import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize'; -import { AuthenticatedUser, SecurityPluginSetup } from '../../../plugins/security/server'; - -/** - * Public interface of the security plugin. - */ -export interface SecurityPlugin { - getUser: (request: LegacyRequest) => Promise; -} - -function getSecurityPluginSetup(server: Server) { - const securityPlugin = server.newPlatform.setup.plugins.security as SecurityPluginSetup; - if (!securityPlugin) { - throw new Error('Kibana Platform Security plugin is not available.'); - } - - return securityPlugin; -} export const security = (kibana: Record) => new kibana.Plugin({ id: 'security', publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'xpack_main'], + require: ['kibana'], configPrefix: 'xpack.security', - uiExports: { - hacks: ['plugins/security/hacks/legacy'], - injectDefaultVars: (server: Server) => { - return { enableSpaceAwarePrivileges: server.config().get('xpack.spaces.enabled') }; - }, - }, - - config(Joi: Root) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }) + uiExports: { hacks: ['plugins/security/hacks/legacy'] }, + config: (Joi: Root) => + Joi.object({ enabled: Joi.boolean().default(true) }) .unknown() - .default(); - }, - - async postInit(server: Server) { - watchStatusAndLicenseToInitialize(server.plugins.xpack_main, this, async () => { - const xpackInfo = server.plugins.xpack_main.info; - if (xpackInfo.isAvailable() && xpackInfo.feature('security').isEnabled()) { - await getSecurityPluginSetup(server).__legacyCompat.registerPrivilegesWithCluster(); - } - }); - }, - - async init(server: Server) { - const securityPlugin = getSecurityPluginSetup(server); - - server.expose({ - getUser: async (request: LegacyRequest) => - securityPlugin.authc.getCurrentUser(KibanaRequest.from(request)), - }); - }, + .default(), + init() {}, }); diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index cb8600ed2c214a..56c427e67ad4cc 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -9,6 +9,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Route, Router, Switch } from 'react-router-dom'; import styled from 'styled-components'; +import { EuiThemeProvider } from '../../../observability/public'; import { CoreStart, AppMountParameters } from '../../../../../src/core/public'; import { ApmPluginSetupDeps } from '../plugin'; import { ApmPluginContext } from '../context/ApmPluginContext'; @@ -18,7 +19,10 @@ import { LocationProvider } from '../context/LocationContext'; import { MatchedRouteProvider } from '../context/MatchedRouteContext'; import { UrlParamsProvider } from '../context/UrlParamsContext'; import { AlertsContextProvider } from '../../../triggers_actions_ui/public'; -import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { + KibanaContextProvider, + useUiSetting$, +} from '../../../../../src/plugins/kibana_react/public'; import { px, unit, units } from '../style/variables'; import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs'; import { APMIndicesPermission } from '../components/app/APMIndicesPermission'; @@ -35,18 +39,22 @@ const MainContainer = styled.div` `; const App = () => { + const [darkMode] = useUiSetting$('theme:darkMode'); + return ( - - - - - - {routes.map((route, i) => ( - - ))} - - - + + + + + + + {routes.map((route, i) => ( + + ))} + + + + ); }; diff --git a/x-pack/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md index ceed5e6c39716a..cb694712d7c97b 100644 --- a/x-pack/plugins/apm/readme.md +++ b/x-pack/plugins/apm/readme.md @@ -83,13 +83,13 @@ For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) **Start server** ``` -node scripts/functional_tests_server --config x-pack/test/api_integration/config.js +node scripts/functional_tests_server --config x-pack/test/api_integration/config.ts ``` **Run tests** ``` -node scripts/functional_test_runner --config x-pack/test/api_integration/config.js --grep='APM specs' +node scripts/functional_test_runner --config x-pack/test/api_integration/config.ts --grep='APM specs' ``` APM tests are located in `x-pack/test/api_integration/apis/apm`. diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.ts index c7a17197ca7787..892f8f0ddd1051 100644 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.ts +++ b/x-pack/plugins/apm/server/lib/helpers/es_client.ts @@ -19,6 +19,7 @@ import { ESSearchRequest, ESSearchResponse, } from '../../../typings/elasticsearch'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/server'; import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames'; import { pickKeys } from '../../../common/utils/pick_keys'; import { APMRequestHandlerContext } from '../../routes/typings'; @@ -95,7 +96,7 @@ async function getParamsForSearchRequest( savedObjectsClient: context.core.savedObjects.client, config: context.config, }), - uiSettings.client.get('search:includeFrozen'), + uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN), ]); // Get indices for legacy data filter (only those which apply) diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts index 7256657903aab0..14409ae166a840 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts @@ -7,6 +7,7 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; import { SetupInitializer } from '../../plugin'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; export const metricElementInitializer: SetupInitializer = (core, setup) => { return () => ({ @@ -23,7 +24,7 @@ export const metricElementInitializer: SetupInitializer = (core, | metric "Countries" metricFont={font size=48 family="${openSans.value}" color="#000000" align="center" lHeight=48} labelFont={font size=14 family="${openSans.value}" color="#000000" align="center"} - metricFormat="${core.uiSettings.get('format:number:defaultPattern')}" + metricFormat="${core.uiSettings.get(UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN)}" | render`, }); }; 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/canvas/canvas_plugin_src/renderers/time_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.tsx index ef5bfb70d4b3db..25278adcf45297 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.tsx @@ -7,6 +7,7 @@ import ReactDOM from 'react-dom'; import React from 'react'; import { toExpression } from '@kbn/interpreter/common'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; import { syncFilterExpression } from '../../../public/lib/sync_filter_expression'; import { RendererStrings } from '../../../i18n'; import { TimeFilter } from './components'; @@ -20,7 +21,7 @@ const { timeFilter: strings } = RendererStrings; export const timeFilterFactory: StartInitializer> = (core, plugins) => { const { uiSettings } = core; - const customQuickRanges = (uiSettings.get('timepicker:quickRanges') || []).map( + const customQuickRanges = (uiSettings.get(UI_SETTINGS.TIMEPICKER_QUICK_RANGES) || []).map( ({ from, to, display }: { from: string; to: string; display: string }) => ({ start: from, end: to, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts index 4025d4deaf997f..5a3e3904f4f234 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts @@ -11,6 +11,7 @@ import { templateFromReactComponent } from '../../../../public/lib/template_from import { ArgumentFactory } from '../../../../types/arguments'; import { ArgumentStrings } from '../../../../i18n'; import { SetupInitializer } from '../../../plugin'; +import { UI_SETTINGS } from '../../../../../../../src/plugins/data/public'; const { NumberFormat: strings } = ArgumentStrings; @@ -19,11 +20,11 @@ export const numberFormatInitializer: SetupInitializer { const formatMap = { - NUMBER: core.uiSettings.get('format:number:defaultPattern'), - PERCENT: core.uiSettings.get('format:percent:defaultPattern'), - CURRENCY: core.uiSettings.get('format:currency:defaultPattern'), + NUMBER: core.uiSettings.get(UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN), + PERCENT: core.uiSettings.get(UI_SETTINGS.FORMAT_PERCENT_DEFAULT_PATTERN), + CURRENCY: core.uiSettings.get(UI_SETTINGS.FORMAT_CURRENCY_DEFAULT_PATTERN), DURATION: '00:00:00', - BYTES: core.uiSettings.get('format:bytes:defaultPattern'), + BYTES: core.uiSettings.get(UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN), }; const numberFormats = [ diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/metric.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/metric.ts index 93912b7b0517f5..11bee46088576e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/metric.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/metric.ts @@ -7,6 +7,7 @@ import { openSans } from '../../../common/lib/fonts'; import { ViewStrings } from '../../../i18n'; import { SetupInitializer } from '../../plugin'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; const { Metric: strings } = ViewStrings; @@ -22,7 +23,7 @@ export const metricInitializer: SetupInitializer = (core, plugin) => { displayName: strings.getMetricFormatDisplayName(), help: strings.getMetricFormatHelp(), argType: 'numberFormat', - default: `"${core.uiSettings.get('format:number:defaultPattern')}"`, + default: `"${core.uiSettings.get(UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN)}"`, }, { name: '_', diff --git a/x-pack/plugins/dashboard_enhanced/kibana.json b/x-pack/plugins/dashboard_enhanced/kibana.json index f416ca97f71107..37211ea5371797 100644 --- a/x-pack/plugins/dashboard_enhanced/kibana.json +++ b/x-pack/plugins/dashboard_enhanced/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["data", "advancedUiActions", "drilldowns", "embeddable", "dashboard", "share"], + "requiredPlugins": ["data", "uiActionsEnhanced", "drilldowns", "embeddable", "dashboard", "share"], "configPath": ["xpack", "dashboardEnhanced"] } diff --git a/x-pack/plugins/dashboard_enhanced/public/plugin.ts b/x-pack/plugins/dashboard_enhanced/public/plugin.ts index c258a4148f84a0..413f5a7afe3564 100644 --- a/x-pack/plugins/dashboard_enhanced/public/plugin.ts +++ b/x-pack/plugins/dashboard_enhanced/public/plugin.ts @@ -9,19 +9,19 @@ import { SharePluginStart, SharePluginSetup } from '../../../../src/plugins/shar import { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { DashboardDrilldownsService } from './services'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; -import { AdvancedUiActionsSetup, AdvancedUiActionsStart } from '../../advanced_ui_actions/public'; +import { AdvancedUiActionsSetup, AdvancedUiActionsStart } from '../../ui_actions_enhanced/public'; import { DrilldownsSetup, DrilldownsStart } from '../../drilldowns/public'; import { DashboardStart } from '../../../../src/plugins/dashboard/public'; export interface SetupDependencies { - advancedUiActions: AdvancedUiActionsSetup; + uiActionsEnhanced: AdvancedUiActionsSetup; drilldowns: DrilldownsSetup; embeddable: EmbeddableSetup; share: SharePluginSetup; } export interface StartDependencies { - advancedUiActions: AdvancedUiActionsStart; + uiActionsEnhanced: AdvancedUiActionsStart; data: DataPublicPluginStart; drilldowns: DrilldownsStart; embeddable: EmbeddableStart; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx index 555acf1fca5ffa..309e6cbf53a3db 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx @@ -8,7 +8,7 @@ import { FlyoutEditDrilldownAction, FlyoutEditDrilldownParams } from './flyout_e import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { drilldownsPluginMock } from '../../../../../../drilldowns/public/mocks'; import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; -import { uiActionsEnhancedPluginMock } from '../../../../../../advanced_ui_actions/public/mocks'; +import { uiActionsEnhancedPluginMock } from '../../../../../../ui_actions_enhanced/public/mocks'; import { EnhancedEmbeddable } from '../../../../../../embeddable_enhanced/public'; import { MockEmbeddable, enhanceEmbeddable } from '../test_helpers'; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.test.tsx index ec3a78e97eae4a..9a4ecb2d4bfb0a 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { render, cleanup, act } from '@testing-library/react/pure'; import { MenuItem } from './menu_item'; import { createStateContainer } from '../../../../../../../../src/plugins/kibana_utils/public'; -import { UiActionsEnhancedDynamicActionManager as DynamicActionManager } from '../../../../../../advanced_ui_actions/public'; +import { UiActionsEnhancedDynamicActionManager as DynamicActionManager } from '../../../../../../ui_actions_enhanced/public'; import { EnhancedEmbeddable } from '../../../../../../embeddable_enhanced/public'; import '@testing-library/jest-dom'; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts index cccacf701a9ada..e831f87baa11c1 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts @@ -10,9 +10,9 @@ import { UiActionsEnhancedMemoryActionStorage as MemoryActionStorage, UiActionsEnhancedDynamicActionManager as DynamicActionManager, AdvancedUiActionsStart, -} from '../../../../../advanced_ui_actions/public'; +} from '../../../../../ui_actions_enhanced/public'; import { TriggerContextMapping } from '../../../../../../../src/plugins/ui_actions/public'; -import { uiActionsEnhancedPluginMock } from '../../../../../advanced_ui_actions/public/mocks'; +import { uiActionsEnhancedPluginMock } from '../../../../../ui_actions_enhanced/public/mocks'; export class MockEmbeddable extends Embeddable { public rootType = 'dashboard'; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts index f5926cd6961c2d..4325e3309b898a 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts @@ -41,7 +41,7 @@ export class DashboardDrilldownsService { setupDrilldowns( core: CoreSetup, - { advancedUiActions: uiActions }: SetupDependencies + { uiActionsEnhanced: uiActions }: SetupDependencies ) { const start = createStartServicesGetter(core.getStartServices); const getDashboardUrlGenerator = () => { diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx index c94d19d28e6da5..6ce7dccd3a3ec8 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx @@ -101,13 +101,13 @@ describe('.execute() & getHref', () => { }, }, plugins: { - advancedUiActions: {}, + uiActionsEnhanced: {}, data: { actions: dataPluginActions, }, }, self: {}, - })) as unknown) as StartServicesGetter>, + })) as unknown) as StartServicesGetter>, getDashboardUrlGenerator: () => new UrlGeneratorsService().setup(coreMock.createSetup()).registerUrlGenerator( createDashboardUrlGenerator(() => diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index 7ff84a75dd52ca..26a69132cffb17 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -10,7 +10,7 @@ import { DashboardUrlGenerator } from '../../../../../../../src/plugins/dashboar import { ActionContext, Config } from './types'; import { CollectConfigContainer } from './components'; import { DASHBOARD_TO_DASHBOARD_DRILLDOWN } from './constants'; -import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../../advanced_ui_actions/public'; +import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../../ui_actions_enhanced/public'; import { txtGoToDashboard } from './i18n'; import { esFilters } from '../../../../../../../src/plugins/data/public'; import { VisualizeEmbeddableContract } from '../../../../../../../src/plugins/visualizations/public'; @@ -22,7 +22,7 @@ import { StartServicesGetter } from '../../../../../../../src/plugins/kibana_uti import { StartDependencies } from '../../../plugin'; export interface Params { - start: StartServicesGetter>; + start: StartServicesGetter>; getDashboardUrlGenerator: () => DashboardUrlGenerator; } diff --git a/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts index c493e8ce86781b..3a511c7b5a1767 100644 --- a/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts @@ -11,6 +11,7 @@ import { ISearchContext, ISearch, getEsPreference, + UI_SETTINGS, } from '../../../../../src/plugins/data/public'; import { IEnhancedEsSearchRequest, EnhancedSearchParams } from '../../common'; import { ASYNC_SEARCH_STRATEGY } from './async_search_strategy'; @@ -27,7 +28,7 @@ export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider { const params: EnhancedSearchParams = { - ignoreThrottled: !context.core.uiSettings.get('search:includeFrozen'), + ignoreThrottled: !context.core.uiSettings.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN), preference: getEsPreference(context.core.uiSettings), ...request.params, }; diff --git a/x-pack/plugins/drilldowns/kibana.json b/x-pack/plugins/drilldowns/kibana.json index 678c054aa322c4..1614f94b488fdb 100644 --- a/x-pack/plugins/drilldowns/kibana.json +++ b/x-pack/plugins/drilldowns/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["uiActions", "embeddable", "advancedUiActions"], + "requiredPlugins": ["uiActions", "embeddable", "uiActionsEnhanced"], "configPath": ["xpack", "drilldowns"] } diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx index a186feec339247..5fde4fc79e4333 100644 --- a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx @@ -12,13 +12,13 @@ import { dashboardFactory, urlFactory, // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../advanced_ui_actions/public/components/action_wizard/test_data'; +} from '../../../../ui_actions_enhanced/public/components/action_wizard/test_data'; import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; import { StubBrowserStorage } from '../../../../../../src/test_utils/public/stub_browser_storage'; import { mockDynamicActionManager } from './test_data'; const FlyoutManageDrilldowns = createFlyoutManageDrilldowns({ - advancedUiActions: { + uiActionsEnhanced: { getActionFactories() { return [dashboardFactory, urlFactory]; }, diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx index 0f7f0cb22760b8..32cbec795d0924 100644 --- a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx @@ -11,7 +11,7 @@ import { createFlyoutManageDrilldowns } from './connected_flyout_manage_drilldow import { dashboardFactory, urlFactory, -} from '../../../../advanced_ui_actions/public/components/action_wizard/test_data'; +} from '../../../../ui_actions_enhanced/public/components/action_wizard/test_data'; import { StubBrowserStorage } from '../../../../../../src/test_utils/public/stub_browser_storage'; import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; import { mockDynamicActionManager } from './test_data'; @@ -24,7 +24,7 @@ import { toastDrilldownsCRUDError } from './i18n'; const storage = new Storage(new StubBrowserStorage()); const notifications = coreMock.createStart().notifications; const FlyoutManageDrilldowns = createFlyoutManageDrilldowns({ - advancedUiActions: { + uiActionsEnhanced: { getActionFactories() { return [dashboardFactory, urlFactory]; }, diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx index 3c9d2d2a86fb11..45cf7365ebd917 100644 --- a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx @@ -7,12 +7,12 @@ import React, { useEffect, useState } from 'react'; import useMountedState from 'react-use/lib/useMountedState'; import { - AdvancedUiActionsActionFactory as ActionFactory, + UiActionsEnhancedActionFactory as ActionFactory, AdvancedUiActionsStart, UiActionsEnhancedDynamicActionManager as DynamicActionManager, UiActionsEnhancedSerializedAction, UiActionsEnhancedSerializedEvent, -} from '../../../../advanced_ui_actions/public'; +} from '../../../../ui_actions_enhanced/public'; import { NotificationsStart } from '../../../../../../src/core/public'; import { DrilldownWizardConfig, FlyoutDrilldownWizard } from '../flyout_drilldown_wizard'; import { FlyoutListManageDrilldowns } from '../flyout_list_manage_drilldowns'; @@ -48,17 +48,17 @@ enum Routes { } export function createFlyoutManageDrilldowns({ - advancedUiActions, + uiActionsEnhanced, storage, notifications, }: { - advancedUiActions: AdvancedUiActionsStart; + uiActionsEnhanced: AdvancedUiActionsStart; storage: IStorageWrapper; notifications: NotificationsStart; }) { // fine to assume this is static, // because all action factories should be registered in setup phase - const allActionFactories = advancedUiActions.getActionFactories(); + const allActionFactories = uiActionsEnhanced.getActionFactories(); const allActionFactoriesById = allActionFactories.reduce((acc, next) => { acc[next.id] = next; return acc; diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/test_data.ts b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/test_data.ts index c9cb0b0eb1cb38..d585fa0692e8c1 100644 --- a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/test_data.ts +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/test_data.ts @@ -9,7 +9,7 @@ import { UiActionsEnhancedDynamicActionManager as DynamicActionManager, UiActionsEnhancedDynamicActionManagerState as DynamicActionManagerState, UiActionsEnhancedSerializedAction, -} from '../../../../advanced_ui_actions/public'; +} from '../../../../ui_actions_enhanced/public'; import { TriggerContextMapping } from '../../../../../../src/plugins/ui_actions/public'; import { createStateContainer } from '../../../../../../src/plugins/kibana_utils/common'; diff --git a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx index add8b748afee94..be048bf920602a 100644 --- a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx +++ b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx @@ -14,8 +14,8 @@ import { dashboardFactory, urlFactory, // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../advanced_ui_actions/public/components/action_wizard/test_data'; -import { AdvancedUiActionsActionFactory as ActionFactory } from '../../../../advanced_ui_actions/public/'; +} from '../../../../ui_actions_enhanced/public/components/action_wizard/test_data'; +import { UiActionsEnhancedActionFactory as ActionFactory } from '../../../../ui_actions_enhanced/public/'; storiesOf('components/FlyoutDrilldownWizard', module) .add('default', () => { diff --git a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx index 84c1a04a71d156..87f886817517f7 100644 --- a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx +++ b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx @@ -16,7 +16,7 @@ import { txtEditDrilldownTitle, } from './i18n'; import { DrilldownHelloBar } from '../drilldown_hello_bar'; -import { AdvancedUiActionsActionFactory as ActionFactory } from '../../../../advanced_ui_actions/public'; +import { UiActionsEnhancedActionFactory as ActionFactory } from '../../../../ui_actions_enhanced/public'; export interface DrilldownWizardConfig { name: string; diff --git a/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx index 38168377b02bd7..1813851d728db6 100644 --- a/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx +++ b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx @@ -8,9 +8,9 @@ import React from 'react'; import { EuiFieldText, EuiForm, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { txtDrilldownAction, txtNameOfDrilldown, txtUntitledDrilldown } from './i18n'; import { - AdvancedUiActionsActionFactory as ActionFactory, + UiActionsEnhancedActionFactory as ActionFactory, ActionWizard, -} from '../../../../advanced_ui_actions/public'; +} from '../../../../ui_actions_enhanced/public'; const noopFn = () => {}; diff --git a/x-pack/plugins/drilldowns/public/plugin.ts b/x-pack/plugins/drilldowns/public/plugin.ts index 0108e04df9c99a..32176241c102fa 100644 --- a/x-pack/plugins/drilldowns/public/plugin.ts +++ b/x-pack/plugins/drilldowns/public/plugin.ts @@ -6,18 +6,18 @@ import { CoreStart, CoreSetup, Plugin } from 'src/core/public'; import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; -import { AdvancedUiActionsSetup, AdvancedUiActionsStart } from '../../advanced_ui_actions/public'; +import { AdvancedUiActionsSetup, AdvancedUiActionsStart } from '../../ui_actions_enhanced/public'; import { createFlyoutManageDrilldowns } from './components/connected_flyout_manage_drilldowns'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; export interface SetupDependencies { uiActions: UiActionsSetup; - advancedUiActions: AdvancedUiActionsSetup; + uiActionsEnhanced: AdvancedUiActionsSetup; } export interface StartDependencies { uiActions: UiActionsStart; - advancedUiActions: AdvancedUiActionsStart; + uiActionsEnhanced: AdvancedUiActionsStart; } // eslint-disable-next-line @@ -36,7 +36,7 @@ export class DrilldownsPlugin public start(core: CoreStart, plugins: StartDependencies): StartContract { return { FlyoutManageDrilldowns: createFlyoutManageDrilldowns({ - advancedUiActions: plugins.advancedUiActions, + uiActionsEnhanced: plugins.uiActionsEnhanced, storage: new Storage(localStorage), notifications: core.notifications, }), diff --git a/x-pack/plugins/embeddable_enhanced/kibana.json b/x-pack/plugins/embeddable_enhanced/kibana.json index 780a1d5d89870e..5663671de7bd9c 100644 --- a/x-pack/plugins/embeddable_enhanced/kibana.json +++ b/x-pack/plugins/embeddable_enhanced/kibana.json @@ -3,5 +3,5 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["embeddable", "advancedUiActions"] + "requiredPlugins": ["embeddable", "uiActionsEnhanced"] } diff --git a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.test.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.test.ts index f8b3a9dfb92d07..5c5d98d75295de 100644 --- a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.test.ts +++ b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.test.ts @@ -9,7 +9,7 @@ import { EmbeddableActionStorage, EmbeddableWithDynamicActionsInput, } from './embeddable_action_storage'; -import { UiActionsEnhancedSerializedEvent } from '../../../advanced_ui_actions/public'; +import { UiActionsEnhancedSerializedEvent } from '../../../ui_actions_enhanced/public'; import { of } from '../../../../../src/plugins/kibana_utils/public'; class TestEmbeddable extends Embeddable { diff --git a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts index e93674ba650a70..fdc42585a80ce3 100644 --- a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts +++ b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts @@ -7,7 +7,7 @@ import { UiActionsEnhancedAbstractActionStorage as AbstractActionStorage, UiActionsEnhancedSerializedEvent as SerializedEvent, -} from '../../../advanced_ui_actions/public'; +} from '../../../ui_actions_enhanced/public'; import { EmbeddableInput, EmbeddableOutput, diff --git a/x-pack/plugins/embeddable_enhanced/public/plugin.ts b/x-pack/plugins/embeddable_enhanced/public/plugin.ts index d26acb4459a717..e6413ac03aeaef 100644 --- a/x-pack/plugins/embeddable_enhanced/public/plugin.ts +++ b/x-pack/plugins/embeddable_enhanced/public/plugin.ts @@ -27,7 +27,7 @@ import { UiActionsEnhancedDynamicActionManager as DynamicActionManager, AdvancedUiActionsSetup, AdvancedUiActionsStart, -} from '../../advanced_ui_actions/public'; +} from '../../ui_actions_enhanced/public'; import { PanelNotificationsAction, ACTION_PANEL_NOTIFICATIONS } from './actions'; declare module '../../../../src/plugins/ui_actions/public' { @@ -38,12 +38,12 @@ declare module '../../../../src/plugins/ui_actions/public' { export interface SetupDependencies { embeddable: EmbeddableSetup; - advancedUiActions: AdvancedUiActionsSetup; + uiActionsEnhanced: AdvancedUiActionsSetup; } export interface StartDependencies { embeddable: EmbeddableStart; - advancedUiActions: AdvancedUiActionsStart; + uiActionsEnhanced: AdvancedUiActionsStart; } // eslint-disable-next-line @@ -56,20 +56,20 @@ export class EmbeddableEnhancedPlugin implements Plugin { constructor(protected readonly context: PluginInitializerContext) {} - private uiActions?: StartDependencies['advancedUiActions']; + private uiActions?: StartDependencies['uiActionsEnhanced']; public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { this.setCustomEmbeddableFactoryProvider(plugins); const panelNotificationAction = new PanelNotificationsAction(); - plugins.advancedUiActions.registerAction(panelNotificationAction); - plugins.advancedUiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, panelNotificationAction.id); + plugins.uiActionsEnhanced.registerAction(panelNotificationAction); + plugins.uiActionsEnhanced.attachAction(PANEL_NOTIFICATION_TRIGGER, panelNotificationAction.id); return {}; } public start(core: CoreStart, plugins: StartDependencies): StartContract { - this.uiActions = plugins.advancedUiActions; + this.uiActions = plugins.uiActionsEnhanced; return {}; } diff --git a/x-pack/plugins/embeddable_enhanced/public/types.ts b/x-pack/plugins/embeddable_enhanced/public/types.ts index 924605be332b21..4f5c316f2fc17c 100644 --- a/x-pack/plugins/embeddable_enhanced/public/types.ts +++ b/x-pack/plugins/embeddable_enhanced/public/types.ts @@ -5,7 +5,7 @@ */ import { IEmbeddable } from '../../../../src/plugins/embeddable/public'; -import { UiActionsEnhancedDynamicActionManager as DynamicActionManager } from '../../advanced_ui_actions/public'; +import { UiActionsEnhancedDynamicActionManager as DynamicActionManager } from '../../ui_actions_enhanced/public'; export type EnhancedEmbeddable = E & { enhancements: { diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index 0144e573fc1462..ada86adf84cfd2 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -51,7 +51,7 @@ describe('doesIlmPolicyExist', () => { await clusterClientAdapter.doesIlmPolicyExist('foo'); expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('transport.request', { method: 'GET', - path: '_ilm/policy/foo', + path: '/_ilm/policy/foo', }); }); @@ -78,7 +78,7 @@ describe('createIlmPolicy', () => { await clusterClientAdapter.createIlmPolicy('foo', { args: true }); expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('transport.request', { method: 'PUT', - path: '_ilm/policy/foo', + path: '/_ilm/policy/foo', body: { args: true }, }); }); diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index 7fd239ca493692..a036bfb74e4081 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -41,7 +41,7 @@ export class ClusterClientAdapter { public async doesIlmPolicyExist(policyName: string): Promise { const request = { method: 'GET', - path: `_ilm/policy/${policyName}`, + path: `/_ilm/policy/${policyName}`, }; try { await this.callEs('transport.request', request); @@ -55,7 +55,7 @@ export class ClusterClientAdapter { public async createIlmPolicy(policyName: string, policy: unknown): Promise { const request = { method: 'PUT', - path: `_ilm/policy/${policyName}`, + path: `/_ilm/policy/${policyName}`, body: policy, }; try { diff --git a/x-pack/plugins/graph/server/routes/search.ts b/x-pack/plugins/graph/server/routes/search.ts index ffca273d66c71a..645e6b520013fd 100644 --- a/x-pack/plugins/graph/server/routes/search.ts +++ b/x-pack/plugins/graph/server/routes/search.ts @@ -7,6 +7,7 @@ import { IRouter } from 'kibana/server'; import { schema } from '@kbn/config-schema'; import { LicenseState, verifyApiAccess } from '../lib/license_state'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/server'; export function registerSearchRoute({ router, @@ -41,7 +42,7 @@ export function registerSearchRoute({ response ) => { verifyApiAccess(licenseState); - const includeFrozen = await uiSettings.get('search:includeFrozen'); + const includeFrozen = await uiSettings.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); try { return response.ok({ body: { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js index bfe1bbb04338c2..28bc8671f29e22 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js @@ -140,6 +140,41 @@ export const MinAgeInput = (props) => { defaultMessage: 'hours from index creation', } ); + + minutesOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.creationMinutesOptionLabel', + { + defaultMessage: 'minutes from index creation', + } + ); + + secondsOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.creationSecondsOptionLabel', + { + defaultMessage: 'seconds from index creation', + } + ); + + millisecondsOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.creationMilliSecondsOptionLabel', + { + defaultMessage: 'milliseconds from index creation', + } + ); + + microsecondsOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.creationMicroSecondsOptionLabel', + { + defaultMessage: 'microseconds from index creation', + } + ); + + nanosecondsOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.creationNanoSecondsOptionLabel', + { + defaultMessage: 'nanoseconds from index creation', + } + ); } return ( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js index 789de0f528b1b7..03538fad9aa83a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js @@ -270,7 +270,9 @@ export const getLifecycle = (state) => { if (phaseName === PHASE_DELETE) { accum[phaseName].actions = { ...accum[phaseName].actions, - delete: {}, + delete: { + ...accum[phaseName].actions.delete, + }, }; } } diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts index 2eb635e19be482..7bf3f96e2b2ef0 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts @@ -67,7 +67,7 @@ const warmPhaseSchema = schema.maybe( actions: schema.object({ set_priority: setPrioritySchema, unfollow: unfollowSchema, - read_only: schema.maybe(schema.object({})), // Readonly has no options + readonly: schema.maybe(schema.object({})), // Readonly has no options allocate: allocateSchema, shrink: schema.maybe( schema.object({ @@ -91,6 +91,11 @@ const coldPhaseSchema = schema.maybe( unfollow: unfollowSchema, allocate: allocateSchema, freeze: schema.maybe(schema.object({})), // Freeze has no options + searchable_snapshot: schema.maybe( + schema.object({ + snapshot_repository: schema.string(), + }) + ), }), }) ); @@ -104,7 +109,11 @@ const deletePhaseSchema = schema.maybe( policy: schema.string(), }) ), - delete: schema.maybe(schema.object({})), // Delete has no options + delete: schema.maybe( + schema.object({ + delete_searchable_snapshot: schema.maybe(schema.boolean()), + }) + ), }), }) ); 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/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index f4c7332a88e1d2..db5cfb1416dec3 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -288,6 +288,7 @@ export const Expressions: React.FC = (props) => { />
+
{ - setLegendOptions((previous) => ({ ...previous, steps: parseInt(e.target.value, 10) })); + const steps = parseInt(e.target.value, 10); + setLegendOptions((previous) => ({ ...previous, steps })); }, [setLegendOptions] ); const handlePaletteChange = useCallback( (e) => { - setLegendOptions((previous) => ({ ...previous, palette: e.target.value })); + const palette = e.target.value; + setLegendOptions((previous) => ({ ...previous, palette })); }, [setLegendOptions] ); diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.tsx index ef6486eac0fdb1..afee0c0498187f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.tsx @@ -6,6 +6,7 @@ import { EuiSuperDatePicker, OnRefreshChangeProps, OnTimeChangeProps } from '@elastic/eui'; import React, { useCallback } from 'react'; +import { UI_SETTINGS } from '../../../../../../../../src/plugins/data/public'; import { euiStyled } from '../../../../../../observability/public'; import { MetricsTimeInput } from '../hooks/use_metrics_time'; import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting'; @@ -22,7 +23,7 @@ interface MetricsTimeControlsProps { } export const MetricsTimeControls = (props: MetricsTimeControlsProps) => { - const [timepickerQuickRanges] = useKibanaUiSetting('timepicker:quickRanges'); + const [timepickerQuickRanges] = useKibanaUiSetting(UI_SETTINGS.TIMEPICKER_QUICK_RANGES); const { onChangeTimeRange, onRefresh, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx index 7ad1d943a98969..1471efbd21e18a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { IIndexPattern, UI_SETTINGS } from '../../../../../../../../src/plugins/data/public'; import { MetricsExplorerMetric, MetricsExplorerAggregation, @@ -61,7 +61,7 @@ export const MetricsExplorerToolbar = ({ onViewStateChange, }: Props) => { const isDefaultOptions = options.aggregation === 'avg' && options.metrics.length === 0; - const [timepickerQuickRanges] = useKibanaUiSetting('timepicker:quickRanges'); + const [timepickerQuickRanges] = useKibanaUiSetting(UI_SETTINGS.TIMEPICKER_QUICK_RANGES); const commonlyUsedRanges = mapKibanaQuickRangesToDatePickerRanges(timepickerQuickRanges); return ( diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index 7c0c535795638f..79c276a1e58f7e 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -32,7 +32,7 @@ import { } from '../../../../../../../src/core/server'; import { RequestHandler } from '../../../../../../../src/core/server'; import { InfraConfig } from '../../../plugin'; -import { IndexPatternsFetcher } from '../../../../../../../src/plugins/data/server'; +import { IndexPatternsFetcher, UI_SETTINGS } from '../../../../../../../src/plugins/data/server'; export class KibanaFramework { public router: IRouter; @@ -197,10 +197,10 @@ export class KibanaFramework { ) { const { elasticsearch, uiSettings } = requestContext.core; - const includeFrozen = await uiSettings.client.get('search:includeFrozen'); + const includeFrozen = await uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); if (endpoint === 'msearch') { const maxConcurrentShardRequests = await uiSettings.client.get( - 'courier:maxConcurrentShardRequests' + UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS ); if (maxConcurrentShardRequests > 0) { params = { ...params, max_concurrent_shard_requests: maxConcurrentShardRequests }; 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/README.md b/x-pack/plugins/ingest_manager/README.md index f0c2466b25b047..50c42544b8bdc7 100644 --- a/x-pack/plugins/ingest_manager/README.md +++ b/x-pack/plugins/ingest_manager/README.md @@ -52,12 +52,12 @@ This plugin follows the `common`, `server`, `public` structure from the [Archite 1. In one terminal, change to the `x-pack` directory and start the test server with ``` - node scripts/functional_tests_server.js --config test/api_integration/config.js + node scripts/functional_tests_server.js --config test/api_integration/config.ts ``` 1. in a second terminal, run the tests from the Kibana root directory with ``` - node scripts/functional_test_runner.js --config x-pack/test/api_integration/config.js + node scripts/functional_test_runner.js --config x-pack/test/api_integration/config.ts ``` #### EPM 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/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index f1a2edd2d554f7..33f4f46681a283 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -19,6 +19,7 @@ import { FilterManager, IFieldType, IIndexPattern, + UI_SETTINGS, } from '../../../../../src/plugins/data/public'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; const dataStartMock = dataPluginMock.createStartContract(); @@ -183,7 +184,7 @@ describe('Lens App', () => { jest.fn((type) => { if (type === 'timepicker:timeDefaults') { return { from: 'now-7d', to: 'now' }; - } else if (type === 'search:queryLanguage') { + } else if (type === UI_SETTINGS.SEARCH_QUERY_LANGUAGE) { return 'kuery'; } else { return []; diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index ffa59a6fb6bc98..1349d33983fcd0 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -27,6 +27,7 @@ import { IndexPattern as IndexPatternInstance, IndexPatternsContract, SavedQuery, + UI_SETTINGS, } from '../../../../../src/plugins/data/public'; interface State { @@ -76,7 +77,8 @@ export function App({ onAppLeave: AppMountParameters['onAppLeave']; }) { const language = - storage.get('kibana.userQueryLanguage') || core.uiSettings.get('search:queryLanguage'); + storage.get('kibana.userQueryLanguage') || + core.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE); const [state, setState] = useState(() => { const currentRange = data.query.timefilter.timefilter.getTime(); @@ -413,7 +415,7 @@ export function App({ query: '', language: storage.get('kibana.userQueryLanguage') || - core.uiSettings.get('search:queryLanguage'), + core.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE), }, })); }} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx index e665e8b8dd326a..defc142d4976ea 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx @@ -12,6 +12,7 @@ import { EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; import { coreMock } from 'src/core/public/mocks'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { UI_SETTINGS } from '../../../../../../../src/plugins/data/public'; import { dataPluginMock, getCalculateAutoTimeExpression, @@ -23,7 +24,7 @@ const dataStart = dataPluginMock.createStartContract(); dataStart.search.aggs.calculateAutoTimeExpression = getCalculateAutoTimeExpression({ ...coreMock.createStart().uiSettings, get: (path: string) => { - if (path === 'histogram:maxBars') { + if (path === UI_SETTINGS.HISTOGRAM_MAX_BARS) { return 10; } }, 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/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index 23cf9e7ff818fc..cd25cb57295115 100644 --- a/x-pack/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -8,6 +8,7 @@ import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist import { CoreSetup, IUiSettingsClient } from 'kibana/public'; import moment from 'moment-timezone'; import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; import { xyVisualization } from './xy_visualization'; import { xyChart, getXyChartRenderer } from './xy_expression'; import { legendConfig, xConfig, layerConfig } from './types'; @@ -47,7 +48,7 @@ export class XyVisualization { ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme, timeZone: getTimeZone(core.uiSettings), - histogramBarTarget: core.uiSettings.get('histogram:barTarget'), + histogramBarTarget: core.uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), }) ); diff --git a/x-pack/plugins/lens/readme.md b/x-pack/plugins/lens/readme.md index 60b4266edadb3d..70d7f16b0f7fc9 100644 --- a/x-pack/plugins/lens/readme.md +++ b/x-pack/plugins/lens/readme.md @@ -11,4 +11,4 @@ Run all tests from the `x-pack` root directory - You may want to comment out all imports except for Lens in the config file. - API Functional tests: - Run `node scripts/functional_tests_server` - - Run `node ../scripts/functional_test_runner.js --config ./test/api_integration/config.js --grep=Lens` + - Run `node ../scripts/functional_test_runner.js --config ./test/api_integration/config.ts --grep=Lens` diff --git a/x-pack/plugins/licensing/server/index.ts b/x-pack/plugins/licensing/server/index.ts index 76e65afc595c4c..ba577660d865cc 100644 --- a/x-pack/plugins/licensing/server/index.ts +++ b/x-pack/plugins/licensing/server/index.ts @@ -10,6 +10,7 @@ import { LicensingPlugin } from './plugin'; export const plugin = (context: PluginInitializerContext) => new LicensingPlugin(context); export * from '../common/types'; +export { FeatureUsageServiceSetup, FeatureUsageServiceStart } from './services'; export * from './types'; export { config } from './licensing_config'; export { CheckLicense, wrapRouteWithLicenseCheck } from './wrap_route_with_license_check'; diff --git a/x-pack/plugins/maps/public/angular/get_initial_query.js b/x-pack/plugins/maps/public/angular/get_initial_query.js index 4f61142413671d..84f431cf2b3b63 100644 --- a/x-pack/plugins/maps/public/angular/get_initial_query.js +++ b/x-pack/plugins/maps/public/angular/get_initial_query.js @@ -5,6 +5,7 @@ */ import { getUiSettings } from '../kibana_services'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; export function getInitialQuery({ mapStateJSON, appState = {}, userQueryLanguage }) { const settings = getUiSettings(); @@ -22,6 +23,6 @@ export function getInitialQuery({ mapStateJSON, appState = {}, userQueryLanguage return { query: '', - language: userQueryLanguage || settings.get('search:queryLanguage'), + language: userQueryLanguage || settings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE), }; } diff --git a/x-pack/plugins/maps/public/angular/get_initial_refresh_config.js b/x-pack/plugins/maps/public/angular/get_initial_refresh_config.js index f13e435cd1d5cf..17a50c6c5f6852 100644 --- a/x-pack/plugins/maps/public/angular/get_initial_refresh_config.js +++ b/x-pack/plugins/maps/public/angular/get_initial_refresh_config.js @@ -5,6 +5,7 @@ */ import { getUiSettings } from '../kibana_services'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; export function getInitialRefreshConfig({ mapStateJSON, globalState = {} }) { const uiSettings = getUiSettings(); @@ -16,7 +17,7 @@ export function getInitialRefreshConfig({ mapStateJSON, globalState = {} }) { } } - const defaultRefreshConfig = uiSettings.get('timepicker:refreshIntervalDefaults'); + const defaultRefreshConfig = uiSettings.get(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS); const refreshInterval = { ...defaultRefreshConfig, ...globalState.refreshInterval }; return { isPaused: refreshInterval.pause, diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js b/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js index 75b6b5d66f1de6..45c7507160e980 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js @@ -20,6 +20,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { UI_SETTINGS } from '../../../../../../../src/plugins/data/public'; import { getIndexPatternService, getUiSettings, getData } from '../../../kibana_services'; import { GlobalFilterCheckbox } from '../../../components/global_filter_checkbox'; @@ -101,7 +102,7 @@ export class FilterEditor extends Component { query={ layerQuery ? layerQuery - : { language: uiSettings.get('search:queryLanguage'), query: '' } + : { language: uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE), query: '' } } onQuerySubmit={this._onQueryChange} indexPatterns={this.state.indexPatterns} diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js index d87761d3dabe8c..8fdb71de2dfed4 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js @@ -8,6 +8,7 @@ import React, { Component } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiPopover, EuiExpression, EuiFormHelpText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { UI_SETTINGS } from '../../../../../../../../src/plugins/data/public'; import { getUiSettings, getData } from '../../../../kibana_services'; export class WhereExpression extends Component { @@ -79,7 +80,7 @@ export class WhereExpression extends Component { query={ whereQuery ? whereQuery - : { language: getUiSettings().get('search:queryLanguage'), query: '' } + : { language: getUiSettings().get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE), query: '' } } onQuerySubmit={this._onQueryChange} indexPatterns={[indexPattern]} diff --git a/x-pack/plugins/ml/public/application/components/job_selector/custom_selection_table/custom_selection_table.js b/x-pack/plugins/ml/public/application/components/custom_selection_table/custom_selection_table.js similarity index 84% rename from x-pack/plugins/ml/public/application/components/job_selector/custom_selection_table/custom_selection_table.js rename to x-pack/plugins/ml/public/application/components/custom_selection_table/custom_selection_table.js index af282d53273d1e..c86b716b2f49b9 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/custom_selection_table/custom_selection_table.js +++ b/x-pack/plugins/ml/public/application/components/custom_selection_table/custom_selection_table.js @@ -29,7 +29,7 @@ import { import { Pager } from '@elastic/eui/lib/services'; import { i18n } from '@kbn/i18n'; -const JOBS_PER_PAGE = 20; +const ITEMS_PER_PAGE = 20; function getError(error) { if (error !== null) { @@ -43,15 +43,18 @@ function getError(error) { } export function CustomSelectionTable({ + checkboxDisabledCheck, columns, filterDefaultFields, filters, items, + itemsPerPage = ITEMS_PER_PAGE, onTableChange, + radioDisabledCheck, selectedIds, singleSelection, sortableProperties, - timeseriesOnly, + tableItemId = 'id', }) { const [itemIdToSelectedMap, setItemIdToSelectedMap] = useState(getCurrentlySelectedItemIdsMap()); const [currentItems, setCurrentItems] = useState(items); @@ -59,7 +62,7 @@ export function CustomSelectionTable({ const [sortedColumn, setSortedColumn] = useState(''); const [pager, setPager] = useState(); const [pagerSettings, setPagerSettings] = useState({ - itemsPerPage: JOBS_PER_PAGE, + itemsPerPage: itemsPerPage, firstItemIndex: 0, lastItemIndex: 1, }); @@ -77,9 +80,9 @@ export function CustomSelectionTable({ }, [selectedIds]); // eslint-disable-line useEffect(() => { - const tablePager = new Pager(currentItems.length, JOBS_PER_PAGE); + const tablePager = new Pager(currentItems.length, itemsPerPage); setPagerSettings({ - itemsPerPage: JOBS_PER_PAGE, + itemsPerPage: itemsPerPage, firstItemIndex: tablePager.getFirstItemIndex(), lastItemIndex: tablePager.getLastItemIndex(), }); @@ -100,7 +103,7 @@ export function CustomSelectionTable({ function handleTableChange({ isSelected, itemId }) { const selectedMapIds = Object.getOwnPropertyNames(itemIdToSelectedMap); - const currentItemIds = currentItems.map((item) => item.id); + const currentItemIds = currentItems.map((item) => item[tableItemId]); let currentSelected = selectedMapIds.filter( (id) => itemIdToSelectedMap[id] === true && id !== itemId @@ -124,11 +127,11 @@ export function CustomSelectionTable({ onTableChange(currentSelected); } - function handleChangeItemsPerPage(itemsPerPage) { - pager.setItemsPerPage(itemsPerPage); + function handleChangeItemsPerPage(numItemsPerPage) { + pager.setItemsPerPage(numItemsPerPage); setPagerSettings({ ...pagerSettings, - itemsPerPage, + itemsPerPage: numItemsPerPage, firstItemIndex: pager.getFirstItemIndex(), lastItemIndex: pager.getLastItemIndex(), }); @@ -161,7 +164,9 @@ export function CustomSelectionTable({ } function areAllItemsSelected() { - const indexOfUnselectedItem = currentItems.findIndex((item) => !isItemSelected(item.id)); + const indexOfUnselectedItem = currentItems.findIndex( + (item) => !isItemSelected(item[tableItemId]) + ); return indexOfUnselectedItem === -1; } @@ -199,7 +204,7 @@ export function CustomSelectionTable({ function toggleAll() { const allSelected = areAllItemsSelected() || itemIdToSelectedMap.all === true; const newItemIdToSelectedMap = {}; - currentItems.forEach((item) => (newItemIdToSelectedMap[item.id] = !allSelected)); + currentItems.forEach((item) => (newItemIdToSelectedMap[item[tableItemId]] = !allSelected)); setItemIdToSelectedMap(newItemIdToSelectedMap); handleTableChange({ isSelected: !allSelected, itemId: 'all' }); } @@ -255,20 +260,23 @@ export function CustomSelectionTable({ {!singleSelection && ( toggleItem(item.id)} + disabled={ + checkboxDisabledCheck !== undefined ? checkboxDisabledCheck(item) : undefined + } + id={`${item[tableItemId]}-checkbox`} + data-test-subj={`${item[tableItemId]}-checkbox`} + checked={isItemSelected(item[tableItemId])} + onChange={() => toggleItem(item[tableItemId])} type="inList" /> )} {singleSelection && ( toggleItem(item.id)} - disabled={timeseriesOnly && item.isSingleMetricViewerJob === false} + id={item[tableItemId]} + data-test-subj={`${item[tableItemId]}-radio-button`} + checked={isItemSelected(item[tableItemId])} + onChange={() => toggleItem(item[tableItemId])} + disabled={radioDisabledCheck !== undefined ? radioDisabledCheck(item) : undefined} /> )} @@ -299,11 +307,11 @@ export function CustomSelectionTable({ return ( {cells} @@ -331,7 +339,7 @@ export function CustomSelectionTable({ - + {renderSelectAll(true)} - + {renderHeaderCells()} {renderRows()} @@ -368,7 +376,7 @@ export function CustomSelectionTable({ handlePageChange(pageIndex)} @@ -379,13 +387,16 @@ export function CustomSelectionTable({ } CustomSelectionTable.propTypes = { + checkboxDisabledCheck: PropTypes.func, columns: PropTypes.array.isRequired, filterDefaultFields: PropTypes.array, filters: PropTypes.array, items: PropTypes.array.isRequired, + itemsPerPage: PropTypes.number, onTableChange: PropTypes.func.isRequired, + radioDisabledCheck: PropTypes.func, selectedId: PropTypes.array, singleSelection: PropTypes.bool, sortableProperties: PropTypes.object, - timeseriesOnly: PropTypes.bool, + tableItemId: PropTypes.string, }; diff --git a/x-pack/plugins/ml/public/application/components/job_selector/custom_selection_table/index.js b/x-pack/plugins/ml/public/application/components/custom_selection_table/index.js similarity index 100% rename from x-pack/plugins/ml/public/application/components/job_selector/custom_selection_table/index.js rename to x-pack/plugins/ml/public/application/components/custom_selection_table/index.js diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js index 4eeef560dd6d1f..7b104ea372ae56 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js @@ -6,7 +6,7 @@ import React, { Fragment, useState, useEffect } from 'react'; import { PropTypes } from 'prop-types'; -import { CustomSelectionTable } from '../custom_selection_table'; +import { CustomSelectionTable } from '../../custom_selection_table'; import { JobSelectorBadge } from '../job_selector_badge'; import { TimeRangeBar } from '../timerange_bar'; @@ -107,7 +107,7 @@ export function JobSelectorTable({ id: 'checkbox', isCheckbox: true, textOnly: false, - width: '24px', + width: '32px', }, { label: 'job ID', @@ -157,6 +157,9 @@ export function JobSelectorTable({ filterDefaultFields={!singleSelection ? JOB_FILTER_FIELDS : undefined} items={jobs} onTableChange={(selectionFromTable) => onSelection({ selectionFromTable })} + radioDisabledCheck={(item) => { + return timeseriesOnly && item.isSingleMetricViewerJob === false; + }} selectedIds={selectedIds} singleSelection={singleSelection} sortableProperties={sortableProperties} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/_index.scss b/x-pack/plugins/ml/public/application/data_frame_analytics/_index.scss index 89a0018f90401a..5508c021d33139 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/_index.scss +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/_index.scss @@ -2,3 +2,4 @@ @import 'pages/analytics_management/components/analytics_list/index'; @import 'pages/analytics_management/components/create_analytics_form/index'; @import 'pages/analytics_management/components/create_analytics_flyout/index'; +@import 'pages/analytics_management/components/create_analytics_button/index'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 7633e07e8f3dca..0b4e6d27b96e5c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -24,6 +24,21 @@ export enum ANALYSIS_CONFIG_TYPE { CLASSIFICATION = 'classification', } +export enum ANALYSIS_ADVANCED_FIELDS { + FEATURE_INFLUENCE_THRESHOLD = 'feature_influence_threshold', + GAMMA = 'gamma', + LAMBDA = 'lambda', + MAX_TREES = 'max_trees', + NUM_TOP_FEATURE_IMPORTANCE_VALUES = 'num_top_feature_importance_values', +} + +export enum OUTLIER_ANALYSIS_METHOD { + LOF = 'lof', + LDOF = 'ldof', + DISTANCE_KTH_NN = 'distance_kth_nn', + DISTANCE_KNN = 'distance_knn', +} + interface OutlierAnalysis { [key: string]: {}; outlier_detection: {}; @@ -263,11 +278,13 @@ export const isClassificationAnalysis = (arg: any): arg is ClassificationAnalysi }; export const isResultsSearchBoolQuery = (arg: any): arg is ResultsSearchBoolQuery => { + if (arg === undefined) return false; const keys = Object.keys(arg); return keys.length === 1 && keys[0] === 'bool'; }; export const isQueryStringQuery = (arg: any): arg is QueryStringQuery => { + if (arg === undefined) return false; const keys = Object.keys(arg); return keys.length === 1 && keys[0] === 'query_string'; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts index 400902c152c9e6..58343e26153ccf 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -17,6 +17,7 @@ export { IndexPattern, REFRESH_ANALYTICS_LIST_STATE, ANALYSIS_CONFIG_TYPE, + OUTLIER_ANALYSIS_METHOD, RegressionEvaluateResponse, getValuesFromResponse, loadEvalData, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step.tsx new file mode 100644 index 00000000000000..f957dcab2e87ea --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step.tsx @@ -0,0 +1,32 @@ +/* + * 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, { FC } from 'react'; +import { EuiForm } from '@elastic/eui'; + +import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { AdvancedStepForm } from './advanced_step_form'; +import { AdvancedStepDetails } from './advanced_step_details'; +import { ANALYTICS_STEPS } from '../../page'; + +export const AdvancedStep: FC = ({ + actions, + state, + step, + setCurrentStep, + stepActivated, +}) => { + return ( + + {step === ANALYTICS_STEPS.ADVANCED && ( + + )} + {step !== ANALYTICS_STEPS.ADVANCED && stepActivated === true && ( + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_details.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_details.tsx new file mode 100644 index 00000000000000..a9c8b6d4040ad1 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_details.tsx @@ -0,0 +1,274 @@ +/* + * 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, { FC, Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonEmpty, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { + UNSET_CONFIG_ITEM, + State, +} from '../../../analytics_management/hooks/use_create_analytics_form/state'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; +import { ANALYTICS_STEPS } from '../../page'; + +function getStringValue(value: number | undefined) { + return value !== undefined ? `${value}` : UNSET_CONFIG_ITEM; +} + +export interface ListItems { + title: string; + description: string | JSX.Element; +} + +export const AdvancedStepDetails: FC<{ setCurrentStep: any; state: State }> = ({ + setCurrentStep, + state, +}) => { + const { form, isJobCreated } = state; + const { + computeFeatureInfluence, + dependentVariable, + eta, + featureBagFraction, + featureInfluenceThreshold, + gamma, + jobType, + lambda, + method, + maxTrees, + modelMemoryLimit, + nNeighbors, + numTopClasses, + numTopFeatureImportanceValues, + outlierFraction, + predictionFieldName, + randomizeSeed, + standardizationEnabled, + } = form; + + const isRegOrClassJob = + jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; + + const advancedFirstCol: ListItems[] = []; + const advancedSecondCol: ListItems[] = []; + const advancedThirdCol: ListItems[] = []; + + const hyperFirstCol: ListItems[] = []; + const hyperSecondCol: ListItems[] = []; + const hyperThirdCol: ListItems[] = []; + + if (jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION) { + advancedFirstCol.push({ + title: i18n.translate( + 'xpack.ml.dataframe.analytics.create.configDetails.computeFeatureInfluence', + { + defaultMessage: 'Compute feature influence', + } + ), + description: computeFeatureInfluence, + }); + + advancedSecondCol.push({ + title: i18n.translate( + 'xpack.ml.dataframe.analytics.create.configDetails.featureInfluenceThreshold', + { + defaultMessage: 'Feature influence threshold', + } + ), + description: getStringValue(featureInfluenceThreshold), + }); + + advancedThirdCol.push({ + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.modelMemoryLimit', { + defaultMessage: 'Model memory limit', + }), + description: `${modelMemoryLimit}`, + }); + + hyperFirstCol.push( + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.nNeighbors', { + defaultMessage: 'N neighbors', + }), + description: getStringValue(nNeighbors), + }, + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.outlierFraction', { + defaultMessage: 'Outlier fraction', + }), + description: getStringValue(outlierFraction), + } + ); + + hyperSecondCol.push({ + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.method', { + defaultMessage: 'Method', + }), + description: method !== undefined ? method : UNSET_CONFIG_ITEM, + }); + + hyperThirdCol.push({ + title: i18n.translate( + 'xpack.ml.dataframe.analytics.create.configDetails.standardizationEnabled', + { + defaultMessage: 'Standardization enabled', + } + ), + description: `${standardizationEnabled}`, + }); + } + + if (isRegOrClassJob) { + if (jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION) { + advancedFirstCol.push({ + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.numTopClasses', { + defaultMessage: 'Top classes', + }), + description: `${numTopClasses}`, + }); + } + + advancedFirstCol.push({ + title: i18n.translate( + 'xpack.ml.dataframe.analytics.create.configDetails.numTopFeatureImportanceValues', + { + defaultMessage: 'Top feature importance values', + } + ), + description: `${numTopFeatureImportanceValues}`, + }); + + hyperFirstCol.push( + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.lambdaFields', { + defaultMessage: 'Lambda', + }), + description: getStringValue(lambda), + }, + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.eta', { + defaultMessage: 'Eta', + }), + description: getStringValue(eta), + } + ); + + advancedSecondCol.push({ + title: i18n.translate( + 'xpack.ml.dataframe.analytics.create.configDetails.predictionFieldName', + { + defaultMessage: 'Prediction field name', + } + ), + description: predictionFieldName ? predictionFieldName : `${dependentVariable}_prediction`, + }); + + hyperSecondCol.push( + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.maxTreesFields', { + defaultMessage: 'Max trees', + }), + description: getStringValue(maxTrees), + }, + { + title: i18n.translate( + 'xpack.ml.dataframe.analytics.create.configDetails.featureBagFraction', + { + defaultMessage: 'Feature bag fraction', + } + ), + description: getStringValue(featureBagFraction), + } + ); + + advancedThirdCol.push({ + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.modelMemoryLimit', { + defaultMessage: 'Model memory limit', + }), + description: `${modelMemoryLimit}`, + }); + + hyperThirdCol.push( + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.gamma', { + defaultMessage: 'Gamma', + }), + description: getStringValue(gamma), + }, + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.randomizedSeed', { + defaultMessage: 'Randomized seed', + }), + description: getStringValue(randomizeSeed), + } + ); + } + + return ( + + +

+ {i18n.translate('xpack.ml.dataframe.analytics.create.advancedConfigDetailsTitle', { + defaultMessage: 'Advanced configuration', + })} +

+
+ + + + + + + + + + + + + + +

+ {i18n.translate('xpack.ml.dataframe.analytics.create.hyperParametersDetailsTitle', { + defaultMessage: 'Hyper parameters', + })} +

+
+ + + + + + + + + + + + + + {!isJobCreated && ( + { + setCurrentStep(ANALYTICS_STEPS.ADVANCED); + }} + > + {i18n.translate('xpack.ml.dataframe.analytics.create.advancedDetails.editButtonText', { + defaultMessage: 'Edit', + })} + + )} +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx new file mode 100644 index 00000000000000..8b137ac72361c7 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx @@ -0,0 +1,332 @@ +/* + * 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, { FC, Fragment, useMemo } from 'react'; +import { + EuiAccordion, + EuiFieldNumber, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSelect, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { HyperParameters } from './hyper_parameters'; +import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { getModelMemoryLimitErrors } from '../../../analytics_management/hooks/use_create_analytics_form/reducer'; +import { + ANALYSIS_CONFIG_TYPE, + NUM_TOP_FEATURE_IMPORTANCE_VALUES_MIN, +} from '../../../../common/analytics'; +import { DEFAULT_MODEL_MEMORY_LIMIT } from '../../../analytics_management/hooks/use_create_analytics_form/state'; +import { ANALYTICS_STEPS } from '../../page'; +import { ContinueButton } from '../continue_button'; +import { OutlierHyperParameters } from './outlier_hyper_parameters'; + +export function getNumberValue(value?: number) { + return value === undefined ? '' : +value; +} + +export const AdvancedStepForm: FC = ({ + actions, + state, + setCurrentStep, +}) => { + const { setFormState } = actions; + const { form, isJobCreated } = state; + const { + computeFeatureInfluence, + featureInfluenceThreshold, + jobType, + modelMemoryLimit, + modelMemoryLimitValidationResult, + numTopClasses, + numTopFeatureImportanceValues, + numTopFeatureImportanceValuesValid, + predictionFieldName, + } = form; + + const mmlErrors = useMemo(() => getModelMemoryLimitErrors(modelMemoryLimitValidationResult), [ + modelMemoryLimitValidationResult, + ]); + + const isRegOrClassJob = + jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; + + const mmlInvalid = modelMemoryLimitValidationResult !== null; + + const outlierDetectionAdvancedConfig = ( + + + + { + setFormState({ + computeFeatureInfluence: e.target.value, + }); + }} + /> + + + + + + setFormState({ + featureInfluenceThreshold: e.target.value === '' ? undefined : +e.target.value, + }) + } + data-test-subj="mlAnalyticsCreateJobWizardFeatureInfluenceThresholdInput" + min={0} + max={1} + step={0.001} + value={getNumberValue(featureInfluenceThreshold)} + /> + + + + ); + + const regAndClassAdvancedConfig = ( + + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.create.numTopFeatureImportanceValuesErrorText', + { + defaultMessage: 'Invalid maximum number of feature importance values.', + } + )} + , + ] + : []), + ]} + > + + setFormState({ + numTopFeatureImportanceValues: e.target.value === '' ? undefined : +e.target.value, + }) + } + step={1} + value={getNumberValue(numTopFeatureImportanceValues)} + /> + + + + _prediction.', + } + )} + > + setFormState({ predictionFieldName: e.target.value })} + data-test-subj="mlAnalyticsCreateJobWizardPredictionFieldNameInput" + /> + + +
+ ); + + return ( + + +

+ {i18n.translate('xpack.ml.dataframe.analytics.create.advancedConfigSectionTitle', { + defaultMessage: 'Advanced configuration', + })} +

+
+ + {jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION && outlierDetectionAdvancedConfig} + {isRegOrClassJob && regAndClassAdvancedConfig} + {jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && ( + + + + setFormState({ + numTopClasses: e.target.value === '' ? undefined : +e.target.value, + }) + } + step={1} + value={getNumberValue(numTopClasses)} + /> + + + )} + + + setFormState({ modelMemoryLimit: e.target.value })} + isInvalid={mmlInvalid} + data-test-subj="mlAnalyticsCreateJobWizardModelMemoryInput" + /> + + + + + +

+ {i18n.translate('xpack.ml.dataframe.analytics.create.hyperParametersSectionTitle', { + defaultMessage: 'Hyper parameters', + })} +

+ + } + initialIsOpen={false} + data-test-subj="mlAnalyticsCreateJobWizardHyperParametersSection" + > + + {jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION && ( + + )} + {isRegOrClassJob && } + +
+ + { + setCurrentStep(ANALYTICS_STEPS.DETAILS); + }} + /> +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx new file mode 100644 index 00000000000000..144a0621060032 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx @@ -0,0 +1,188 @@ +/* + * 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, { FC, Fragment } from 'react'; +import { EuiFieldNumber, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { getNumberValue } from './advanced_step_form'; + +const MAX_TREES_LIMIT = 2000; + +export const HyperParameters: FC = ({ actions, state }) => { + const { setFormState } = actions; + + const { eta, featureBagFraction, gamma, lambda, maxTrees, randomizeSeed } = state.form; + + return ( + + + + + setFormState({ lambda: e.target.value === '' ? undefined : +e.target.value }) + } + step={0.001} + min={0} + value={getNumberValue(lambda)} + /> + + + + + + setFormState({ maxTrees: e.target.value === '' ? undefined : +e.target.value }) + } + isInvalid={maxTrees !== undefined && !Number.isInteger(maxTrees)} + step={1} + min={1} + max={MAX_TREES_LIMIT} + value={getNumberValue(maxTrees)} + /> + + + + + + setFormState({ gamma: e.target.value === '' ? undefined : +e.target.value }) + } + step={0.001} + min={0} + value={getNumberValue(gamma)} + /> + + + + + + setFormState({ eta: e.target.value === '' ? undefined : +e.target.value }) + } + step={0.001} + min={0.001} + max={1} + value={getNumberValue(eta)} + /> + + + + + + setFormState({ + featureBagFraction: e.target.value === '' ? undefined : +e.target.value, + }) + } + isInvalid={ + featureBagFraction !== undefined && + (featureBagFraction > 1 || featureBagFraction <= 0) + } + step={0.001} + max={1} + value={getNumberValue(featureBagFraction)} + /> + + + + + + setFormState({ randomizeSeed: e.target.value === '' ? undefined : +e.target.value }) + } + isInvalid={randomizeSeed !== undefined && typeof randomizeSeed !== 'number'} + value={getNumberValue(randomizeSeed)} + step={1} + /> + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/index.ts new file mode 100644 index 00000000000000..6a19e55b533c10 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/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 { AdvancedStep } from './advanced_step'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/outlier_hyper_parameters.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/outlier_hyper_parameters.tsx new file mode 100644 index 00000000000000..dfe7969d8a6d96 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/outlier_hyper_parameters.tsx @@ -0,0 +1,153 @@ +/* + * 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, { FC, Fragment } from 'react'; +import { EuiFieldNumber, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { OUTLIER_ANALYSIS_METHOD } from '../../../../common/analytics'; +import { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { getNumberValue } from './advanced_step_form'; + +export const OutlierHyperParameters: FC = ({ actions, state }) => { + const { setFormState } = actions; + + const { method, nNeighbors, outlierFraction, standardizationEnabled } = state.form; + + return ( + + + + ({ + value: outlierMethod, + text: outlierMethod, + }))} + value={method} + hasNoInitialSelection={true} + onChange={(e) => { + setFormState({ method: e.target.value }); + }} + data-test-subj="mlAnalyticsCreateJobWizardMethodInput" + /> + + + + + + setFormState({ nNeighbors: e.target.value === '' ? undefined : +e.target.value }) + } + step={1} + min={1} + value={getNumberValue(nNeighbors)} + /> + + + + + + setFormState({ outlierFraction: e.target.value === '' ? undefined : +e.target.value }) + } + step={0.001} + min={0} + max={1} + value={getNumberValue(outlierFraction)} + /> + + + + + { + setFormState({ standardizationEnabled: e.target.value }); + }} + /> + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx new file mode 100644 index 00000000000000..e437d27372a3eb --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment } from 'react'; +import { EuiCard, EuiHorizontalRule, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +function redirectToAnalyticsManagementPage() { + window.location.href = '#/data_frame_analytics?'; +} + +export const BackToListPanel: FC = () => ( + + + } + title={i18n.translate('xpack.ml.dataframe.analytics.create.analyticsListCardTitle', { + defaultMessage: 'Data Frame Analytics', + })} + description={i18n.translate( + 'xpack.ml.dataframe.analytics.create.analyticsListCardDescription', + { + defaultMessage: 'Return to the analytics management page.', + } + )} + onClick={redirectToAnalyticsManagementPage} + data-test-subj="analyticsWizardCardManagement" + /> + +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/index.ts new file mode 100644 index 00000000000000..4da65615628797 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/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 { BackToListPanel } from './back_to_list_panel'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx new file mode 100644 index 00000000000000..ad540285e49f09 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx @@ -0,0 +1,207 @@ +/* + * 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, { FC, Fragment, memo, useEffect, useState } from 'react'; +import { EuiCallOut, EuiFormRow, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; +// @ts-ignore no declaration +import { LEFT_ALIGNMENT, CENTER_ALIGNMENT, SortableProperties } from '@elastic/eui/lib/services'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { FieldSelectionItem } from '../../../../common/analytics'; +// @ts-ignore could not find declaration file +import { CustomSelectionTable } from '../../../../../components/custom_selection_table'; + +const columns = [ + { + id: 'checkbox', + isCheckbox: true, + textOnly: false, + width: '32px', + }, + { + label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.fieldNameColumn', { + defaultMessage: 'Field name', + }), + id: 'name', + isSortable: true, + alignment: LEFT_ALIGNMENT, + }, + { + id: 'mapping_types', + label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.mappingColumn', { + defaultMessage: 'Mapping', + }), + isSortable: false, + alignment: LEFT_ALIGNMENT, + }, + { + label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.isIncludedColumn', { + defaultMessage: 'Is included', + }), + id: 'is_included', + alignment: LEFT_ALIGNMENT, + isSortable: true, + // eslint-disable-next-line @typescript-eslint/camelcase + render: ({ is_included }: { is_included: boolean }) => (is_included ? 'Yes' : 'No'), + }, + { + label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.isRequiredColumn', { + defaultMessage: 'Is required', + }), + id: 'is_required', + alignment: LEFT_ALIGNMENT, + isSortable: true, + // eslint-disable-next-line @typescript-eslint/camelcase + render: ({ is_required }: { is_required: boolean }) => (is_required ? 'Yes' : 'No'), + }, + { + label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.reasonColumn', { + defaultMessage: 'Reason', + }), + id: 'reason', + alignment: LEFT_ALIGNMENT, + isSortable: false, + }, +]; + +const checkboxDisabledCheck = (item: FieldSelectionItem) => + (item.is_included === false && !item.reason?.includes('in excludes list')) || + item.is_required === true; + +export const MemoizedAnalysisFieldsTable: FC<{ + excludes: string[]; + loadingItems: boolean; + setFormState: any; + tableItems: FieldSelectionItem[]; +}> = memo( + ({ excludes, loadingItems, setFormState, tableItems }) => { + const [sortableProperties, setSortableProperties] = useState(); + const [currentSelection, setCurrentSelection] = useState([]); + + useEffect(() => { + if (excludes.length > 0) { + setCurrentSelection(excludes); + } + }, []); + + // Only set form state on unmount to prevent re-renders due to props changing if exludes was updated on each selection + useEffect(() => { + return () => { + setFormState({ excludes: currentSelection }); + }; + }, [currentSelection]); + + useEffect(() => { + let sortablePropertyItems = []; + const defaultSortProperty = 'name'; + + sortablePropertyItems = [ + { + name: 'name', + getValue: (item: any) => item.name.toLowerCase(), + isAscending: true, + }, + { + name: 'is_included', + getValue: (item: any) => item.is_included, + isAscending: true, + }, + { + name: 'is_required', + getValue: (item: any) => item.is_required, + isAscending: true, + }, + ]; + const sortableProps = new SortableProperties(sortablePropertyItems, defaultSortProperty); + + setSortableProperties(sortableProps); + }, []); + + const filters = [ + { + type: 'field_value_selection', + field: 'is_included', + name: i18n.translate('xpack.ml.dataframe.analytics.create.excludedFilterLabel', { + defaultMessage: 'Is included', + }), + multiSelect: false, + options: [ + { + value: true, + view: ( + + {i18n.translate('xpack.ml.dataframe.analytics.create.isIncludedOption', { + defaultMessage: 'Yes', + })} + + ), + }, + { + value: false, + view: ( + + {i18n.translate('xpack.ml.dataframe.analytics.create.isNotIncludedOption', { + defaultMessage: 'No', + })} + + ), + }, + ], + }, + ]; + + return ( + + + + + {tableItems.length === 0 && ( + + + + )} + {tableItems.length > 0 && ( + + { + setCurrentSelection(selection); + }} + selectedIds={currentSelection} + singleSelection={false} + sortableProperties={sortableProperties} + tableItemId={'name'} + /> + + )} + + + ); + }, + (prevProps, nextProps) => prevProps.tableItems.length === nextProps.tableItems.length +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step.tsx new file mode 100644 index 00000000000000..220910535aafe9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiForm } from '@elastic/eui'; + +import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { ConfigurationStepDetails } from './configuration_step_details'; +import { ConfigurationStepForm } from './configuration_step_form'; +import { ANALYTICS_STEPS } from '../../page'; + +export const ConfigurationStep: FC = ({ + actions, + state, + setCurrentStep, + step, + stepActivated, +}) => { + return ( + + {step === ANALYTICS_STEPS.CONFIGURATION && ( + + )} + {step !== ANALYTICS_STEPS.CONFIGURATION && stepActivated === true && ( + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx new file mode 100644 index 00000000000000..6603af9aa302ee --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx @@ -0,0 +1,115 @@ +/* + * 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, { FC, Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonEmpty, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; +import { + State, + UNSET_CONFIG_ITEM, +} from '../../../analytics_management/hooks/use_create_analytics_form/state'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; +import { useMlContext } from '../../../../../contexts/ml'; +import { ANALYTICS_STEPS } from '../../page'; + +interface Props { + setCurrentStep: React.Dispatch>; + state: State; +} + +export const ConfigurationStepDetails: FC = ({ setCurrentStep, state }) => { + const mlContext = useMlContext(); + const { currentIndexPattern } = mlContext; + const { form, isJobCreated } = state; + const { dependentVariable, excludes, jobConfigQueryString, jobType, trainingPercent } = form; + + const isJobTypeWithDepVar = + jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; + + const detailsFirstCol = [ + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.sourceIndex', { + defaultMessage: 'Source index', + }), + description: currentIndexPattern.title || UNSET_CONFIG_ITEM, + }, + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.Query', { + defaultMessage: 'Query', + }), + description: jobConfigQueryString || UNSET_CONFIG_ITEM, + }, + ]; + + const detailsSecondCol = [ + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.jobType', { + defaultMessage: 'Job type', + }), + description: jobType! as string, + }, + ]; + + const detailsThirdCol = [ + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.excludedFields', { + defaultMessage: 'Excluded fields', + }), + description: excludes.length > 0 ? excludes.join(', ') : UNSET_CONFIG_ITEM, + }, + ]; + + if (isJobTypeWithDepVar) { + detailsSecondCol.push({ + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.trainingPercent', { + defaultMessage: 'Training percent', + }), + description: `${trainingPercent}`, + }); + detailsThirdCol.unshift({ + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.dependentVariable', { + defaultMessage: 'Dependent variable', + }), + description: dependentVariable, + }); + } + + return ( + + + + + + + + + + + + + + {!isJobCreated && ( + { + setCurrentStep(ANALYTICS_STEPS.CONFIGURATION); + }} + > + {i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.editButtonText', { + defaultMessage: 'Edit', + })} + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx new file mode 100644 index 00000000000000..9446dfd4ed5250 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -0,0 +1,449 @@ +/* + * 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, { FC, Fragment, useEffect, useRef } from 'react'; +import { EuiBadge, EuiComboBox, EuiFormRow, EuiRange, EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { debounce } from 'lodash'; + +import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; +import { useMlContext } from '../../../../../contexts/ml'; + +import { + DfAnalyticsExplainResponse, + FieldSelectionItem, + ANALYSIS_CONFIG_TYPE, + TRAINING_PERCENT_MIN, + TRAINING_PERCENT_MAX, +} from '../../../../common/analytics'; +import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { Messages } from '../../../analytics_management/components/create_analytics_form/messages'; +import { + DEFAULT_MODEL_MEMORY_LIMIT, + getJobConfigFromFormState, + State, +} from '../../../analytics_management/hooks/use_create_analytics_form/state'; +import { shouldAddAsDepVarOption } from '../../../analytics_management/components/create_analytics_form/form_options_validation'; +import { ml } from '../../../../../services/ml_api_service'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; + +import { ANALYTICS_STEPS } from '../../page'; +import { ContinueButton } from '../continue_button'; +import { JobType } from './job_type'; +import { SupportedFieldsMessage } from './supported_fields_message'; +import { MemoizedAnalysisFieldsTable } from './analysis_fields_table'; +import { DataGrid } from '../../../../../components/data_grid'; +import { useIndexData } from '../../hooks'; +import { ExplorationQueryBar } from '../../../analytics_exploration/components/exploration_query_bar'; +import { useSavedSearch } from './use_saved_search'; + +const requiredFieldsErrorText = i18n.translate( + 'xpack.ml.dataframe.analytics.createWizard.requiredFieldsErrorMessage', + { + defaultMessage: 'At least one field must be included in the analysis.', + } +); + +export const ConfigurationStepForm: FC = ({ + actions, + state, + setCurrentStep, +}) => { + const mlContext = useMlContext(); + const { currentSavedSearch, currentIndexPattern } = mlContext; + const { savedSearchQuery, savedSearchQueryStr } = useSavedSearch(); + + const { initiateWizard, setEstimatedModelMemoryLimit, setFormState } = actions; + const { estimatedModelMemoryLimit, form, isJobCreated, requestMessages } = state; + const firstUpdate = useRef(true); + const { + dependentVariable, + dependentVariableFetchFail, + dependentVariableOptions, + excludes, + excludesTableItems, + fieldOptionsFetchFail, + jobConfigQuery, + jobConfigQueryString, + jobType, + loadingDepVarOptions, + loadingFieldOptions, + maxDistinctValuesError, + modelMemoryLimit, + previousJobType, + requiredFieldsError, + trainingPercent, + } = form; + + const setJobConfigQuery = ({ query, queryString }: { query: any; queryString: string }) => { + setFormState({ jobConfigQuery: query, jobConfigQueryString: queryString }); + }; + + const indexData = useIndexData( + currentIndexPattern, + savedSearchQuery !== undefined ? savedSearchQuery : jobConfigQuery + ); + + const indexPreviewProps = { + ...indexData, + dataTestSubj: 'mlAnalyticsCreationDataGrid', + toastNotifications: getToastNotifications(), + }; + + const isJobTypeWithDepVar = + jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; + + const dependentVariableEmpty = isJobTypeWithDepVar && dependentVariable === ''; + + const isStepInvalid = + dependentVariableEmpty || + jobType === undefined || + maxDistinctValuesError !== undefined || + requiredFieldsError !== undefined; + + const loadDepVarOptions = async (formState: State['form']) => { + setFormState({ + loadingDepVarOptions: true, + maxDistinctValuesError: undefined, + }); + try { + if (currentIndexPattern !== undefined) { + const formStateUpdate: { + loadingDepVarOptions: boolean; + dependentVariableFetchFail: boolean; + dependentVariableOptions: State['form']['dependentVariableOptions']; + dependentVariable?: State['form']['dependentVariable']; + } = { + loadingDepVarOptions: false, + dependentVariableFetchFail: false, + dependentVariableOptions: [] as State['form']['dependentVariableOptions'], + }; + + // Get fields and filter for supported types for job type + const { fields } = newJobCapsService; + + let resetDependentVariable = true; + for (const field of fields) { + if (shouldAddAsDepVarOption(field, jobType)) { + formStateUpdate.dependentVariableOptions.push({ + label: field.id, + }); + + if (formState.dependentVariable === field.id) { + resetDependentVariable = false; + } + } + } + + if (resetDependentVariable) { + formStateUpdate.dependentVariable = ''; + } + + setFormState(formStateUpdate); + } + } catch (e) { + setFormState({ loadingDepVarOptions: false, dependentVariableFetchFail: true }); + } + }; + + const debouncedGetExplainData = debounce(async () => { + const jobTypeChanged = previousJobType !== jobType; + const shouldUpdateModelMemoryLimit = !firstUpdate.current || !modelMemoryLimit; + const shouldUpdateEstimatedMml = + !firstUpdate.current || !modelMemoryLimit || estimatedModelMemoryLimit === ''; + + if (firstUpdate.current) { + firstUpdate.current = false; + } + // Reset if jobType changes (jobType requires dependent_variable to be set - + // which won't be the case if switching from outlier detection) + if (jobTypeChanged) { + setFormState({ + loadingFieldOptions: true, + }); + } + + try { + const jobConfig = getJobConfigFromFormState(form); + delete jobConfig.dest; + delete jobConfig.model_memory_limit; + const resp: DfAnalyticsExplainResponse = await ml.dataFrameAnalytics.explainDataFrameAnalytics( + jobConfig + ); + const expectedMemoryWithoutDisk = resp.memory_estimation?.expected_memory_without_disk; + + if (shouldUpdateEstimatedMml) { + setEstimatedModelMemoryLimit(expectedMemoryWithoutDisk); + } + + const fieldSelection: FieldSelectionItem[] | undefined = resp.field_selection; + + let hasRequiredFields = false; + if (fieldSelection) { + for (let i = 0; i < fieldSelection.length; i++) { + const field = fieldSelection[i]; + if (field.is_included === true && field.is_required === false) { + hasRequiredFields = true; + break; + } + } + } + + // If job type has changed load analysis field options again + if (jobTypeChanged) { + setFormState({ + ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemoryWithoutDisk } : {}), + excludesTableItems: fieldSelection ? fieldSelection : [], + loadingFieldOptions: false, + fieldOptionsFetchFail: false, + maxDistinctValuesError: undefined, + requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined, + }); + } else { + setFormState({ + ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemoryWithoutDisk } : {}), + requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined, + }); + } + } catch (e) { + let errorMessage; + if ( + jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && + e.body && + e.body.message !== undefined && + e.body.message.includes('status_exception') && + (e.body.message.includes('must have at most') || + e.body.message.includes('must have at least')) + ) { + errorMessage = e.body.message; + } + const fallbackModelMemoryLimit = + jobType !== undefined + ? DEFAULT_MODEL_MEMORY_LIMIT[jobType] + : DEFAULT_MODEL_MEMORY_LIMIT.outlier_detection; + setEstimatedModelMemoryLimit(fallbackModelMemoryLimit); + setFormState({ + fieldOptionsFetchFail: true, + maxDistinctValuesError: errorMessage, + loadingFieldOptions: false, + ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: fallbackModelMemoryLimit } : {}), + }); + } + }, 300); + + useEffect(() => { + initiateWizard(); + }, []); + + useEffect(() => { + setFormState({ sourceIndex: currentIndexPattern.title }); + }, []); + + useEffect(() => { + if (savedSearchQueryStr !== undefined) { + setFormState({ jobConfigQuery: savedSearchQuery, jobConfigQueryString: savedSearchQueryStr }); + } + }, [JSON.stringify(savedSearchQuery), savedSearchQueryStr]); + + useEffect(() => { + if (isJobTypeWithDepVar) { + loadDepVarOptions(form); + } + }, [jobType]); + + useEffect(() => { + const hasBasicRequiredFields = jobType !== undefined; + + const hasRequiredAnalysisFields = + (isJobTypeWithDepVar && dependentVariable !== '') || + jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION; + + if (hasBasicRequiredFields && hasRequiredAnalysisFields) { + debouncedGetExplainData(); + } + + return () => { + debouncedGetExplainData.cancel(); + }; + }, [jobType, dependentVariable, trainingPercent, JSON.stringify(excludes), jobConfigQueryString]); + + return ( + + + + + {savedSearchQuery === undefined && ( + + + + )} + + {savedSearchQuery !== undefined && ( + + {i18n.translate('xpack.ml.dataframe.analytics.create.savedSearchLabel', { + defaultMessage: 'Saved search', + })} + + )} + + {savedSearchQuery !== undefined + ? currentSavedSearch?.attributes.title + : currentIndexPattern.title} + + + } + fullWidth + > + + + {isJobTypeWithDepVar && ( + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.create.dependentVariableOptionsFetchError', + { + defaultMessage: + 'There was a problem fetching fields. Please refresh the page and try again.', + } + )} + , + ] + : []), + ...(fieldOptionsFetchFail === true && maxDistinctValuesError !== undefined + ? [ + + {i18n.translate( + 'xpack.ml.dataframe.analytics.create.dependentVariableMaxDistictValuesError', + { + defaultMessage: 'Invalid. {message}', + values: { message: maxDistinctValuesError }, + } + )} + , + ] + : []), + ]} + > + + setFormState({ + dependentVariable: selectedOptions[0].label || '', + }) + } + isClearable={false} + isInvalid={dependentVariable === ''} + data-test-subj="mlAnalyticsCreateJobWizardDependentVariableSelect" + /> + + + )} + + + + + {isJobTypeWithDepVar && ( + + setFormState({ trainingPercent: +e.target.value })} + data-test-subj="mlAnalyticsCreateJobWizardTrainingPercentSlider" + /> + + )} + + { + setCurrentStep(ANALYTICS_STEPS.ADVANCED); + }} + /> + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/index.ts new file mode 100644 index 00000000000000..ba67a6f080d45c --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/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 { ConfigurationStep } from './configuration_step'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx new file mode 100644 index 00000000000000..f31c9cd28f65a4 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiFormRow, EuiSelect } from '@elastic/eui'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../common'; + +import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state'; + +interface Props { + type: AnalyticsJobType; + setFormState: React.Dispatch>; +} + +export const JobType: FC = ({ type, setFormState }) => { + const outlierHelpText = i18n.translate( + 'xpack.ml.dataframe.analytics.create.outlierDetectionHelpText', + { + defaultMessage: + 'Outlier detection jobs require a source index that is mapped as a table-like data structure and analyze only numeric and boolean fields. Use the advanced editor to add custom options to the configuration.', + } + ); + + const regressionHelpText = i18n.translate( + 'xpack.ml.dataframe.analytics.create.outlierRegressionHelpText', + { + defaultMessage: + 'Regression jobs analyze only numeric fields. Use the advanced editor to apply custom options, such as the prediction field name.', + } + ); + + const classificationHelpText = i18n.translate( + 'xpack.ml.dataframe.analytics.create.classificationHelpText', + { + defaultMessage: + 'Classification jobs require a source index that is mapped as a table-like data structure and support fields that are numeric, boolean, text, keyword, or ip. Use the advanced editor to apply custom options, such as the prediction field name.', + } + ); + + const helpText = { + [ANALYSIS_CONFIG_TYPE.REGRESSION]: regressionHelpText, + [ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION]: outlierHelpText, + [ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: classificationHelpText, + }; + + return ( + + + ({ + value: jobType, + text: jobType.replace(/_/g, ' '), + 'data-test-subj': `mlAnalyticsCreation-${jobType}-option`, + }))} + value={type} + hasNoInitialSelection={true} + onChange={(e) => { + const value = e.target.value as AnalyticsJobType; + setFormState({ + previousJobType: type, + jobType: value, + excludes: [], + requiredFieldsError: undefined, + }); + }} + data-test-subj="mlAnalyticsCreateJobWizardJobTypeSelect" + /> + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx new file mode 100644 index 00000000000000..fe13cc1d6edfc5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx @@ -0,0 +1,120 @@ +/* + * 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, { FC, Fragment, useState, useEffect } from 'react'; +import { EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; +import { Field, EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields'; +import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES } from '../../../../common/fields'; +import { + OMIT_FIELDS, + CATEGORICAL_TYPES, +} from '../../../analytics_management/components/create_analytics_form/form_options_validation'; +import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; +import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; + +const containsClassificationFieldsCb = ({ name, type }: Field) => + !OMIT_FIELDS.includes(name) && + name !== EVENT_RATE_FIELD_ID && + (BASIC_NUMERICAL_TYPES.has(type) || + CATEGORICAL_TYPES.has(type) || + type === ES_FIELD_TYPES.BOOLEAN); + +const containsRegressionFieldsCb = ({ name, type }: Field) => + !OMIT_FIELDS.includes(name) && + name !== EVENT_RATE_FIELD_ID && + (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); + +const containsOutlierFieldsCb = ({ name, type }: Field) => + !OMIT_FIELDS.includes(name) && name !== EVENT_RATE_FIELD_ID && BASIC_NUMERICAL_TYPES.has(type); + +const callbacks: Record boolean> = { + [ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: containsClassificationFieldsCb, + [ANALYSIS_CONFIG_TYPE.REGRESSION]: containsRegressionFieldsCb, + [ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION]: containsOutlierFieldsCb, +}; + +const messages: Record = { + [ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: ( + + ), + [ANALYSIS_CONFIG_TYPE.REGRESSION]: ( + + ), + [ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION]: ( + + ), +}; + +interface Props { + jobType: AnalyticsJobType; +} + +export const SupportedFieldsMessage: FC = ({ jobType }) => { + const [sourceIndexContainsSupportedFields, setSourceIndexContainsSupportedFields] = useState< + boolean + >(true); + const [sourceIndexFieldsCheckFailed, setSourceIndexFieldsCheckFailed] = useState(false); + const { fields } = newJobCapsService; + + // Find out if index pattern contains supported fields for job type. Provides a hint in the form + // that job may not run correctly if no supported fields are found. + const validateFields = () => { + if (fields && jobType !== undefined) { + try { + const containsSupportedFields: boolean = fields.some(callbacks[jobType]); + + setSourceIndexContainsSupportedFields(containsSupportedFields); + setSourceIndexFieldsCheckFailed(false); + } catch (e) { + setSourceIndexFieldsCheckFailed(true); + } + } + }; + + useEffect(() => { + if (jobType !== undefined) { + setSourceIndexContainsSupportedFields(true); + setSourceIndexFieldsCheckFailed(false); + validateFields(); + } + }, [jobType]); + + if (sourceIndexContainsSupportedFields === true) return null; + + if (sourceIndexFieldsCheckFailed === true) { + return ( + + + + + ); + } + + return ( + + + {jobType !== undefined && messages[jobType]} + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/use_saved_search.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/use_saved_search.ts new file mode 100644 index 00000000000000..856358538b26fa --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/use_saved_search.ts @@ -0,0 +1,50 @@ +/* + * 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 { useState, useEffect } from 'react'; +import { useMlContext } from '../../../../../contexts/ml'; +import { esQuery, esKuery } from '../../../../../../../../../../src/plugins/data/public'; +import { SEARCH_QUERY_LANGUAGE } from '../../../../../../../common/constants/search'; +import { getQueryFromSavedSearch } from '../../../../../util/index_utils'; + +export function useSavedSearch() { + const [savedSearchQuery, setSavedSearchQuery] = useState(undefined); + const [savedSearchQueryStr, setSavedSearchQueryStr] = useState(undefined); + + const mlContext = useMlContext(); + const { currentSavedSearch, currentIndexPattern, kibanaConfig } = mlContext; + + const getQueryData = () => { + let qry; + let qryString; + + if (currentSavedSearch !== null) { + const { query } = getQueryFromSavedSearch(currentSavedSearch); + const queryLanguage = query.language as SEARCH_QUERY_LANGUAGE; + qryString = query.query; + + if (queryLanguage === SEARCH_QUERY_LANGUAGE.KUERY) { + const ast = esKuery.fromKueryExpression(qryString); + qry = esKuery.toElasticsearchQuery(ast, currentIndexPattern); + } else { + qry = esQuery.luceneStringToDsl(qryString); + esQuery.decorateQuery(qry, kibanaConfig.get('query:queryString:options')); + } + + setSavedSearchQuery(qry); + setSavedSearchQueryStr(qryString); + } + }; + + useEffect(() => { + getQueryData(); + }, []); + + return { + savedSearchQuery, + savedSearchQueryStr, + }; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/continue_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/continue_button.tsx new file mode 100644 index 00000000000000..6e95a0a246573b --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/continue_button.tsx @@ -0,0 +1,34 @@ +/* + * 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, { FC } from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +const continueButtonText = i18n.translate( + 'xpack.ml.dataframe.analytics.creation.continueButtonText', + { + defaultMessage: 'Continue', + } +); + +export const ContinueButton: FC<{ isDisabled: boolean; onClick: any }> = ({ + isDisabled, + onClick, +}) => ( + + + + {continueButtonText} + + + +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx new file mode 100644 index 00000000000000..2dda5f5d819b78 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx @@ -0,0 +1,95 @@ +/* + * 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, { FC, Fragment, useState } from 'react'; +import { + EuiButton, + EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { Messages } from '../../../analytics_management/components/create_analytics_form/messages'; +import { ANALYTICS_STEPS } from '../../page'; +import { BackToListPanel } from '../back_to_list_panel'; + +interface Props extends CreateAnalyticsFormProps { + step: ANALYTICS_STEPS; +} + +export const CreateStep: FC = ({ actions, state, step }) => { + const { createAnalyticsJob, startAnalyticsJob } = actions; + const { + isAdvancedEditorValidJson, + isJobCreated, + isJobStarted, + isModalButtonDisabled, + isValid, + requestMessages, + } = state; + + const [checked, setChecked] = useState(true); + + if (step !== ANALYTICS_STEPS.CREATE) return null; + + const handleCreation = async () => { + await createAnalyticsJob(); + + if (checked) { + startAnalyticsJob(); + } + }; + + return ( + + {!isJobCreated && !isJobStarted && ( + + + + setChecked(e.target.checked)} + /> + + + + + {i18n.translate('xpack.ml.dataframe.analytics.create.wizardCreateButton', { + defaultMessage: 'Create', + })} + + + + )} + + + {isJobCreated === true && } + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/index.ts new file mode 100644 index 00000000000000..01c8e4bff934dc --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/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 { CreateStep } from './create_step'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step.tsx new file mode 100644 index 00000000000000..a40813ed2fc3ec --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step.tsx @@ -0,0 +1,32 @@ +/* + * 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, { FC } from 'react'; +import { EuiForm } from '@elastic/eui'; + +import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { DetailsStepDetails } from './details_step_details'; +import { DetailsStepForm } from './details_step_form'; +import { ANALYTICS_STEPS } from '../../page'; + +export const DetailsStep: FC = ({ + actions, + state, + setCurrentStep, + step, + stepActivated, +}) => { + return ( + + {step === ANALYTICS_STEPS.DETAILS && ( + + )} + {step !== ANALYTICS_STEPS.DETAILS && stepActivated === true && ( + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx new file mode 100644 index 00000000000000..a4d86b48006e8a --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx @@ -0,0 +1,87 @@ +/* + * 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, { FC, Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonEmpty, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; +import { State } from '../../../analytics_management/hooks/use_create_analytics_form/state'; +import { ANALYTICS_STEPS } from '../../page'; + +export interface ListItems { + title: string; + description: string | JSX.Element; +} + +export const DetailsStepDetails: FC<{ setCurrentStep: any; state: State }> = ({ + setCurrentStep, + state, +}) => { + const { form, isJobCreated } = state; + const { description, jobId, destinationIndex } = form; + + const detailsFirstCol: ListItems[] = [ + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.jobId', { + defaultMessage: 'Job ID', + }), + description: jobId, + }, + ]; + + const detailsSecondCol: ListItems[] = [ + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.jobDescription', { + defaultMessage: 'Job description', + }), + description, + }, + ]; + + const detailsThirdCol: ListItems[] = [ + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.destIndex', { + defaultMessage: 'Destination index', + }), + description: destinationIndex || '', + }, + ]; + + return ( + + + + + + + + + + + + + + {!isJobCreated && ( + { + setCurrentStep(ANALYTICS_STEPS.DETAILS); + }} + > + {i18n.translate('xpack.ml.dataframe.analytics.create.detailsDetails.editButtonText', { + defaultMessage: 'Edit', + })} + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx new file mode 100644 index 00000000000000..67f8472e7ad146 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx @@ -0,0 +1,221 @@ +/* + * 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, { FC, Fragment, useRef } from 'react'; +import { EuiFieldText, EuiFormRow, EuiLink, EuiSpacer, EuiSwitch, EuiTextArea } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { useMlKibana } from '../../../../../contexts/kibana'; +import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { JOB_ID_MAX_LENGTH } from '../../../../../../../common/constants/validation'; +import { ContinueButton } from '../continue_button'; +import { ANALYTICS_STEPS } from '../../page'; + +export const DetailsStepForm: FC = ({ + actions, + state, + setCurrentStep, +}) => { + const { + services: { docLinks }, + } = useMlKibana(); + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; + + const { setFormState } = actions; + const { form, isJobCreated } = state; + const { + createIndexPattern, + description, + destinationIndex, + destinationIndexNameEmpty, + destinationIndexNameExists, + destinationIndexNameValid, + destinationIndexPatternTitleExists, + jobId, + jobIdEmpty, + jobIdExists, + jobIdInvalidMaxLength, + jobIdValid, + } = form; + const forceInput = useRef(null); + + const isStepInvalid = + jobIdEmpty === true || + jobIdExists === true || + jobIdValid === false || + destinationIndexNameEmpty === true || + destinationIndexNameValid === false || + (destinationIndexPatternTitleExists === true && createIndexPattern === true); + + return ( + + + { + if (input) { + forceInput.current = input; + } + }} + disabled={isJobCreated} + placeholder={i18n.translate('xpack.ml.dataframe.analytics.create.jobIdPlaceholder', { + defaultMessage: 'Job ID', + })} + value={jobId} + onChange={(e) => setFormState({ jobId: e.target.value })} + aria-label={i18n.translate('xpack.ml.dataframe.analytics.create.jobIdInputAriaLabel', { + defaultMessage: 'Choose a unique analytics job ID.', + })} + isInvalid={(!jobIdEmpty && !jobIdValid) || jobIdExists || jobIdEmpty} + data-test-subj="mlAnalyticsCreateJobFlyoutJobIdInput" + /> + + + { + const value = e.target.value; + setFormState({ description: value }); + }} + data-test-subj="mlDFAnalyticsJobCreationJobDescription" + /> + + + {i18n.translate('xpack.ml.dataframe.analytics.create.destinationIndexInvalidError', { + defaultMessage: 'Invalid destination index name.', + })} +
+ + {i18n.translate( + 'xpack.ml.dataframe.stepDetailsForm.destinationIndexInvalidErrorLink', + { + defaultMessage: 'Learn more about index name limitations.', + } + )} + +
, + ] + } + > + setFormState({ destinationIndex: e.target.value })} + aria-label={i18n.translate( + 'xpack.ml.dataframe.analytics.create.destinationIndexInputAriaLabel', + { + defaultMessage: 'Choose a unique destination index name.', + } + )} + isInvalid={!destinationIndexNameEmpty && !destinationIndexNameValid} + data-test-subj="mlAnalyticsCreateJobFlyoutDestinationIndexInput" + /> + + + setFormState({ createIndexPattern: !createIndexPattern })} + data-test-subj="mlAnalyticsCreateJobWizardCreateIndexPatternSwitch" + /> + + + { + setCurrentStep(ANALYTICS_STEPS.CREATE); + }} + /> + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/index.ts new file mode 100644 index 00000000000000..6cadd87d97e272 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/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 { DetailsStep } from './details_step'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/index.ts new file mode 100644 index 00000000000000..efd7c0634bed44 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ConfigurationStep } from './configuration_step/index'; +export { AdvancedStep } from './advanced_step/index'; +export { DetailsStep } from './details_step/index'; +export { CreateStep } from './create_step/index'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/index.ts new file mode 100644 index 00000000000000..5199fa1b6e4c6d --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/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 { useIndexData } from './use_index_data'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts new file mode 100644 index 00000000000000..e8f25584201e37 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts @@ -0,0 +1,103 @@ +/* + * 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 { useEffect } from 'react'; + +import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; +import { + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + useDataGrid, + useRenderCellValue, + EsSorting, + SearchResponse7, + UseIndexDataReturnType, +} from '../../../../components/data_grid'; +import { getErrorMessage } from '../../../../../../common/util/errors'; +import { INDEX_STATUS } from '../../../common/analytics'; +import { ml } from '../../../../services/ml_api_service'; + +type IndexSearchResponse = SearchResponse7; + +export const useIndexData = (indexPattern: IndexPattern, query: any): UseIndexDataReturnType => { + const indexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern); + + // EuiDataGrid State + const columns = [ + ...indexPatternFields.map((id) => { + const field = indexPattern.fields.getByName(id); + const schema = getDataGridSchemaFromKibanaFieldType(field); + return { id, schema }; + }), + ]; + + const dataGrid = useDataGrid(columns); + + const { + pagination, + resetPagination, + setErrorMessage, + setRowCount, + setStatus, + setTableItems, + sortingColumns, + tableItems, + } = dataGrid; + + useEffect(() => { + resetPagination(); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(query)]); + + const getIndexData = async function () { + setErrorMessage(''); + setStatus(INDEX_STATUS.LOADING); + + const sort: EsSorting = sortingColumns.reduce((s, column) => { + s[column.id] = { order: column.direction }; + return s; + }, {} as EsSorting); + + const esSearchRequest = { + index: indexPattern.title, + body: { + // Instead of using the default query (`*`), fall back to a more efficient `match_all` query. + query, // isDefaultQuery(query) ? matchAllQuery : query, + from: pagination.pageIndex * pagination.pageSize, + size: pagination.pageSize, + ...(Object.keys(sort).length > 0 ? { sort } : {}), + }, + }; + + try { + const resp: IndexSearchResponse = await ml.esSearch(esSearchRequest); + + const docs = resp.hits.hits.map((d) => d._source); + + setRowCount(resp.hits.total.value); + setTableItems(docs); + setStatus(INDEX_STATUS.LOADED); + } catch (e) { + setErrorMessage(getErrorMessage(e)); + setStatus(INDEX_STATUS.ERROR); + } + }; + + useEffect(() => { + getIndexData(); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); + + const renderCellValue = useRenderCellValue(indexPattern, pagination, tableItems); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/index.ts new file mode 100644 index 00000000000000..7e2d651439ae30 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/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 { Page } from './page'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx new file mode 100644 index 00000000000000..def862b859162c --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx @@ -0,0 +1,191 @@ +/* + * 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, { FC, useEffect, useState } from 'react'; +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiSpacer, + EuiSteps, + EuiStepStatus, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useMlContext } from '../../../contexts/ml'; +import { newJobCapsService } from '../../../services/new_job_capabilities_service'; +import { useCreateAnalyticsForm } from '../analytics_management/hooks/use_create_analytics_form'; +import { CreateAnalyticsAdvancedEditor } from '../analytics_management/components/create_analytics_advanced_editor'; +import { AdvancedStep, ConfigurationStep, CreateStep, DetailsStep } from './components'; + +export enum ANALYTICS_STEPS { + CONFIGURATION, + ADVANCED, + DETAILS, + CREATE, +} + +export const Page: FC = () => { + const [currentStep, setCurrentStep] = useState(ANALYTICS_STEPS.CONFIGURATION); + const [activatedSteps, setActivatedSteps] = useState([true, false, false, false]); + + const mlContext = useMlContext(); + const { currentIndexPattern } = mlContext; + + const createAnalyticsForm = useCreateAnalyticsForm(); + const { isAdvancedEditorEnabled } = createAnalyticsForm.state; + const { jobType } = createAnalyticsForm.state.form; + const { switchToAdvancedEditor } = createAnalyticsForm.actions; + + useEffect(() => { + if (activatedSteps[currentStep] === false) { + activatedSteps.splice(currentStep, 1, true); + setActivatedSteps(activatedSteps); + } + }, [currentStep]); + + useEffect(() => { + if (currentIndexPattern) { + (async function () { + await newJobCapsService.initializeFromIndexPattern(currentIndexPattern, false, false); + })(); + } + }, []); + + const analyticsWizardSteps = [ + { + title: i18n.translate('xpack.ml.dataframe.analytics.creation.configurationStepTitle', { + defaultMessage: 'Configuration', + }), + children: ( + + ), + status: + currentStep >= ANALYTICS_STEPS.CONFIGURATION ? undefined : ('incomplete' as EuiStepStatus), + }, + { + title: i18n.translate('xpack.ml.dataframe.analytics.creation.advancedStepTitle', { + defaultMessage: 'Additional options', + }), + children: ( + + ), + status: currentStep >= ANALYTICS_STEPS.ADVANCED ? undefined : ('incomplete' as EuiStepStatus), + 'data-test-subj': 'mlAnalyticsCreateJobWizardAdvancedStep', + }, + { + title: i18n.translate('xpack.ml.dataframe.analytics.creation.detailsStepTitle', { + defaultMessage: 'Job details', + }), + children: ( + + ), + status: currentStep >= ANALYTICS_STEPS.DETAILS ? undefined : ('incomplete' as EuiStepStatus), + 'data-test-subj': 'mlAnalyticsCreateJobWizardDetailsStep', + }, + { + title: i18n.translate('xpack.ml.dataframe.analytics.creation.createStepTitle', { + defaultMessage: 'Create', + }), + children: , + status: currentStep >= ANALYTICS_STEPS.CREATE ? undefined : ('incomplete' as EuiStepStatus), + 'data-test-subj': 'mlAnalyticsCreateJobWizardCreateStep', + }, + ]; + + return ( + + + + + + + + +

+ +

+
+
+ +

+ +

+
+
+
+ {isAdvancedEditorEnabled === false && ( + + + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.create.switchToJsonEditorSwitch', + { + defaultMessage: 'Switch to json editor', + } + )} + + + + + )} +
+ + {isAdvancedEditorEnabled === true && ( + + )} + {isAdvancedEditorEnabled === false && ( + + )} +
+
+
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx index f95e6a93058ba2..8c158c1ca14a0c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Dispatch, FC, SetStateAction, useState } from 'react'; +import React, { Dispatch, FC, SetStateAction, useEffect, useState } from 'react'; import { EuiCode, EuiInputPopover } from '@elastic/eui'; @@ -30,11 +30,15 @@ interface ErrorMessage { interface ExplorationQueryBarProps { indexPattern: IIndexPattern; setSearchQuery: Dispatch>; + includeQueryString?: boolean; + defaultQueryString?: string; } export const ExplorationQueryBar: FC = ({ indexPattern, setSearchQuery, + includeQueryString = false, + defaultQueryString, }) => { // The internal state of the input query bar updated on every key stroke. const [searchInput, setSearchInput] = useState({ @@ -44,20 +48,34 @@ export const ExplorationQueryBar: FC = ({ const [errorMessage, setErrorMessage] = useState(undefined); + useEffect(() => { + if (defaultQueryString !== undefined) { + setSearchInput({ query: defaultQueryString, language: SEARCH_QUERY_LANGUAGE.KUERY }); + } + }, []); + const searchChangeHandler = (query: Query) => setSearchInput(query); const searchSubmitHandler = (query: Query) => { try { switch (query.language) { case SEARCH_QUERY_LANGUAGE.KUERY: + const convertedKQuery = esKuery.toElasticsearchQuery( + esKuery.fromKueryExpression(query.query as string), + indexPattern + ); setSearchQuery( - esKuery.toElasticsearchQuery( - esKuery.fromKueryExpression(query.query as string), - indexPattern - ) + includeQueryString + ? { queryString: query.query, query: convertedKQuery } + : convertedKQuery ); return; case SEARCH_QUERY_LANGUAGE.LUCENE: - setSearchQuery(esQuery.luceneStringToDsl(query.query as string)); + const convertedLQuery = esQuery.luceneStringToDsl(query.query as string); + setSearchQuery( + includeQueryString + ? { queryString: query.query, query: convertedLQuery } + : convertedLQuery + ); return; } } catch (e) { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx index 72514c91ff58b3..295a3988e1b58e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; -import { DeepReadonly } from '../../../../../../../common/types/common'; +// import { DeepReadonly } from '../../../../../../../common/types/common'; import { checkPermission, @@ -21,7 +21,7 @@ import { isClassificationAnalysis, } from '../../../../common/analytics'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; -import { CloneAction } from './action_clone'; +// import { CloneAction } from './action_clone'; import { getResultsUrl, isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; import { stopAnalytics } from '../../services/analytics_service'; @@ -106,10 +106,10 @@ export const getActions = (createAnalyticsForm: CreateAnalyticsFormProps) => { return ; }, }, - { - render: (item: DeepReadonly) => { - return ; - }, - }, + // { + // render: (item: DeepReadonly) => { + // return ; + // }, + // }, ]; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 4a99c042b108b7..bb012a21908597 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -53,6 +53,7 @@ import { CreateAnalyticsButton } from '../create_analytics_button'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; import { CreateAnalyticsFlyoutWrapper } from '../create_analytics_flyout_wrapper'; import { getSelectedJobIdFromUrl } from '../../../../../jobs/jobs_list/components/utils'; +import { SourceSelection } from '../source_selection'; function getItemIdToExpandedRowMap( itemIds: DataFrameAnalyticsId[], @@ -90,6 +91,7 @@ export const DataFrameAnalyticsList: FC = ({ createAnalyticsForm, }) => { const [isInitialized, setIsInitialized] = useState(false); + const [isSourceIndexModalVisible, setIsSourceIndexModalVisible] = useState(false); const [isLoading, setIsLoading] = useState(false); const [filterActive, setFilterActive] = useState(false); @@ -271,7 +273,7 @@ export const DataFrameAnalyticsList: FC = ({ !isManagementTable && createAnalyticsForm ? [ setIsSourceIndexModalVisible(true)} isDisabled={disabled} data-test-subj="mlAnalyticsCreateFirstButton" > @@ -287,6 +289,9 @@ export const DataFrameAnalyticsList: FC = ({ {!isManagementTable && createAnalyticsForm && ( )} + {isSourceIndexModalVisible === true && ( + setIsSourceIndexModalVisible(false)} /> + )} ); } @@ -402,7 +407,10 @@ export const DataFrameAnalyticsList: FC = ({ {!isManagementTable && createAnalyticsForm && ( - + )} @@ -435,6 +443,9 @@ export const DataFrameAnalyticsList: FC = ({ {!isManagementTable && createAnalyticsForm?.state.isModalVisible && ( )} + {isSourceIndexModalVisible === true && ( + setIsSourceIndexModalVisible(false)} /> + )} ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx index 48eb95948bb12b..17b905cab135bb 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx @@ -23,11 +23,14 @@ import { XJsonMode } from '../../../../../../../shared_imports'; const xJsonMode = new XJsonMode(); import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; +import { CreateStep } from '../../../analytics_creation/components/create_step'; +import { ANALYTICS_STEPS } from '../../../analytics_creation/page'; -export const CreateAnalyticsAdvancedEditor: FC = ({ actions, state }) => { +export const CreateAnalyticsAdvancedEditor: FC = (props) => { + const { actions, state } = props; const { setAdvancedEditorRawString, setFormState } = actions; - const { advancedEditorMessages, advancedEditorRawString, isJobCreated, requestMessages } = state; + const { advancedEditorMessages, advancedEditorRawString, isJobCreated } = state; const { createIndexPattern, @@ -56,120 +59,105 @@ export const CreateAnalyticsAdvancedEditor: FC = ({ ac return ( - {requestMessages.map((requestMessage, i) => ( + + { + if (input) { + forceInput.current = input; + } + }} + disabled={isJobCreated} + placeholder="analytics job ID" + value={jobId} + onChange={(e) => setFormState({ jobId: e.target.value })} + aria-label={i18n.translate( + 'xpack.ml.dataframe.analytics.create.advancedEditor.jobIdInputAriaLabel', + { + defaultMessage: 'Choose a unique analytics job ID.', + } + )} + isInvalid={(!jobIdEmpty && !jobIdValid) || jobIdExists} + /> + + + + + + + {advancedEditorMessages.map((advancedEditorMessage, i) => ( - {requestMessage.error !== undefined ?

{requestMessage.error}

: null} + {advancedEditorMessage.message !== '' && advancedEditorMessage.error !== undefined ? ( +

{advancedEditorMessage.error}

+ ) : null}
- +
))} {!isJobCreated && ( - - { - if (input) { - forceInput.current = input; - } - }} - disabled={isJobCreated} - placeholder="analytics job ID" - value={jobId} - onChange={(e) => setFormState({ jobId: e.target.value })} - aria-label={i18n.translate( - 'xpack.ml.dataframe.analytics.create.advancedEditor.jobIdInputAriaLabel', - { - defaultMessage: 'Choose a unique analytics job ID.', - } - )} - isInvalid={(!jobIdEmpty && !jobIdValid) || jobIdExists} - /> - - - - - - - {advancedEditorMessages.map((advancedEditorMessage, i) => ( - - - {advancedEditorMessage.message !== '' && - advancedEditorMessage.error !== undefined ? ( -

{advancedEditorMessage.error}

- ) : null} -
- -
- ))} = ({ ac
)} + +
); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/_index.scss b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/_index.scss new file mode 100644 index 00000000000000..14ff9de7ded4d4 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/_index.scss @@ -0,0 +1,4 @@ +.dataFrameAnalyticsCreateSearchDialog { + width: $euiSizeL * 30; + min-height: $euiSizeL * 25; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx index 10565fb4d7a934..8e9b09ef41fa83 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx @@ -35,7 +35,9 @@ describe('Data Frame Analytics: ', () => { test('Minimal initialization', () => { const { getLastHookValue } = getMountedHook(); const props = getLastHookValue(); - const wrapper = mount(); + const wrapper = mount( + + ); expect(wrapper.find('EuiButton').text()).toBe('Create analytics job'); }); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx index 952bd48468b0af..595a1cf73e189e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx @@ -10,15 +10,26 @@ import { i18n } from '@kbn/i18n'; import { createPermissionFailureMessage } from '../../../../../capabilities/check_capabilities'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; -export const CreateAnalyticsButton: FC = (props) => { - const { disabled } = props.state; - const { openModal } = props.actions; +interface Props extends CreateAnalyticsFormProps { + setIsSourceIndexModalVisible: React.Dispatch>; +} + +export const CreateAnalyticsButton: FC = ({ + state, + actions, + setIsSourceIndexModalVisible, +}) => { + const { disabled } = state; + + const handleClick = () => { + setIsSourceIndexModalVisible(true); + }; const button = ( = ({ messages }) => { {messages.map((requestMessage, i) => ( void; +} + +export const SourceSelection: FC = ({ onClose }) => { + const { uiSettings, savedObjects } = useMlKibana().services; + + const onSearchSelected = (id: string, type: string) => { + window.location.href = `ml#/data_frame_analytics/new_job?${ + type === 'index-pattern' ? 'index' : 'savedSearchId' + }=${encodeURIComponent(id)}`; + }; + + return ( + <> + + + + + {' '} + /{' '} + + + + + 'search', + name: i18n.translate( + 'xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.search', + { + defaultMessage: 'Saved search', + } + ), + }, + { + type: 'index-pattern', + getIconForSavedObject: () => 'indexPatternApp', + name: i18n.translate( + 'xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.indexPattern', + { + defaultMessage: 'Index pattern', + } + ), + }, + ]} + fixedPageSize={fixedPageSize} + uiSettings={uiSettings} + savedObjects={savedObjects} + /> + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts index 66e4103f5bb41d..c42e03b584a568 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts @@ -72,6 +72,7 @@ export interface ActionDispatchers { closeModal: () => void; createAnalyticsJob: () => void; openModal: () => Promise; + initiateWizard: () => Promise; resetAdvancedEditorMessages: () => void; setAdvancedEditorRawString: (payload: State['advancedEditorRawString']) => void; setFormState: (payload: Partial) => void; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts index e5e56c084f7b99..8ec415bc2abb46 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts @@ -5,4 +5,9 @@ */ export { DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES } from './state'; -export { useCreateAnalyticsForm, CreateAnalyticsFormProps } from './use_create_analytics_form'; +export { + AnalyticsCreationStep, + useCreateAnalyticsForm, + CreateAnalyticsFormProps, + CreateAnalyticsStepProps, +} from './use_create_analytics_form'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 4ff7deab34f26a..387ce89ee41204 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -11,6 +11,7 @@ import { mlNodesAvailable } from '../../../../../ml_nodes_check'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { + FieldSelectionItem, isClassificationAnalysis, isRegressionAnalysis, DataFrameAnalyticsId, @@ -26,7 +27,8 @@ export enum DEFAULT_MODEL_MEMORY_LIMIT { classification = '100mb', } -export const DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES = 2; +export const DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES = 0; +export const UNSET_CONFIG_ITEM = '--'; export type EsIndexName = string; export type DependentVariable = string; @@ -47,6 +49,7 @@ export interface State { advancedEditorMessages: FormMessage[]; advancedEditorRawString: string; form: { + computeFeatureInfluence: string; createIndexPattern: boolean; dependentVariable: DependentVariable; dependentVariableFetchFail: boolean; @@ -57,31 +60,47 @@ export interface State { destinationIndexNameEmpty: boolean; destinationIndexNameValid: boolean; destinationIndexPatternTitleExists: boolean; + eta: undefined | number; excludes: string[]; + excludesTableItems: FieldSelectionItem[]; excludesOptions: EuiComboBoxOptionOption[]; + featureBagFraction: undefined | number; + featureInfluenceThreshold: undefined | number; fieldOptionsFetchFail: boolean; + gamma: undefined | number; jobId: DataFrameAnalyticsId; jobIdExists: boolean; jobIdEmpty: boolean; jobIdInvalidMaxLength: boolean; jobIdValid: boolean; jobType: AnalyticsJobType; + jobConfigQuery: any; + jobConfigQueryString: string | undefined; + lambda: number | undefined; loadingDepVarOptions: boolean; loadingFieldOptions: boolean; maxDistinctValuesError: string | undefined; + maxTrees: undefined | number; + method: undefined | string; modelMemoryLimit: string | undefined; modelMemoryLimitUnitValid: boolean; modelMemoryLimitValidationResult: any; + nNeighbors: undefined | number; numTopFeatureImportanceValues: number | undefined; numTopFeatureImportanceValuesValid: boolean; + numTopClasses: number; + outlierFraction: undefined | number; + predictionFieldName: undefined | string; previousJobType: null | AnalyticsJobType; previousSourceIndex: EsIndexName | undefined; requiredFieldsError: string | undefined; + randomizeSeed: undefined | number; sourceIndex: EsIndexName; sourceIndexNameEmpty: boolean; sourceIndexNameValid: boolean; sourceIndexContainsNumericalFields: boolean; sourceIndexFieldsCheckFailed: boolean; + standardizationEnabled: undefined | string; trainingPercent: number; }; disabled: boolean; @@ -105,7 +124,8 @@ export const getInitialState = (): State => ({ advancedEditorMessages: [], advancedEditorRawString: '', form: { - createIndexPattern: false, + computeFeatureInfluence: 'true', + createIndexPattern: true, dependentVariable: '', dependentVariableFetchFail: false, dependentVariableOptions: [], @@ -115,8 +135,13 @@ export const getInitialState = (): State => ({ destinationIndexNameEmpty: true, destinationIndexNameValid: false, destinationIndexPatternTitleExists: false, + eta: undefined, excludes: [], + featureBagFraction: undefined, + featureInfluenceThreshold: undefined, fieldOptionsFetchFail: false, + gamma: undefined, + excludesTableItems: [], excludesOptions: [], jobId: '', jobIdExists: false, @@ -124,22 +149,33 @@ export const getInitialState = (): State => ({ jobIdInvalidMaxLength: false, jobIdValid: false, jobType: undefined, + jobConfigQuery: { match_all: {} }, + jobConfigQueryString: undefined, + lambda: undefined, loadingDepVarOptions: false, loadingFieldOptions: false, maxDistinctValuesError: undefined, + maxTrees: undefined, + method: undefined, modelMemoryLimit: undefined, modelMemoryLimitUnitValid: true, modelMemoryLimitValidationResult: null, + nNeighbors: undefined, numTopFeatureImportanceValues: DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES, numTopFeatureImportanceValuesValid: true, + numTopClasses: 2, + outlierFraction: undefined, + predictionFieldName: undefined, previousJobType: null, previousSourceIndex: undefined, requiredFieldsError: undefined, + randomizeSeed: undefined, sourceIndex: '', sourceIndexNameEmpty: true, sourceIndexNameValid: false, sourceIndexContainsNumericalFields: true, sourceIndexFieldsCheckFailed: false, + standardizationEnabled: 'true', trainingPercent: 80, }, jobConfig: {}, @@ -222,6 +258,7 @@ export const getJobConfigFromFormState = ( index: formState.sourceIndex.includes(',') ? formState.sourceIndex.split(',').map((d) => d.trim()) : formState.sourceIndex, + query: formState.jobConfigQuery, }, dest: { index: formState.destinationIndex, @@ -239,15 +276,55 @@ export const getJobConfigFromFormState = ( formState.jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || formState.jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION ) { - jobConfig.analysis = { - [formState.jobType]: { - dependent_variable: formState.dependentVariable, - num_top_feature_importance_values: formState.numTopFeatureImportanceValues, - training_percent: formState.trainingPercent, + let analysis = { + dependent_variable: formState.dependentVariable, + num_top_feature_importance_values: formState.numTopFeatureImportanceValues, + training_percent: formState.trainingPercent, + }; + + analysis = Object.assign( + analysis, + formState.predictionFieldName && { prediction_field_name: formState.predictionFieldName }, + formState.eta && { eta: formState.eta }, + formState.featureBagFraction && { + feature_bag_fraction: formState.featureBagFraction, }, + formState.gamma && { gamma: formState.gamma }, + formState.lambda && { lambda: formState.lambda }, + formState.maxTrees && { max_trees: formState.maxTrees }, + formState.randomizeSeed && { randomize_seed: formState.randomizeSeed } + ); + + jobConfig.analysis = { + [formState.jobType]: analysis, }; } + if ( + formState.jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && + jobConfig?.analysis?.classification !== undefined && + formState.numTopClasses !== undefined + ) { + // @ts-ignore + jobConfig.analysis.classification.num_top_classes = formState.numTopClasses; + } + + if (formState.jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION) { + const analysis = Object.assign( + {}, + formState.method && { method: formState.method }, + formState.nNeighbors && { + n_neighbors: formState.nNeighbors, + }, + formState.outlierFraction && { outlier_fraction: formState.outlierFraction }, + formState.standardizationEnabled && { + standardization_enabled: formState.standardizationEnabled, + } + ); + // @ts-ignore + jobConfig.analysis.outlier_detection = analysis; + } + return jobConfig; }; @@ -279,6 +356,11 @@ export function getCloneFormStateFromJobConfig( resultState.dependentVariable = analysisConfig.dependent_variable; resultState.numTopFeatureImportanceValues = analysisConfig.num_top_feature_importance_values; resultState.trainingPercent = analysisConfig.training_percent; + + if (isClassificationAnalysis(analyticsJobConfig.analysis)) { + // @ts-ignore + resultState.numTopClasses = analysisConfig.num_top_classes; + } } return resultState; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 1ec767d014a2e3..c4cbe149f88bc1 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -36,11 +36,24 @@ import { getCloneFormStateFromJobConfig, } from './state'; +import { ANALYTICS_STEPS } from '../../../analytics_creation/page'; + +export interface AnalyticsCreationStep { + number: ANALYTICS_STEPS; + completed: boolean; +} + export interface CreateAnalyticsFormProps { actions: ActionDispatchers; state: State; } +export interface CreateAnalyticsStepProps extends CreateAnalyticsFormProps { + setCurrentStep: React.Dispatch>; + step?: ANALYTICS_STEPS; + stepActivated?: boolean; +} + export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { const mlContext = useMlContext(); const [state, dispatch] = useReducer(reducer, getInitialState()); @@ -261,8 +274,12 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { dispatch({ type: ACTION.OPEN_MODAL }); }; + const initiateWizard = async () => { + await mlContext.indexPatterns.clearCache(); + await prepareFormValidation(); + }; + const startAnalyticsJob = async () => { - setIsModalButtonDisabled(true); try { const response = await ml.dataFrameAnalytics.startDataFrameAnalytics(jobId); if (response.acknowledged !== true) { @@ -278,7 +295,6 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { ), }); setIsJobStarted(true); - setIsModalButtonDisabled(false); refresh(); } catch (e) { addRequestMessage({ @@ -290,7 +306,6 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { } ), }); - setIsModalButtonDisabled(false); } }; @@ -331,6 +346,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { closeModal, createAnalyticsJob, openModal, + initiateWizard, resetAdvancedEditorMessages, setAdvancedEditorRawString, setFormState, diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx index d68c0342ac857d..97b4043c9fd644 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -27,6 +27,7 @@ import { Query, esQuery, esKuery, + UI_SETTINGS, } from '../../../../../../../src/plugins/data/public'; import { SavedSearchSavedObject } from '../../../../common/types/kibana'; import { NavigationMenu } from '../../components/navigation_menu'; @@ -254,7 +255,7 @@ export const Page: FC = () => { qry = esKuery.toElasticsearchQuery(ast, currentIndexPattern); } else { qry = esQuery.luceneStringToDsl(qryString); - esQuery.decorateQuery(qry, kibanaConfig.get('query:queryString:options')); + esQuery.decorateQuery(qry, kibanaConfig.get(UI_SETTINGS.QUERY_STRING_OPTIONS)); } return { diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx new file mode 100644 index 00000000000000..68af9a2a49cab7 --- /dev/null +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx @@ -0,0 +1,41 @@ +/* + * 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, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { parse } from 'query-string'; +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { basicResolvers } from '../../resolvers'; +import { Page } from '../../../data_frame_analytics/pages/analytics_creation'; +import { ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameManagementLabel', { + defaultMessage: 'Data Frame Analytics', + }), + href: '#/data_frame_analytics', + }, +]; + +export const analyticsJobsCreationRoute: MlRoute = { + path: '/data_frame_analytics/new_job', + render: (props, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ location, deps }) => { + const { index, savedSearchId }: Record = parse(location.search, { sort: false }); + const { context } = useResolver(index, savedSearchId, deps.config, basicResolvers(deps)); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts index 552c15a408b65b..9b6bcc25c8c7eb 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts @@ -6,3 +6,4 @@ export * from './analytics_jobs_list'; export * from './analytics_job_exploration'; +export * from './analytics_job_creation'; diff --git a/x-pack/plugins/ml/public/application/services/explorer_service.ts b/x-pack/plugins/ml/public/application/services/explorer_service.ts index efcec8cf2b9549..717ed3ba64c378 100644 --- a/x-pack/plugins/ml/public/application/services/explorer_service.ts +++ b/x-pack/plugins/ml/public/application/services/explorer_service.ts @@ -6,7 +6,11 @@ import { IUiSettingsClient } from 'kibana/public'; import { i18n } from '@kbn/i18n'; -import { TimefilterContract, TimeRange } from '../../../../../../src/plugins/data/public'; +import { + TimefilterContract, + TimeRange, + UI_SETTINGS, +} from '../../../../../../src/plugins/data/public'; import { getBoundsRoundedToInterval, TimeBuckets, TimeRangeBounds } from '../util/time_buckets'; import { ExplorerJob, OverallSwimlaneData, SwimlaneData } from '../explorer/explorer_utils'; import { VIEW_BY_JOB_LABEL } from '../explorer/explorer_constants'; @@ -25,8 +29,8 @@ export class ExplorerService { private mlResultsService: MlResultsService ) { this.timeBuckets = new TimeBuckets({ - 'histogram:maxBars': uiSettings.get('histogram:maxBars'), - 'histogram:barTarget': uiSettings.get('histogram:barTarget'), + 'histogram:maxBars': uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), + 'histogram:barTarget': uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), dateFormat: uiSettings.get('dateFormat'), 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), }); diff --git a/x-pack/plugins/ml/public/application/util/time_buckets.d.ts b/x-pack/plugins/ml/public/application/util/time_buckets.d.ts index 9c5d663a6e4ab1..91661ea3fd3fab 100644 --- a/x-pack/plugins/ml/public/application/util/time_buckets.d.ts +++ b/x-pack/plugins/ml/public/application/util/time_buckets.d.ts @@ -5,6 +5,7 @@ */ import { Moment } from 'moment'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; export interface TimeRangeBounds { min?: Moment; diff --git a/x-pack/plugins/ml/public/application/util/time_buckets.js b/x-pack/plugins/ml/public/application/util/time_buckets.js index 2b23eca1ab5c0f..1915a4ce6516bc 100644 --- a/x-pack/plugins/ml/public/application/util/time_buckets.js +++ b/x-pack/plugins/ml/public/application/util/time_buckets.js @@ -11,7 +11,7 @@ import dateMath from '@elastic/datemath'; import { timeBucketsCalcAutoIntervalProvider } from './calc_auto_interval'; import { parseInterval } from '../../../common/util/parse_interval'; import { getFieldFormats, getUiSettings } from './dependency_cache'; -import { FIELD_FORMAT_IDS } from '../../../../../../src/plugins/data/public'; +import { FIELD_FORMAT_IDS, UI_SETTINGS } from '../../../../../../src/plugins/data/public'; const unitsDesc = dateMath.unitsDesc; const largeMax = unitsDesc.indexOf('w'); // Multiple units of week or longer converted to days for ES intervals. @@ -21,8 +21,8 @@ const calcAuto = timeBucketsCalcAutoIntervalProvider(); export function getTimeBucketsFromCache() { const uiSettings = getUiSettings(); return new TimeBuckets({ - 'histogram:maxBars': uiSettings.get('histogram:maxBars'), - 'histogram:barTarget': uiSettings.get('histogram:barTarget'), + [UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), + [UI_SETTINGS.HISTOGRAM_BAR_TARGET]: uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), dateFormat: uiSettings.get('dateFormat'), 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), }); @@ -35,8 +35,8 @@ export function getTimeBucketsFromCache() { */ export function TimeBuckets(timeBucketsConfig) { this._timeBucketsConfig = timeBucketsConfig; - this.barTarget = this._timeBucketsConfig['histogram:barTarget']; - this.maxBars = this._timeBucketsConfig['histogram:maxBars']; + this.barTarget = this._timeBucketsConfig[UI_SETTINGS.HISTOGRAM_BAR_TARGET]; + this.maxBars = this._timeBucketsConfig[UI_SETTINGS.HISTOGRAM_MAX_BARS]; } /** diff --git a/x-pack/plugins/ml/public/application/util/time_buckets.test.js b/x-pack/plugins/ml/public/application/util/time_buckets.test.js index baecf7df906412..250c7255f5b99f 100644 --- a/x-pack/plugins/ml/public/application/util/time_buckets.test.js +++ b/x-pack/plugins/ml/public/application/util/time_buckets.test.js @@ -5,6 +5,7 @@ */ import moment from 'moment'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; import { TimeBuckets, getBoundsRoundedToInterval, calcEsInterval } from './time_buckets'; describe('ML - time buckets', () => { @@ -13,8 +14,8 @@ describe('ML - time buckets', () => { beforeEach(() => { const timeBucketsConfig = { - 'histogram:maxBars': 100, - 'histogram:barTarget': 50, + [UI_SETTINGS.HISTOGRAM_MAX_BARS]: 100, + [UI_SETTINGS.HISTOGRAM_BAR_TARGET]: 50, }; autoBuckets = new TimeBuckets(timeBucketsConfig); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts index e48ce9f9a1d92d..74a7b432de2673 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts @@ -27,7 +27,7 @@ import { MlStartDependencies } from '../../plugin'; import { SWIMLANE_TYPE } from '../../application/explorer/explorer_constants'; import { Filter } from '../../../../../../src/plugins/data/common/es_query/filters'; import { Query } from '../../../../../../src/plugins/data/common/query'; -import { esKuery } from '../../../../../../src/plugins/data/public'; +import { esKuery, UI_SETTINGS } from '../../../../../../src/plugins/data/public'; import { ExplorerJob, OverallSwimlaneData } from '../../application/explorer/explorer_utils'; import { parseInterval } from '../../../common/util/parse_interval'; import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; @@ -62,8 +62,8 @@ export function useSwimlaneInputResolver( const timeBuckets = useMemo(() => { return new TimeBuckets({ - 'histogram:maxBars': uiSettings.get('histogram:maxBars'), - 'histogram:barTarget': uiSettings.get('histogram:barTarget'), + 'histogram:maxBars': uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), + 'histogram:barTarget': uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), dateFormat: uiSettings.get('dateFormat'), 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), }); diff --git a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts index 0b2469c103578c..e6b4e4ccf85823 100644 --- a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts @@ -47,6 +47,7 @@ export const dataAnalyticsExplainSchema = schema.object({ /** Source */ source: schema.object({ index: schema.string(), + query: schema.maybe(schema.any()), }), analysis: schema.any(), analyzed_fields: schema.maybe(schema.any()), diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index 73133da5a006ce..24383028e558c9 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -17,6 +17,7 @@ import { FeatureCatalogueCategory, HomePublicPluginSetup, } from '../../../../src/plugins/home/public'; +import { UI_SETTINGS } from '../../../../src/plugins/data/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { MonitoringPluginDependencies, MonitoringConfig } from './types'; import { @@ -110,7 +111,7 @@ export class MonitoringPlugin timefilter.setRefreshInterval(refreshInterval); timefilter.setTime(time); uiSettings.overrideLocalDefault( - 'timepicker:refreshIntervalDefaults', + UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS, JSON.stringify(refreshInterval) ); uiSettings.overrideLocalDefault('timepicker:timeDefaults', JSON.stringify(time)); diff --git a/x-pack/plugins/monitoring/public/views/access_denied/index.html b/x-pack/plugins/monitoring/public/views/access_denied/index.html index 63cd4440ecf8a2..24863559212f78 100644 --- a/x-pack/plugins/monitoring/public/views/access_denied/index.html +++ b/x-pack/plugins/monitoring/public/views/access_denied/index.html @@ -30,12 +30,13 @@
- + + +
diff --git a/x-pack/plugins/monitoring/public/views/access_denied/index.js b/x-pack/plugins/monitoring/public/views/access_denied/index.js index 856e59702963a1..f7a4d03a26452b 100644 --- a/x-pack/plugins/monitoring/public/views/access_denied/index.js +++ b/x-pack/plugins/monitoring/public/views/access_denied/index.js @@ -4,16 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { noop } from 'lodash'; +import { kbnBaseUrl } from '../../../../../../src/plugins/kibana_legacy/common/kbn_base_url'; import { uiRoutes } from '../../angular/helpers/routes'; -import { Legacy } from '../../legacy_shims'; import template from './index.html'; const tryPrivilege = ($http, kbnUrl) => { return $http .get('../api/monitoring/v1/check_access') .then(() => kbnUrl.redirect('/home')) - .catch(noop); + .catch(() => true); }; uiRoutes.when('/access-denied', { @@ -31,17 +30,13 @@ uiRoutes.when('/access-denied', { }, }, controllerAs: 'accessDenied', - controller($scope, $injector) { - const $window = $injector.get('$window'); - const kbnBaseUrl = $injector.get('kbnBaseUrl'); + controller: function ($scope, $injector) { const $http = $injector.get('$http'); const kbnUrl = $injector.get('kbnUrl'); const $interval = $injector.get('$interval'); // The template's "Back to Kibana" button click handler - this.goToKibana = () => { - $window.location.href = Legacy.shims.getBasePath() + kbnBaseUrl; - }; + this.goToKibanaURL = kbnBaseUrl; // keep trying to load data in the background const accessPoller = $interval(() => tryPrivilege($http, kbnUrl), 5 * 1000); // every 5 seconds diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts index dfabfa98f8cbf3..b1234a6ddf0b66 100644 --- a/x-pack/plugins/reporting/server/config/schema.ts +++ b/x-pack/plugins/reporting/server/config/schema.ts @@ -162,6 +162,7 @@ const PollSchema = schema.object({ }); export const ConfigSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), kibanaServer: KibanaServerSchema, queue: QueueSchema, capture: CaptureSchema, diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.test.ts index 6f227d00974ca1..ddcf94079ade42 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.test.ts @@ -8,7 +8,7 @@ import nodeCrypto from '@elastic/node-crypto'; // @ts-ignore import Puid from 'puid'; import sinon from 'sinon'; -import { fieldFormats } from '../../../../../../../src/plugins/data/server'; +import { fieldFormats, UI_SETTINGS } from '../../../../../../../src/plugins/data/server'; import { CancellationToken } from '../../../../common'; import { CSV_BOM_CHARS } from '../../../../common/constants'; import { LevelLogger } from '../../../lib'; @@ -16,6 +16,10 @@ import { setFieldFormats } from '../../../services'; import { createMockReportingCore } from '../../../test_helpers'; import { JobDocPayloadDiscoverCsv } from '../types'; import { executeJobFactory } from './execute_job'; +import { + CSV_SEPARATOR_SETTING, + CSV_QUOTE_VALUES_SETTING, +} from '../../../../../../../src/plugins/share/server'; const delay = (ms: number) => new Promise((resolve) => setTimeout(() => resolve(), ms)); @@ -94,13 +98,13 @@ describe('CSV Execute Job', function () { .stub(clusterStub, 'callAsCurrentUser') .resolves(defaultElasticsearchResponse); - mockUiSettingsClient.get.withArgs('csv:separator').returns(','); - mockUiSettingsClient.get.withArgs('csv:quoteValues').returns(true); + mockUiSettingsClient.get.withArgs(CSV_SEPARATOR_SETTING).returns(','); + mockUiSettingsClient.get.withArgs(CSV_QUOTE_VALUES_SETTING).returns(true); setFieldFormats({ fieldFormatServiceFactory() { const uiConfigMock = {}; - (uiConfigMock as any)['format:defaultTypeMap'] = { + (uiConfigMock as any)[UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP] = { _default_: { id: 'string', params: {} }, }; @@ -748,7 +752,7 @@ describe('CSV Execute Job', function () { }); it('should use custom uiSettings csv:separator for header', async function () { - mockUiSettingsClient.get.withArgs('csv:separator').returns(';'); + mockUiSettingsClient.get.withArgs(CSV_SEPARATOR_SETTING).returns(';'); const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, @@ -760,7 +764,7 @@ describe('CSV Execute Job', function () { }); it('should escape column headers if uiSettings csv:quoteValues is true', async function () { - mockUiSettingsClient.get.withArgs('csv:quoteValues').returns(true); + mockUiSettingsClient.get.withArgs(CSV_QUOTE_VALUES_SETTING).returns(true); const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, @@ -772,7 +776,7 @@ describe('CSV Execute Job', function () { }); it(`shouldn't escape column headers if uiSettings csv:quoteValues is false`, async function () { - mockUiSettingsClient.get.withArgs('csv:quoteValues').returns(false); + mockUiSettingsClient.get.withArgs(CSV_QUOTE_VALUES_SETTING).returns(false); const executeJob = await executeJobFactory(mockReportingCore, mockLogger); const jobParams = getJobDocPayload({ headers: encryptedHeaders, diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.ts index de5ddb2503b2f8..4b17cc669efe1b 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.ts @@ -7,6 +7,10 @@ import { i18n } from '@kbn/i18n'; import Hapi from 'hapi'; import { IUiSettingsClient, KibanaRequest } from '../../../../../../../src/core/server'; +import { + CSV_SEPARATOR_SETTING, + CSV_QUOTE_VALUES_SETTING, +} from '../../../../../../../src/plugins/share/server'; import { ReportingCore } from '../../..'; import { CSV_BOM_CHARS, CSV_JOB_TYPE } from '../../../../common/constants'; import { getFieldFormats } from '../../../../server/services'; @@ -94,8 +98,8 @@ export const executeJobFactory: ExecuteJobFactory { const [separator, quoteValues, timezone] = await Promise.all([ - client.get('csv:separator'), - client.get('csv:quoteValues'), + client.get(CSV_SEPARATOR_SETTING), + client.get(CSV_QUOTE_VALUES_SETTING), client.get('dateFormat:tz'), ]); diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/lib/field_format_map.test.ts b/x-pack/plugins/reporting/server/export_types/csv/server/lib/field_format_map.test.ts index 0434da3d11fe1e..83aa23de676639 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/lib/field_format_map.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/server/lib/field_format_map.test.ts @@ -9,6 +9,7 @@ import expect from '@kbn/expect'; import { fieldFormats, FieldFormatsGetConfigFn, + UI_SETTINGS, } from '../../../../../../../../src/plugins/data/server'; import { fieldFormatMapFactory } from './field_format_map'; @@ -28,10 +29,10 @@ describe('field format map', function () { }, }; const configMock: Record = {}; - configMock['format:defaultTypeMap'] = { + configMock[UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP] = { number: { id: 'number', params: {} }, }; - configMock['format:number:defaultPattern'] = '0,0.[000]'; + configMock[UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN] = '0,0.[000]'; const getConfig = ((key: string) => configMock[key]) as FieldFormatsGetConfigFn; const testValue = '4000'; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts index a9e4366f4eda6b..3f997a703bef12 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts @@ -16,7 +16,12 @@ import { Filter, IIndexPattern, Query, + UI_SETTINGS, } from '../../../../../../../../src/plugins/data/server'; +import { + CSV_SEPARATOR_SETTING, + CSV_QUOTE_VALUES_SETTING, +} from '../../../../../../../../src/plugins/share/server'; import { CancellationToken } from '../../../../../common'; import { LevelLogger } from '../../../../lib'; import { createGenerateCsv } from '../../../csv/server/lib/generate_csv'; @@ -32,9 +37,9 @@ import { getFilters } from './get_filters'; const getEsQueryConfig = async (config: IUiSettingsClient) => { const configs = await Promise.all([ - config.get('query:allowLeadingWildcards'), - config.get('query:queryString:options'), - config.get('courier:ignoreFilterIfFieldNotInIndex'), + config.get(UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS), + config.get(UI_SETTINGS.QUERY_STRING_OPTIONS), + config.get(UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX), ]); const [allowLeadingWildcards, queryStringOptions, ignoreFilterIfFieldNotInIndex] = configs; return { @@ -45,7 +50,10 @@ const getEsQueryConfig = async (config: IUiSettingsClient) => { }; const getUiSettings = async (config: IUiSettingsClient) => { - const configs = await Promise.all([config.get('csv:separator'), config.get('csv:quoteValues')]); + const configs = await Promise.all([ + config.get(CSV_SEPARATOR_SETTING), + config.get(CSV_QUOTE_VALUES_SETTING), + ]); const [separator, quoteValues] = configs; return { separator, quoteValues }; }; diff --git a/x-pack/plugins/security/public/account_management/account_management_app.ts b/x-pack/plugins/security/public/account_management/account_management_app.ts index 41567a04fe0302..0bb7785259c0ea 100644 --- a/x-pack/plugins/security/public/account_management/account_management_app.ts +++ b/x-pack/plugins/security/public/account_management/account_management_app.ts @@ -5,7 +5,12 @@ */ import { i18n } from '@kbn/i18n'; -import { StartServicesAccessor, ApplicationSetup, AppMountParameters } from 'src/core/public'; +import { + ApplicationSetup, + AppMountParameters, + AppNavLinkStatus, + StartServicesAccessor, +} from '../../../../../src/core/public'; import { AuthenticationServiceSetup } from '../authentication'; interface CreateDeps { @@ -23,8 +28,7 @@ export const accountManagementApp = Object.freeze({ application.register({ id: this.id, title, - // TODO: switch to proper enum once https://github.com/elastic/kibana/issues/58327 is resolved. - navLinkStatus: 3, + navLinkStatus: AppNavLinkStatus.hidden, appRoute: '/security/account', async mount({ element }: AppMountParameters) { const [ diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.scss b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.scss index 6784052ef4337e..344cde9c7825ce 100644 --- a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.scss +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.scss @@ -23,9 +23,10 @@ } &:focus { + @include euiFocusRing; + border-color: transparent; border-radius: $euiBorderRadius; - @include euiFocusRing; .secLoginCard__title { text-decoration: underline; diff --git a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts index 98110a83103aa0..6821c163d817dd 100644 --- a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts +++ b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts @@ -11,13 +11,15 @@ import { Feature } from '../../../../../features/public'; import { KibanaPrivileges } from '../model'; import { SecurityLicenseFeatures } from '../../..'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { featuresPluginMock } from '../../../../../features/server/mocks'; + export const createRawKibanaPrivileges = ( features: Feature[], { allowSubFeaturePrivileges = true } = {} ) => { - const featuresService = { - getFeatures: () => features, - }; + const featuresService = featuresPluginMock.createSetup(); + featuresService.getFeatures.mockReturnValue(features); const licensingService = { getFeatures: () => ({ allowSubFeaturePrivileges } as SecurityLicenseFeatures), diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx index afb8b6ec5dbe0c..43387d913e6fc5 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -163,7 +163,12 @@ function getProps({ const { http, docLinks, notifications } = coreMock.createStart(); http.get.mockImplementation(async (path: any) => { if (path === '/api/spaces/space') { - return buildSpaces(); + if (spacesEnabled) { + return buildSpaces(); + } + + const notFoundError = { response: { status: 404 } }; + throw notFoundError; } }); @@ -181,7 +186,6 @@ function getProps({ notifications, docLinks: new DocumentationLinksService(docLinks), fatalErrors, - spacesEnabled, uiCapabilities: buildUICapabilities(canManageSpaces), history: (scopedHistoryMock.create() as unknown) as ScopedHistory, }; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index 77f4455d813c6b..15888733ec4247 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -80,7 +80,6 @@ interface Props { docLinks: DocumentationLinksService; http: HttpStart; license: SecurityLicense; - spacesEnabled: boolean; uiCapabilities: Capabilities; notifications: NotificationsStart; fatalErrors: FatalErrorsSetup; @@ -225,14 +224,21 @@ function useRole( return [role, setRole] as [Role | null, typeof setRole]; } -function useSpaces(http: HttpStart, fatalErrors: FatalErrorsSetup, spacesEnabled: boolean) { - const [spaces, setSpaces] = useState(null); +function useSpaces(http: HttpStart, fatalErrors: FatalErrorsSetup) { + const [spaces, setSpaces] = useState<{ enabled: boolean; list: Space[] } | null>(null); useEffect(() => { - (spacesEnabled ? http.get('/api/spaces/space') : Promise.resolve([])).then( - (fetchedSpaces) => setSpaces(fetchedSpaces), - (err) => fatalErrors.add(err) + http.get('/api/spaces/space').then( + (fetchedSpaces) => setSpaces({ enabled: true, list: fetchedSpaces }), + (err: IHttpFetchError) => { + // Spaces plugin can be disabled and hence this endpoint can be unavailable. + if (err.response?.status === 404) { + setSpaces({ enabled: false, list: [] }); + } else { + fatalErrors.add(err); + } + } ); - }, [http, fatalErrors, spacesEnabled]); + }, [http, fatalErrors]); return spaces; } @@ -278,7 +284,6 @@ export const EditRolePage: FunctionComponent = ({ roleName, action, fatalErrors, - spacesEnabled, license, docLinks, uiCapabilities, @@ -295,7 +300,7 @@ export const EditRolePage: FunctionComponent = ({ const runAsUsers = useRunAsUsers(userAPIClient, fatalErrors); const indexPatternsTitles = useIndexPatternsTitles(indexPatterns, fatalErrors, notifications); const privileges = usePrivileges(privilegesAPIClient, fatalErrors); - const spaces = useSpaces(http, fatalErrors, spacesEnabled); + const spaces = useSpaces(http, fatalErrors); const features = useFeatures(getFeatures, fatalErrors); const [role, setRole] = useRole( rolesAPIClient, @@ -434,8 +439,8 @@ export const EditRolePage: FunctionComponent = ({ = ({ setFormError(null); try { - await rolesAPIClient.saveRole({ role, spacesEnabled }); + await rolesAPIClient.saveRole({ role, spacesEnabled: spaces.enabled }); } catch (error) { notifications.toasts.addDanger(get(error, 'data.message')); return; @@ -554,7 +559,7 @@ export const EditRolePage: FunctionComponent = ({ backToRoleList(); }; - const description = spacesEnabled ? ( + const description = spaces.enabled ? ( (), + getFeatureUsageService: jest + .fn() + .mockReturnValue(securityFeatureUsageServiceMock.createStartContract()), }; } @@ -1451,6 +1455,9 @@ describe('Authenticator', () => { ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); + expect( + mockOptions.getFeatureUsageService().recordPreAccessAgreementUsage + ).not.toHaveBeenCalled(); }); it('fails if cannot retrieve user session', async () => { @@ -1463,6 +1470,9 @@ describe('Authenticator', () => { ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); + expect( + mockOptions.getFeatureUsageService().recordPreAccessAgreementUsage + ).not.toHaveBeenCalled(); }); it('fails if license doesn allow access agreement acknowledgement', async () => { @@ -1477,6 +1487,9 @@ describe('Authenticator', () => { ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); + expect( + mockOptions.getFeatureUsageService().recordPreAccessAgreementUsage + ).not.toHaveBeenCalled(); }); it('properly acknowledges access agreement for the authenticated user', async () => { @@ -1493,6 +1506,10 @@ describe('Authenticator', () => { type: 'basic', name: 'basic1', }); + + expect( + mockOptions.getFeatureUsageService().recordPreAccessAgreementUsage + ).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 98342a8494e383..ac5c2a72b9667c 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -38,6 +38,7 @@ import { DeauthenticationResult } from './deauthentication_result'; import { Tokens } from './tokens'; import { canRedirectRequest } from './can_redirect_request'; import { HTTPAuthorizationHeader } from './http_authentication'; +import { SecurityFeatureUsageServiceStart } from '../feature_usage'; /** * The shape of the session that is actually stored in the cookie. @@ -94,6 +95,7 @@ export interface ProviderLoginAttempt { export interface AuthenticatorOptions { auditLogger: SecurityAuditLogger; + getFeatureUsageService: () => SecurityFeatureUsageServiceStart; getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null; config: Pick; basePath: HttpServiceSetup['basePath']; @@ -502,6 +504,8 @@ export class Authenticator { currentUser.username, existingSession.provider ); + + this.options.getFeatureUsageService().recordPreAccessAgreementUsage(); } /** diff --git a/x-pack/plugins/security/server/authentication/index.test.ts b/x-pack/plugins/security/server/authentication/index.test.ts index 1c1e0ed781f18e..c7323509c00d68 100644 --- a/x-pack/plugins/security/server/authentication/index.test.ts +++ b/x-pack/plugins/security/server/authentication/index.test.ts @@ -42,6 +42,8 @@ import { } from './api_keys'; import { SecurityLicense } from '../../common/licensing'; import { SecurityAuditLogger } from '../audit'; +import { SecurityFeatureUsageServiceStart } from '../feature_usage'; +import { securityFeatureUsageServiceMock } from '../feature_usage/index.mock'; describe('setupAuthentication()', () => { let mockSetupAuthenticationParams: { @@ -51,6 +53,7 @@ describe('setupAuthentication()', () => { http: jest.Mocked; clusterClient: jest.Mocked; license: jest.Mocked; + getFeatureUsageService: () => jest.Mocked; }; let mockScopedClusterClient: jest.Mocked>; beforeEach(() => { @@ -69,6 +72,9 @@ describe('setupAuthentication()', () => { clusterClient: elasticsearchServiceMock.createClusterClient(), license: licenseMock.create(), loggers: loggingServiceMock.create(), + getFeatureUsageService: jest + .fn() + .mockReturnValue(securityFeatureUsageServiceMock.createStartContract()), }; mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index 779b852195b028..ec48c727a57398 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -17,6 +17,7 @@ import { ConfigType } from '../config'; import { getErrorStatusCode } from '../errors'; import { Authenticator, ProviderSession } from './authenticator'; import { APIKeys, CreateAPIKeyParams, InvalidateAPIKeyParams } from './api_keys'; +import { SecurityFeatureUsageServiceStart } from '../feature_usage'; export { canRedirectRequest } from './can_redirect_request'; export { Authenticator, ProviderLoginAttempt } from './authenticator'; @@ -37,6 +38,7 @@ export { interface SetupAuthenticationParams { auditLogger: SecurityAuditLogger; + getFeatureUsageService: () => SecurityFeatureUsageServiceStart; http: CoreSetup['http']; clusterClient: IClusterClient; config: ConfigType; @@ -48,6 +50,7 @@ export type Authentication = UnwrapPromise { diff --git a/x-pack/plugins/security/server/authorization/app_authorization.ts b/x-pack/plugins/security/server/authorization/app_authorization.ts index aead8cb07897c4..1036997ca821d1 100644 --- a/x-pack/plugins/security/server/authorization/app_authorization.ts +++ b/x-pack/plugins/security/server/authorization/app_authorization.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, Logger } from '../../../../../src/core/server'; -import { FeaturesService } from '../plugin'; -import { Authorization } from '.'; +import { HttpServiceSetup, Logger } from '../../../../../src/core/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../../features/server'; +import { AuthorizationServiceSetup } from '.'; class ProtectedApplications { private applications: Set | null = null; - constructor(private readonly featuresService: FeaturesService) {} + constructor(private readonly featuresService: FeaturesPluginSetup) {} public shouldProtect(appId: string) { // Currently, once we get the list of features we essentially "lock" additional @@ -30,14 +30,14 @@ class ProtectedApplications { } export function initAppAuthorization( - http: CoreSetup['http'], + http: HttpServiceSetup, { actions, checkPrivilegesDynamicallyWithRequest, mode, - }: Pick, + }: Pick, logger: Logger, - featuresService: FeaturesService + featuresService: FeaturesPluginSetup ) { const protectedApplications = new ProtectedApplications(featuresService); diff --git a/x-pack/plugins/security/server/authorization/authorization_service.test.ts b/x-pack/plugins/security/server/authorization/authorization_service.test.ts new file mode 100644 index 00000000000000..978c985cfe820d --- /dev/null +++ b/x-pack/plugins/security/server/authorization/authorization_service.test.ts @@ -0,0 +1,263 @@ +/* + * 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 { + mockAuthorizationModeFactory, + mockCheckPrivilegesDynamicallyWithRequestFactory, + mockCheckPrivilegesWithRequestFactory, + mockCheckSavedObjectsPrivilegesWithRequestFactory, + mockPrivilegesFactory, + mockRegisterPrivilegesWithCluster, +} from './service.test.mocks'; + +import { BehaviorSubject } from 'rxjs'; +import { CoreStatus, ServiceStatusLevels } from '../../../../../src/core/server'; +import { checkPrivilegesWithRequestFactory } from './check_privileges'; +import { checkPrivilegesDynamicallyWithRequestFactory } from './check_privileges_dynamically'; +import { checkSavedObjectsPrivilegesWithRequestFactory } from './check_saved_objects_privileges'; +import { authorizationModeFactory } from './mode'; +import { privilegesFactory } from './privileges'; +import { AuthorizationService } from '.'; + +import { + coreMock, + elasticsearchServiceMock, + loggingServiceMock, +} from '../../../../../src/core/server/mocks'; +import { featuresPluginMock } from '../../../features/server/mocks'; +import { licenseMock } from '../../common/licensing/index.mock'; +import { SecurityLicense, SecurityLicenseFeatures } from '../../common/licensing'; +import { nextTick } from 'test_utils/enzyme_helpers'; + +const kibanaIndexName = '.a-kibana-index'; +const application = `kibana-${kibanaIndexName}`; +const mockCheckPrivilegesWithRequest = Symbol(); +const mockCheckPrivilegesDynamicallyWithRequest = Symbol(); +const mockCheckSavedObjectsPrivilegesWithRequest = Symbol(); +const mockPrivilegesService = Symbol(); +const mockAuthorizationMode = Symbol(); +beforeEach(() => { + mockCheckPrivilegesWithRequestFactory.mockReturnValue(mockCheckPrivilegesWithRequest); + mockCheckPrivilegesDynamicallyWithRequestFactory.mockReturnValue( + mockCheckPrivilegesDynamicallyWithRequest + ); + mockCheckSavedObjectsPrivilegesWithRequestFactory.mockReturnValue( + mockCheckSavedObjectsPrivilegesWithRequest + ); + mockPrivilegesFactory.mockReturnValue(mockPrivilegesService); + mockAuthorizationModeFactory.mockReturnValue(mockAuthorizationMode); +}); + +afterEach(() => { + mockRegisterPrivilegesWithCluster.mockClear(); +}); + +it(`#setup returns exposed services`, () => { + const mockClusterClient = elasticsearchServiceMock.createClusterClient(); + const mockGetSpacesService = jest + .fn() + .mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }); + const mockFeaturesSetup = featuresPluginMock.createSetup(); + const mockLicense = licenseMock.create(); + const mockCoreSetup = coreMock.createSetup(); + + const authorizationService = new AuthorizationService(); + const authz = authorizationService.setup({ + http: mockCoreSetup.http, + capabilities: mockCoreSetup.capabilities, + status: mockCoreSetup.status, + clusterClient: mockClusterClient, + license: mockLicense, + loggers: loggingServiceMock.create(), + kibanaIndexName, + packageVersion: 'some-version', + features: mockFeaturesSetup, + getSpacesService: mockGetSpacesService, + }); + + expect(authz.actions.version).toBe('version:some-version'); + expect(authz.applicationName).toBe(application); + + expect(authz.checkPrivilegesWithRequest).toBe(mockCheckPrivilegesWithRequest); + expect(checkPrivilegesWithRequestFactory).toHaveBeenCalledWith( + authz.actions, + mockClusterClient, + authz.applicationName + ); + + expect(authz.checkPrivilegesDynamicallyWithRequest).toBe( + mockCheckPrivilegesDynamicallyWithRequest + ); + expect(checkPrivilegesDynamicallyWithRequestFactory).toHaveBeenCalledWith( + mockCheckPrivilegesWithRequest, + mockGetSpacesService + ); + + expect(authz.checkSavedObjectsPrivilegesWithRequest).toBe( + mockCheckSavedObjectsPrivilegesWithRequest + ); + expect(checkSavedObjectsPrivilegesWithRequestFactory).toHaveBeenCalledWith( + mockCheckPrivilegesWithRequest, + mockGetSpacesService + ); + + expect(authz.privileges).toBe(mockPrivilegesService); + expect(privilegesFactory).toHaveBeenCalledWith(authz.actions, mockFeaturesSetup, mockLicense); + + expect(authz.mode).toBe(mockAuthorizationMode); + expect(authorizationModeFactory).toHaveBeenCalledWith(mockLicense); + + expect(mockCoreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + expect(mockCoreSetup.capabilities.registerSwitcher).toHaveBeenCalledWith(expect.any(Function)); +}); + +describe('#start', () => { + let statusSubject: BehaviorSubject; + let licenseSubject: BehaviorSubject; + let mockLicense: jest.Mocked; + beforeEach(() => { + const mockClusterClient = elasticsearchServiceMock.createClusterClient(); + + licenseSubject = new BehaviorSubject(({} as unknown) as SecurityLicenseFeatures); + mockLicense = licenseMock.create(); + mockLicense.isEnabled.mockReturnValue(false); + mockLicense.features$ = licenseSubject; + + statusSubject = new BehaviorSubject({ + elasticsearch: { level: ServiceStatusLevels.unavailable, summary: 'Service is NOT working' }, + savedObjects: { level: ServiceStatusLevels.unavailable, summary: 'Service is NOT working' }, + }); + const mockCoreSetup = coreMock.createSetup(); + mockCoreSetup.status.core$ = statusSubject; + + const authorizationService = new AuthorizationService(); + authorizationService.setup({ + http: mockCoreSetup.http, + capabilities: mockCoreSetup.capabilities, + status: mockCoreSetup.status, + clusterClient: mockClusterClient, + license: mockLicense, + loggers: loggingServiceMock.create(), + kibanaIndexName, + packageVersion: 'some-version', + features: featuresPluginMock.createSetup(), + getSpacesService: jest + .fn() + .mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }), + }); + + const featuresStart = featuresPluginMock.createStart(); + featuresStart.getFeatures.mockReturnValue([]); + + authorizationService.start({ clusterClient: mockClusterClient, features: featuresStart }); + + // ES and license aren't available yet. + expect(mockRegisterPrivilegesWithCluster).not.toHaveBeenCalled(); + }); + + it('registers cluster privileges', async () => { + // ES is available now, but not license. + statusSubject.next({ + elasticsearch: { level: ServiceStatusLevels.available, summary: 'Service is working' }, + savedObjects: { level: ServiceStatusLevels.unavailable, summary: 'Service is NOT working' }, + }); + expect(mockRegisterPrivilegesWithCluster).not.toHaveBeenCalled(); + + // Both ES and license are available now. + mockLicense.isEnabled.mockReturnValue(true); + licenseSubject.next(({} as unknown) as SecurityLicenseFeatures); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(1); + + await nextTick(); + + // New changes still trigger privileges re-registration. + licenseSubject.next(({} as unknown) as SecurityLicenseFeatures); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(2); + }); + + it('schedules retries if fails to register cluster privileges', async () => { + jest.useFakeTimers(); + + mockRegisterPrivilegesWithCluster.mockRejectedValue(new Error('Some error')); + + // Both ES and license are available. + mockLicense.isEnabled.mockReturnValue(true); + statusSubject.next({ + elasticsearch: { level: ServiceStatusLevels.available, summary: 'Service is working' }, + savedObjects: { level: ServiceStatusLevels.unavailable, summary: 'Service is NOT working' }, + }); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(1); + + // Next retry isn't performed immediately, retry happens only after a timeout. + await nextTick(); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(100); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(2); + + // Delay between consequent retries is increasing. + await nextTick(); + jest.advanceTimersByTime(100); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(2); + await nextTick(); + jest.advanceTimersByTime(100); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(3); + + // When call finally succeeds retries aren't scheduled anymore. + mockRegisterPrivilegesWithCluster.mockResolvedValue(undefined); + await nextTick(); + jest.runAllTimers(); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(4); + await nextTick(); + jest.runAllTimers(); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(4); + + // New changes still trigger privileges re-registration. + licenseSubject.next(({} as unknown) as SecurityLicenseFeatures); + expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(5); + }); +}); + +it('#stop unsubscribes from license and ES updates.', () => { + const mockClusterClient = elasticsearchServiceMock.createClusterClient(); + + const licenseSubject = new BehaviorSubject(({} as unknown) as SecurityLicenseFeatures); + const mockLicense = licenseMock.create(); + mockLicense.isEnabled.mockReturnValue(false); + mockLicense.features$ = licenseSubject; + + const mockCoreSetup = coreMock.createSetup(); + mockCoreSetup.status.core$ = new BehaviorSubject({ + elasticsearch: { level: ServiceStatusLevels.available, summary: 'Service is working' }, + savedObjects: { level: ServiceStatusLevels.available, summary: 'Service is working' }, + }); + + const authorizationService = new AuthorizationService(); + authorizationService.setup({ + http: mockCoreSetup.http, + capabilities: mockCoreSetup.capabilities, + status: mockCoreSetup.status, + clusterClient: mockClusterClient, + license: mockLicense, + loggers: loggingServiceMock.create(), + kibanaIndexName, + packageVersion: 'some-version', + features: featuresPluginMock.createSetup(), + getSpacesService: jest + .fn() + .mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }), + }); + + const featuresStart = featuresPluginMock.createStart(); + featuresStart.getFeatures.mockReturnValue([]); + authorizationService.start({ clusterClient: mockClusterClient, features: featuresStart }); + + authorizationService.stop(); + + // After stop we don't register privileges even if all requirements are met. + mockLicense.isEnabled.mockReturnValue(true); + licenseSubject.next(({} as unknown) as SecurityLicenseFeatures); + expect(mockRegisterPrivilegesWithCluster).not.toHaveBeenCalled(); +}); diff --git a/x-pack/plugins/security/server/authorization/authorization_service.ts b/x-pack/plugins/security/server/authorization/authorization_service.ts new file mode 100644 index 00000000000000..989784a1436d26 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/authorization_service.ts @@ -0,0 +1,221 @@ +/* + * 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 { combineLatest, BehaviorSubject, Subscription } from 'rxjs'; +import { distinctUntilChanged, filter } from 'rxjs/operators'; +import { UICapabilities } from 'ui/capabilities'; +import { + LoggerFactory, + KibanaRequest, + IClusterClient, + ServiceStatusLevels, + Logger, + StatusServiceSetup, + HttpServiceSetup, + CapabilitiesSetup, +} from '../../../../../src/core/server'; + +import { + PluginSetupContract as FeaturesPluginSetup, + PluginStartContract as FeaturesPluginStart, +} from '../../../features/server'; + +import { SpacesService } from '../plugin'; +import { Actions } from './actions'; +import { CheckPrivilegesWithRequest, checkPrivilegesWithRequestFactory } from './check_privileges'; +import { + CheckPrivilegesDynamicallyWithRequest, + checkPrivilegesDynamicallyWithRequestFactory, +} from './check_privileges_dynamically'; +import { + CheckSavedObjectsPrivilegesWithRequest, + checkSavedObjectsPrivilegesWithRequestFactory, +} from './check_saved_objects_privileges'; +import { AuthorizationMode, authorizationModeFactory } from './mode'; +import { privilegesFactory, PrivilegesService } from './privileges'; +import { initAppAuthorization } from './app_authorization'; +import { initAPIAuthorization } from './api_authorization'; +import { disableUICapabilitiesFactory } from './disable_ui_capabilities'; +import { validateFeaturePrivileges } from './validate_feature_privileges'; +import { validateReservedPrivileges } from './validate_reserved_privileges'; +import { registerPrivilegesWithCluster } from './register_privileges_with_cluster'; +import { APPLICATION_PREFIX } from '../../common/constants'; +import { SecurityLicense } from '../../common/licensing'; + +export { Actions } from './actions'; +export { CheckSavedObjectsPrivileges } from './check_saved_objects_privileges'; +export { featurePrivilegeIterator } from './privileges'; + +interface AuthorizationServiceSetupParams { + packageVersion: string; + http: HttpServiceSetup; + status: StatusServiceSetup; + capabilities: CapabilitiesSetup; + clusterClient: IClusterClient; + license: SecurityLicense; + loggers: LoggerFactory; + features: FeaturesPluginSetup; + kibanaIndexName: string; + getSpacesService(): SpacesService | undefined; +} + +interface AuthorizationServiceStartParams { + features: FeaturesPluginStart; + clusterClient: IClusterClient; +} + +export interface AuthorizationServiceSetup { + actions: Actions; + checkPrivilegesWithRequest: CheckPrivilegesWithRequest; + checkPrivilegesDynamicallyWithRequest: CheckPrivilegesDynamicallyWithRequest; + checkSavedObjectsPrivilegesWithRequest: CheckSavedObjectsPrivilegesWithRequest; + applicationName: string; + mode: AuthorizationMode; + privileges: PrivilegesService; +} + +export class AuthorizationService { + private logger!: Logger; + private license!: SecurityLicense; + private status!: StatusServiceSetup; + private applicationName!: string; + private privileges!: PrivilegesService; + + private statusSubscription?: Subscription; + + setup({ + http, + capabilities, + status, + packageVersion, + clusterClient, + license, + loggers, + features, + kibanaIndexName, + getSpacesService, + }: AuthorizationServiceSetupParams): AuthorizationServiceSetup { + this.logger = loggers.get('authorization'); + this.license = license; + this.status = status; + this.applicationName = `${APPLICATION_PREFIX}${kibanaIndexName}`; + + const mode = authorizationModeFactory(license); + const actions = new Actions(packageVersion); + this.privileges = privilegesFactory(actions, features, license); + + const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( + actions, + clusterClient, + this.applicationName + ); + + const authz = { + actions, + applicationName: this.applicationName, + mode, + privileges: this.privileges, + checkPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: checkPrivilegesDynamicallyWithRequestFactory( + checkPrivilegesWithRequest, + getSpacesService + ), + checkSavedObjectsPrivilegesWithRequest: checkSavedObjectsPrivilegesWithRequestFactory( + checkPrivilegesWithRequest, + getSpacesService + ), + }; + + capabilities.registerSwitcher( + async (request: KibanaRequest, uiCapabilities: UICapabilities) => { + // If we have a license which doesn't enable security, or we're a legacy user we shouldn't + // disable any ui capabilities + if (!mode.useRbacForRequest(request)) { + return uiCapabilities; + } + + const disableUICapabilities = disableUICapabilitiesFactory( + request, + features.getFeatures(), + this.logger, + authz + ); + + if (!request.auth.isAuthenticated) { + return disableUICapabilities.all(uiCapabilities); + } + + return await disableUICapabilities.usingPrivileges(uiCapabilities); + } + ); + + initAPIAuthorization(http, authz, loggers.get('api-authorization')); + initAppAuthorization(http, authz, loggers.get('app-authorization'), features); + + return authz; + } + + start({ clusterClient, features }: AuthorizationServiceStartParams) { + const allFeatures = features.getFeatures(); + validateFeaturePrivileges(allFeatures); + validateReservedPrivileges(allFeatures); + + this.registerPrivileges(clusterClient); + } + + stop() { + if (this.statusSubscription !== undefined) { + this.statusSubscription.unsubscribe(); + this.statusSubscription = undefined; + } + } + + private registerPrivileges(clusterClient: IClusterClient) { + const RETRY_SCALE_DURATION = 100; + const RETRY_TIMEOUT_MAX = 10000; + const retries$ = new BehaviorSubject(0); + let retryTimeout: NodeJS.Timeout; + + // Register cluster privileges once Elasticsearch is available and Security plugin is enabled. + this.statusSubscription = combineLatest([ + this.status.core$, + this.license.features$, + retries$.asObservable().pipe( + // We shouldn't emit new value if retry counter is reset. This comparator isn't called for + // the initial value. + distinctUntilChanged((prev, curr) => prev === curr || curr === 0) + ), + ]) + .pipe( + filter( + ([status]) => + this.license.isEnabled() && status.elasticsearch.level === ServiceStatusLevels.available + ) + ) + .subscribe(async () => { + // If status or license change occurred before retry timeout we should cancel it. + if (retryTimeout) { + clearTimeout(retryTimeout); + } + + try { + await registerPrivilegesWithCluster( + this.logger, + this.privileges, + this.applicationName, + clusterClient + ); + retries$.next(0); + } catch (err) { + const retriesElapsed = retries$.getValue() + 1; + retryTimeout = setTimeout( + () => retries$.next(retriesElapsed), + Math.min(retriesElapsed * RETRY_SCALE_DURATION, RETRY_TIMEOUT_MAX) + ); + } + }); + } +} diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts index 72937c15756ac9..183ad9169a1233 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts @@ -10,13 +10,13 @@ import { KibanaRequest, Logger } from '../../../../../src/core/server'; import { Feature } from '../../../features/server'; import { CheckPrivilegesResponse } from './check_privileges'; -import { Authorization } from './index'; +import { AuthorizationServiceSetup } from '.'; export function disableUICapabilitiesFactory( request: KibanaRequest, features: Feature[], logger: Logger, - authz: Authorization + authz: AuthorizationServiceSetup ) { const featureNavLinkIds = features .map((feature) => feature.navLinkId) diff --git a/x-pack/plugins/security/server/authorization/index.test.ts b/x-pack/plugins/security/server/authorization/index.test.ts deleted file mode 100644 index 32520534547649..00000000000000 --- a/x-pack/plugins/security/server/authorization/index.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - mockAuthorizationModeFactory, - mockCheckPrivilegesDynamicallyWithRequestFactory, - mockCheckPrivilegesWithRequestFactory, - mockCheckSavedObjectsPrivilegesWithRequestFactory, - mockPrivilegesFactory, -} from './service.test.mocks'; - -import { checkPrivilegesWithRequestFactory } from './check_privileges'; -import { checkPrivilegesDynamicallyWithRequestFactory } from './check_privileges_dynamically'; -import { checkSavedObjectsPrivilegesWithRequestFactory } from './check_saved_objects_privileges'; -import { authorizationModeFactory } from './mode'; -import { privilegesFactory } from './privileges'; -import { setupAuthorization } from '.'; - -import { - coreMock, - elasticsearchServiceMock, - loggingServiceMock, -} from '../../../../../src/core/server/mocks'; -import { licenseMock } from '../../common/licensing/index.mock'; - -test(`returns exposed services`, () => { - const kibanaIndexName = '.a-kibana-index'; - const application = `kibana-${kibanaIndexName}`; - - const mockCheckPrivilegesWithRequest = Symbol(); - mockCheckPrivilegesWithRequestFactory.mockReturnValue(mockCheckPrivilegesWithRequest); - - const mockCheckPrivilegesDynamicallyWithRequest = Symbol(); - mockCheckPrivilegesDynamicallyWithRequestFactory.mockReturnValue( - mockCheckPrivilegesDynamicallyWithRequest - ); - - const mockCheckSavedObjectsPrivilegesWithRequest = Symbol(); - mockCheckSavedObjectsPrivilegesWithRequestFactory.mockReturnValue( - mockCheckSavedObjectsPrivilegesWithRequest - ); - - const mockPrivilegesService = Symbol(); - mockPrivilegesFactory.mockReturnValue(mockPrivilegesService); - const mockAuthorizationMode = Symbol(); - mockAuthorizationModeFactory.mockReturnValue(mockAuthorizationMode); - - const mockClusterClient = elasticsearchServiceMock.createClusterClient(); - const mockGetSpacesService = jest - .fn() - .mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }); - const mockFeaturesService = { getFeatures: () => [] }; - const mockLicense = licenseMock.create(); - - const authz = setupAuthorization({ - http: coreMock.createSetup().http, - clusterClient: mockClusterClient, - license: mockLicense, - loggers: loggingServiceMock.create(), - kibanaIndexName, - packageVersion: 'some-version', - featuresService: mockFeaturesService, - getSpacesService: mockGetSpacesService, - }); - - expect(authz.actions.version).toBe('version:some-version'); - expect(authz.applicationName).toBe(application); - - expect(authz.checkPrivilegesWithRequest).toBe(mockCheckPrivilegesWithRequest); - expect(checkPrivilegesWithRequestFactory).toHaveBeenCalledWith( - authz.actions, - mockClusterClient, - authz.applicationName - ); - - expect(authz.checkPrivilegesDynamicallyWithRequest).toBe( - mockCheckPrivilegesDynamicallyWithRequest - ); - expect(checkPrivilegesDynamicallyWithRequestFactory).toHaveBeenCalledWith( - mockCheckPrivilegesWithRequest, - mockGetSpacesService - ); - - expect(authz.checkSavedObjectsPrivilegesWithRequest).toBe( - mockCheckSavedObjectsPrivilegesWithRequest - ); - expect(checkSavedObjectsPrivilegesWithRequestFactory).toHaveBeenCalledWith( - mockCheckPrivilegesWithRequest, - mockGetSpacesService - ); - - expect(authz.privileges).toBe(mockPrivilegesService); - expect(privilegesFactory).toHaveBeenCalledWith(authz.actions, mockFeaturesService, mockLicense); - - expect(authz.mode).toBe(mockAuthorizationMode); - expect(authorizationModeFactory).toHaveBeenCalledWith(mockLicense); -}); diff --git a/x-pack/plugins/security/server/authorization/index.ts b/x-pack/plugins/security/server/authorization/index.ts index cf970a561b93f9..d5c1323354f86e 100644 --- a/x-pack/plugins/security/server/authorization/index.ts +++ b/x-pack/plugins/security/server/authorization/index.ts @@ -4,134 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UICapabilities } from 'ui/capabilities'; -import { - CoreSetup, - LoggerFactory, - KibanaRequest, - IClusterClient, -} from '../../../../../src/core/server'; - -import { FeaturesService, SpacesService } from '../plugin'; -import { Actions } from './actions'; -import { CheckPrivilegesWithRequest, checkPrivilegesWithRequestFactory } from './check_privileges'; -import { - CheckPrivilegesDynamicallyWithRequest, - checkPrivilegesDynamicallyWithRequestFactory, -} from './check_privileges_dynamically'; -import { - CheckSavedObjectsPrivilegesWithRequest, - checkSavedObjectsPrivilegesWithRequestFactory, -} from './check_saved_objects_privileges'; -import { AuthorizationMode, authorizationModeFactory } from './mode'; -import { privilegesFactory, PrivilegesService } from './privileges'; -import { initAppAuthorization } from './app_authorization'; -import { initAPIAuthorization } from './api_authorization'; -import { disableUICapabilitiesFactory } from './disable_ui_capabilities'; -import { validateFeaturePrivileges } from './validate_feature_privileges'; -import { validateReservedPrivileges } from './validate_reserved_privileges'; -import { registerPrivilegesWithCluster } from './register_privileges_with_cluster'; -import { APPLICATION_PREFIX } from '../../common/constants'; -import { SecurityLicense } from '../../common/licensing'; - export { Actions } from './actions'; +export { AuthorizationService, AuthorizationServiceSetup } from './authorization_service'; export { CheckSavedObjectsPrivileges } from './check_saved_objects_privileges'; export { featurePrivilegeIterator } from './privileges'; - -interface SetupAuthorizationParams { - packageVersion: string; - http: CoreSetup['http']; - clusterClient: IClusterClient; - license: SecurityLicense; - loggers: LoggerFactory; - featuresService: FeaturesService; - kibanaIndexName: string; - getSpacesService(): SpacesService | undefined; -} - -export interface Authorization { - actions: Actions; - checkPrivilegesWithRequest: CheckPrivilegesWithRequest; - checkPrivilegesDynamicallyWithRequest: CheckPrivilegesDynamicallyWithRequest; - checkSavedObjectsPrivilegesWithRequest: CheckSavedObjectsPrivilegesWithRequest; - applicationName: string; - mode: AuthorizationMode; - privileges: PrivilegesService; - disableUnauthorizedCapabilities: ( - request: KibanaRequest, - capabilities: UICapabilities - ) => Promise; - registerPrivilegesWithCluster: () => Promise; -} - -export function setupAuthorization({ - http, - packageVersion, - clusterClient, - license, - loggers, - featuresService, - kibanaIndexName, - getSpacesService, -}: SetupAuthorizationParams): Authorization { - const actions = new Actions(packageVersion); - const mode = authorizationModeFactory(license); - const applicationName = `${APPLICATION_PREFIX}${kibanaIndexName}`; - const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( - actions, - clusterClient, - applicationName - ); - const privileges = privilegesFactory(actions, featuresService, license); - const logger = loggers.get('authorization'); - - const authz = { - actions, - applicationName, - checkPrivilegesWithRequest, - checkPrivilegesDynamicallyWithRequest: checkPrivilegesDynamicallyWithRequestFactory( - checkPrivilegesWithRequest, - getSpacesService - ), - checkSavedObjectsPrivilegesWithRequest: checkSavedObjectsPrivilegesWithRequestFactory( - checkPrivilegesWithRequest, - getSpacesService - ), - mode, - privileges, - - async disableUnauthorizedCapabilities(request: KibanaRequest, capabilities: UICapabilities) { - // If we have a license which doesn't enable security, or we're a legacy user we shouldn't - // disable any ui capabilities - if (!mode.useRbacForRequest(request)) { - return capabilities; - } - - const disableUICapabilities = disableUICapabilitiesFactory( - request, - featuresService.getFeatures(), - logger, - authz - ); - - if (!request.auth.isAuthenticated) { - return disableUICapabilities.all(capabilities); - } - - return await disableUICapabilities.usingPrivileges(capabilities); - }, - - registerPrivilegesWithCluster: async () => { - const features = featuresService.getFeatures(); - validateFeaturePrivileges(features); - validateReservedPrivileges(features); - - await registerPrivilegesWithCluster(logger, privileges, applicationName, clusterClient); - }, - }; - - initAPIAuthorization(http, authz, loggers.get('api-authorization')); - initAppAuthorization(http, authz, loggers.get('app-authorization'), featuresService); - - return authz; -} diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts index b023c12d35b79a..06f064a379fe6e 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts @@ -8,6 +8,8 @@ import { Feature } from '../../../../features/server'; import { Actions } from '../actions'; import { privilegesFactory } from './privileges'; +import { featuresPluginMock } from '../../../../features/server/mocks'; + const actions = new Actions('1.0.0-zeta1'); describe('features', () => { @@ -42,7 +44,9 @@ describe('features', () => { }), ]; - const mockFeaturesService = { getFeatures: jest.fn().mockReturnValue(features) }; + const mockFeaturesService = featuresPluginMock.createSetup(); + mockFeaturesService.getFeatures.mockReturnValue(features); + const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts index f3b2881e79ece5..5a15290a7f1a29 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts @@ -6,11 +6,10 @@ import { uniq } from 'lodash'; import { SecurityLicense } from '../../../common/licensing'; -import { Feature } from '../../../../features/server'; +import { Feature, PluginSetupContract as FeaturesPluginSetup } from '../../../../features/server'; import { RawKibanaPrivileges } from '../../../common/model'; import { Actions } from '../actions'; import { featurePrivilegeBuilderFactory } from './feature_privilege_builder'; -import { FeaturesService } from '../../plugin'; import { featurePrivilegeIterator, subFeaturePrivilegeIterator, @@ -22,7 +21,7 @@ export interface PrivilegesService { export function privilegesFactory( actions: Actions, - featuresService: FeaturesService, + featuresService: FeaturesPluginSetup, licenseService: Pick ) { const featurePrivilegeBuilder = featurePrivilegeBuilderFactory(actions); diff --git a/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.test.ts b/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.test.ts index fff4345c72409f..e21203e60b887e 100644 --- a/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.test.ts +++ b/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.test.ts @@ -49,7 +49,7 @@ const registerPrivilegesWithClusterTest = ( }); for (const deletedPrivilege of deletedPrivileges) { expect(mockLogger.debug).toHaveBeenCalledWith( - `Deleting Kibana Privilege ${deletedPrivilege} from Elasticearch for ${application}` + `Deleting Kibana Privilege ${deletedPrivilege} from Elasticsearch for ${application}` ); expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( 'shield.deletePrivilege', @@ -82,7 +82,7 @@ const registerPrivilegesWithClusterTest = ( `Registering Kibana Privileges with Elasticsearch for ${application}` ); expect(mockLogger.debug).toHaveBeenCalledWith( - `Kibana Privileges already registered with Elasticearch for ${application}` + `Kibana Privileges already registered with Elasticsearch for ${application}` ); }; }; diff --git a/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.ts b/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.ts index 22e7830d20e28e..8e54794494a908 100644 --- a/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.ts +++ b/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.ts @@ -61,14 +61,14 @@ export async function registerPrivilegesWithCluster( privilege: application, }); if (arePrivilegesEqual(existingPrivileges, expectedPrivileges)) { - logger.debug(`Kibana Privileges already registered with Elasticearch for ${application}`); + logger.debug(`Kibana Privileges already registered with Elasticsearch for ${application}`); return; } const privilegesToDelete = getPrivilegesToDelete(existingPrivileges, expectedPrivileges); for (const privilegeToDelete of privilegesToDelete) { logger.debug( - `Deleting Kibana Privilege ${privilegeToDelete} from Elasticearch for ${application}` + `Deleting Kibana Privilege ${privilegeToDelete} from Elasticsearch for ${application}` ); try { await clusterClient.callAsInternalUser('shield.deletePrivilege', { diff --git a/x-pack/plugins/security/server/authorization/service.test.mocks.ts b/x-pack/plugins/security/server/authorization/service.test.mocks.ts index 5cd2eac20094dd..d73adde66a4907 100644 --- a/x-pack/plugins/security/server/authorization/service.test.mocks.ts +++ b/x-pack/plugins/security/server/authorization/service.test.mocks.ts @@ -28,3 +28,8 @@ export const mockAuthorizationModeFactory = jest.fn(); jest.mock('./mode', () => ({ authorizationModeFactory: mockAuthorizationModeFactory, })); + +export const mockRegisterPrivilegesWithCluster = jest.fn(); +jest.mock('./register_privileges_with_cluster', () => ({ + registerPrivilegesWithCluster: mockRegisterPrivilegesWithCluster, +})); diff --git a/x-pack/plugins/security/server/feature_usage/feature_usage_service.test.ts b/x-pack/plugins/security/server/feature_usage/feature_usage_service.test.ts new file mode 100644 index 00000000000000..46796fa73ef26f --- /dev/null +++ b/x-pack/plugins/security/server/feature_usage/feature_usage_service.test.ts @@ -0,0 +1,42 @@ +/* + * 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 { SecurityFeatureUsageService } from './feature_usage_service'; + +describe('#setup', () => { + it('registers all known security features', () => { + const featureUsage = { register: jest.fn() }; + const securityFeatureUsage = new SecurityFeatureUsageService(); + securityFeatureUsage.setup({ featureUsage }); + expect(featureUsage.register).toHaveBeenCalledTimes(2); + expect(featureUsage.register.mock.calls.map((c) => c[0])).toMatchInlineSnapshot(` + Array [ + "Subfeature privileges", + "Pre-access agreement", + ] + `); + }); +}); + +describe('start contract', () => { + it('notifies when sub-feature privileges are in use', () => { + const featureUsage = { notifyUsage: jest.fn(), getLastUsages: jest.fn() }; + const securityFeatureUsage = new SecurityFeatureUsageService(); + const startContract = securityFeatureUsage.start({ featureUsage }); + startContract.recordSubFeaturePrivilegeUsage(); + expect(featureUsage.notifyUsage).toHaveBeenCalledTimes(1); + expect(featureUsage.notifyUsage).toHaveBeenCalledWith('Subfeature privileges'); + }); + + it('notifies when pre-access agreement is used', () => { + const featureUsage = { notifyUsage: jest.fn(), getLastUsages: jest.fn() }; + const securityFeatureUsage = new SecurityFeatureUsageService(); + const startContract = securityFeatureUsage.start({ featureUsage }); + startContract.recordPreAccessAgreementUsage(); + expect(featureUsage.notifyUsage).toHaveBeenCalledTimes(1); + expect(featureUsage.notifyUsage).toHaveBeenCalledWith('Pre-access agreement'); + }); +}); diff --git a/x-pack/plugins/security/server/feature_usage/feature_usage_service.ts b/x-pack/plugins/security/server/feature_usage/feature_usage_service.ts new file mode 100644 index 00000000000000..1bc1e664981bf6 --- /dev/null +++ b/x-pack/plugins/security/server/feature_usage/feature_usage_service.ts @@ -0,0 +1,38 @@ +/* + * 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 { FeatureUsageServiceSetup, FeatureUsageServiceStart } from '../../../licensing/server'; + +interface SetupDeps { + featureUsage: FeatureUsageServiceSetup; +} + +interface StartDeps { + featureUsage: FeatureUsageServiceStart; +} + +export interface SecurityFeatureUsageServiceStart { + recordPreAccessAgreementUsage: () => void; + recordSubFeaturePrivilegeUsage: () => void; +} + +export class SecurityFeatureUsageService { + public setup({ featureUsage }: SetupDeps) { + featureUsage.register('Subfeature privileges', 'gold'); + featureUsage.register('Pre-access agreement', 'gold'); + } + + public start({ featureUsage }: StartDeps): SecurityFeatureUsageServiceStart { + return { + recordPreAccessAgreementUsage() { + featureUsage.notifyUsage('Pre-access agreement'); + }, + recordSubFeaturePrivilegeUsage() { + featureUsage.notifyUsage('Subfeature privileges'); + }, + }; + } +} diff --git a/x-pack/plugins/security/server/feature_usage/index.mock.ts b/x-pack/plugins/security/server/feature_usage/index.mock.ts new file mode 100644 index 00000000000000..6ed42145abd766 --- /dev/null +++ b/x-pack/plugins/security/server/feature_usage/index.mock.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SecurityFeatureUsageServiceStart } from './feature_usage_service'; + +export const securityFeatureUsageServiceMock = { + createStartContract() { + return { + recordPreAccessAgreementUsage: jest.fn(), + recordSubFeaturePrivilegeUsage: jest.fn(), + } as jest.Mocked; + }, +}; diff --git a/x-pack/plugins/security/server/feature_usage/index.ts b/x-pack/plugins/security/server/feature_usage/index.ts new file mode 100644 index 00000000000000..a3e1f35ee3824e --- /dev/null +++ b/x-pack/plugins/security/server/feature_usage/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + SecurityFeatureUsageService, + SecurityFeatureUsageServiceStart, +} from './feature_usage_service'; diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index 72a946d6c51557..c2d99433b03466 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SecurityPluginSetup } from './plugin'; - import { authenticationMock } from './authentication/index.mock'; import { authorizationMock } from './authorization/index.mock'; import { licenseMock } from '../common/licensing/index.mock'; @@ -23,7 +21,6 @@ function createSetupMock() { }, registerSpacesService: jest.fn(), license: licenseMock.create(), - __legacyCompat: {} as SecurityPluginSetup['__legacyCompat'], }; } diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 3e30ff9447f3e0..e01c608e5f3065 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -41,16 +41,15 @@ describe('Security Plugin', () => { mockClusterClient = elasticsearchServiceMock.createCustomClusterClient(); mockCoreSetup.elasticsearch.legacy.createClient.mockReturnValue(mockClusterClient); - mockDependencies = { licensing: { license$: of({}) } } as PluginSetupDependencies; + mockDependencies = ({ + licensing: { license$: of({}), featureUsage: { register: jest.fn() } }, + } as unknown) as PluginSetupDependencies; }); describe('setup()', () => { it('exposes proper contract', async () => { await expect(plugin.setup(mockCoreSetup, mockDependencies)).resolves.toMatchInlineSnapshot(` Object { - "__legacyCompat": Object { - "registerPrivilegesWithCluster": [Function], - }, "audit": Object { "getLogger": [Function], }, diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index bdda0be9b15a76..c8f47aaae7b5d8 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -8,32 +8,35 @@ import { combineLatest } from 'rxjs'; import { first, map } from 'rxjs/operators'; import { TypeOf } from '@kbn/config-schema'; import { + deepFreeze, ICustomClusterClient, CoreSetup, + CoreStart, Logger, PluginInitializerContext, } from '../../../../src/core/server'; -import { deepFreeze } from '../../../../src/core/server'; import { SpacesPluginSetup } from '../../spaces/server'; -import { PluginSetupContract as FeaturesSetupContract } from '../../features/server'; -import { LicensingPluginSetup } from '../../licensing/server'; +import { + PluginSetupContract as FeaturesPluginSetup, + PluginStartContract as FeaturesPluginStart, +} from '../../features/server'; +import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; import { Authentication, setupAuthentication } from './authentication'; -import { Authorization, setupAuthorization } from './authorization'; +import { AuthorizationService, AuthorizationServiceSetup } from './authorization'; import { ConfigSchema, createConfig } from './config'; import { defineRoutes } from './routes'; import { SecurityLicenseService, SecurityLicense } from '../common/licensing'; import { setupSavedObjects } from './saved_objects'; import { AuditService, SecurityAuditLogger, AuditServiceSetup } from './audit'; import { elasticsearchClientPlugin } from './elasticsearch_client_plugin'; +import { SecurityFeatureUsageService, SecurityFeatureUsageServiceStart } from './feature_usage'; export type SpacesService = Pick< SpacesPluginSetup['spacesService'], 'getSpaceId' | 'namespaceToSpaceId' >; -export type FeaturesService = Pick; - /** * Describes public Security plugin contract returned at the `setup` stage. */ @@ -48,7 +51,7 @@ export interface SecurityPluginSetup { | 'grantAPIKeyAsInternalUser' | 'invalidateAPIKeyAsInternalUser' >; - authz: Pick; + authz: Pick; license: SecurityLicense; audit: Pick; @@ -61,17 +64,18 @@ export interface SecurityPluginSetup { * @param service Spaces service exposed by the Spaces plugin. */ registerSpacesService: (service: SpacesService) => void; - - __legacyCompat: { - registerPrivilegesWithCluster: () => void; - }; } export interface PluginSetupDependencies { - features: FeaturesService; + features: FeaturesPluginSetup; licensing: LicensingPluginSetup; } +export interface PluginStartDependencies { + features: FeaturesPluginStart; + licensing: LicensingPluginStart; +} + /** * Represents Security Plugin instance that will be managed by the Kibana plugin system. */ @@ -80,7 +84,18 @@ export class Plugin { private clusterClient?: ICustomClusterClient; private spacesService?: SpacesService | symbol = Symbol('not accessed'); private securityLicenseService?: SecurityLicenseService; + + private readonly featureUsageService = new SecurityFeatureUsageService(); + private featureUsageServiceStart?: SecurityFeatureUsageServiceStart; + private readonly getFeatureUsageService = () => { + if (!this.featureUsageServiceStart) { + throw new Error(`featureUsageServiceStart is not registered!`); + } + return this.featureUsageServiceStart; + }; + private readonly auditService = new AuditService(this.initializerContext.logger.get('audit')); + private readonly authorizationService = new AuthorizationService(); private readonly getSpacesService = () => { // Changing property value from Symbol to undefined denotes the fact that property was accessed. @@ -95,7 +110,10 @@ export class Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup(core: CoreSetup, { features, licensing }: PluginSetupDependencies) { + public async setup( + core: CoreSetup, + { features, licensing }: PluginSetupDependencies + ) { const [config, legacyConfig] = await combineLatest([ this.initializerContext.config.create>().pipe( map((rawConfig) => @@ -118,11 +136,14 @@ export class Plugin { license$: licensing.license$, }); + this.featureUsageService.setup({ featureUsage: licensing.featureUsage }); + const audit = this.auditService.setup({ license, config: config.audit }); const auditLogger = new SecurityAuditLogger(audit.getLogger()); const authc = await setupAuthentication({ auditLogger, + getFeatureUsageService: this.getFeatureUsageService, http: core.http, clusterClient: this.clusterClient, config, @@ -130,15 +151,17 @@ export class Plugin { loggers: this.initializerContext.logger, }); - const authz = await setupAuthorization({ + const authz = this.authorizationService.setup({ http: core.http, + capabilities: core.capabilities, + status: core.status, clusterClient: this.clusterClient, license, loggers: this.initializerContext.logger, kibanaIndexName: legacyConfig.kibana.index, packageVersion: this.initializerContext.env.packageInfo.version, getSpacesService: this.getSpacesService, - featuresService: features, + features, }); setupSavedObjects({ @@ -148,8 +171,6 @@ export class Plugin { getSpacesService: this.getSpacesService, }); - core.capabilities.registerSwitcher(authz.disableUnauthorizedCapabilities); - defineRoutes({ router: core.http.createRouter(), basePath: core.http.basePath, @@ -160,6 +181,11 @@ export class Plugin { authc, authz, license, + getFeatures: () => + core + .getStartServices() + .then(([, { features: featuresStart }]) => featuresStart.getFeatures()), + getFeatureUsageService: this.getFeatureUsageService, }); return deepFreeze({ @@ -192,15 +218,15 @@ export class Plugin { this.spacesService = service; }, - - __legacyCompat: { - registerPrivilegesWithCluster: async () => await authz.registerPrivilegesWithCluster(), - }, }); } - public start() { + public start(core: CoreStart, { features, licensing }: PluginStartDependencies) { this.logger.debug('Starting plugin'); + this.featureUsageServiceStart = this.featureUsageService.start({ + featureUsage: licensing.featureUsage, + }); + this.authorizationService.start({ features, clusterClient: this.clusterClient! }); } public stop() { @@ -216,7 +242,11 @@ export class Plugin { this.securityLicenseService = undefined; } + if (this.featureUsageServiceStart) { + this.featureUsageServiceStart = undefined; + } this.auditService.stop(); + this.authorizationService.stop(); } private wasSpacesServiceAccessed() { diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts index d7710bf669ce1e..bec60fa149bcf9 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts @@ -15,6 +15,8 @@ import { httpServerMock, } from '../../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../../index.mock'; +import { Feature } from '../../../../../features/server'; +import { securityFeatureUsageServiceMock } from '../../../feature_usage/index.mock'; const application = 'kibana-.kibana'; const privilegeMap = { @@ -47,7 +49,12 @@ interface TestOptions { licenseCheckResult?: LicenseCheck; apiResponses?: Array<() => Promise>; payload?: Record; - asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; + asserts: { + statusCode: number; + result?: Record; + apiArguments?: unknown[][]; + recordSubFeaturePrivilegeUsage?: boolean; + }; } const putRoleTest = ( @@ -71,6 +78,47 @@ const putRoleTest = ( mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); } + mockRouteDefinitionParams.getFeatureUsageService.mockReturnValue( + securityFeatureUsageServiceMock.createStartContract() + ); + + mockRouteDefinitionParams.getFeatures.mockResolvedValue([ + new Feature({ + id: 'feature_1', + name: 'feature 1', + app: [], + privileges: { + all: { + ui: [], + savedObject: { all: [], read: [] }, + }, + read: { + ui: [], + savedObject: { all: [], read: [] }, + }, + }, + subFeatures: [ + { + name: 'sub feature 1', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'sub_feature_privilege_1', + name: 'first sub-feature privilege', + includeIn: 'none', + ui: [], + savedObject: { all: [], read: [] }, + }, + ], + }, + ], + }, + ], + }), + ]); + definePutRolesRoutes(mockRouteDefinitionParams); const [[{ validate }, handler]] = mockRouteDefinitionParams.router.put.mock.calls; @@ -99,6 +147,16 @@ const putRoleTest = ( expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); + + if (asserts.recordSubFeaturePrivilegeUsage) { + expect( + mockRouteDefinitionParams.getFeatureUsageService().recordSubFeaturePrivilegeUsage + ).toHaveBeenCalledTimes(1); + } else { + expect( + mockRouteDefinitionParams.getFeatureUsageService().recordSubFeaturePrivilegeUsage + ).not.toHaveBeenCalled(); + } }); }; @@ -598,5 +656,131 @@ describe('PUT role', () => { result: undefined, }, }); + + putRoleTest(`notifies when sub-feature privileges are included`, { + name: 'foo-role', + payload: { + kibana: [ + { + spaces: ['*'], + feature: { + feature_1: ['sub_feature_privilege_1'], + }, + }, + ], + }, + apiResponses: [async () => ({}), async () => {}], + asserts: { + recordSubFeaturePrivilegeUsage: true, + apiArguments: [ + ['shield.getRole', { name: 'foo-role', ignore: [404] }], + [ + 'shield.putRole', + { + name: 'foo-role', + body: { + cluster: [], + indices: [], + run_as: [], + applications: [ + { + application: 'kibana-.kibana', + privileges: ['feature_feature_1.sub_feature_privilege_1'], + resources: ['*'], + }, + ], + metadata: undefined, + }, + }, + ], + ], + statusCode: 204, + result: undefined, + }, + }); + + putRoleTest(`does not record sub-feature privilege usage for unknown privileges`, { + name: 'foo-role', + payload: { + kibana: [ + { + spaces: ['*'], + feature: { + feature_1: ['unknown_sub_feature_privilege_1'], + }, + }, + ], + }, + apiResponses: [async () => ({}), async () => {}], + asserts: { + recordSubFeaturePrivilegeUsage: false, + apiArguments: [ + ['shield.getRole', { name: 'foo-role', ignore: [404] }], + [ + 'shield.putRole', + { + name: 'foo-role', + body: { + cluster: [], + indices: [], + run_as: [], + applications: [ + { + application: 'kibana-.kibana', + privileges: ['feature_feature_1.unknown_sub_feature_privilege_1'], + resources: ['*'], + }, + ], + metadata: undefined, + }, + }, + ], + ], + statusCode: 204, + result: undefined, + }, + }); + + putRoleTest(`does not record sub-feature privilege usage for unknown features`, { + name: 'foo-role', + payload: { + kibana: [ + { + spaces: ['*'], + feature: { + unknown_feature: ['sub_feature_privilege_1'], + }, + }, + ], + }, + apiResponses: [async () => ({}), async () => {}], + asserts: { + recordSubFeaturePrivilegeUsage: false, + apiArguments: [ + ['shield.getRole', { name: 'foo-role', ignore: [404] }], + [ + 'shield.putRole', + { + name: 'foo-role', + body: { + cluster: [], + indices: [], + run_as: [], + applications: [ + { + application: 'kibana-.kibana', + privileges: ['feature_unknown_feature.sub_feature_privilege_1'], + resources: ['*'], + }, + ], + metadata: undefined, + }, + }, + ], + ], + statusCode: 204, + result: undefined, + }, + }); }); }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.ts index 5db83375afa965..d83cf92bcaa0d4 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; +import { Feature } from '../../../../../features/common'; import { RouteDefinitionParams } from '../../index'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; import { wrapIntoCustomErrorResponse } from '../../../errors'; @@ -14,7 +15,37 @@ import { transformPutPayloadToElasticsearchRole, } from './model'; -export function definePutRolesRoutes({ router, authz, clusterClient }: RouteDefinitionParams) { +const roleGrantsSubFeaturePrivileges = ( + features: Feature[], + role: TypeOf> +) => { + if (!role.kibana) { + return false; + } + + const subFeaturePrivileges = new Map( + features.map((feature) => [ + feature.id, + feature.subFeatures.map((sf) => sf.privilegeGroups.map((pg) => pg.privileges)).flat(2), + ]) + ); + + const hasAnySubFeaturePrivileges = role.kibana.some((kibanaPrivilege) => + Object.entries(kibanaPrivilege.feature ?? {}).some(([featureId, privileges]) => { + return !!subFeaturePrivileges.get(featureId)?.some(({ id }) => privileges.includes(id)); + }) + ); + + return hasAnySubFeaturePrivileges; +}; + +export function definePutRolesRoutes({ + router, + authz, + clusterClient, + getFeatures, + getFeatureUsageService, +}: RouteDefinitionParams) { router.put( { path: '/api/security/role/{name}', @@ -46,9 +77,16 @@ export function definePutRolesRoutes({ router, authz, clusterClient }: RouteDefi rawRoles[name] ? rawRoles[name].applications : [] ); - await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.putRole', { name: request.params.name, body }); + const [features] = await Promise.all([ + getFeatures(), + clusterClient + .asScoped(request) + .callAsCurrentUser('shield.putRole', { name: request.params.name, body }), + ]); + + if (roleGrantsSubFeaturePrivileges(features, request.body)) { + getFeatureUsageService().recordSubFeaturePrivilegeUsage(); + } return response.noContent(); } catch (error) { diff --git a/x-pack/plugins/security/server/routes/index.mock.ts b/x-pack/plugins/security/server/routes/index.mock.ts index b0c74b98ee19bf..1a93d6701e257d 100644 --- a/x-pack/plugins/security/server/routes/index.mock.ts +++ b/x-pack/plugins/security/server/routes/index.mock.ts @@ -29,5 +29,7 @@ export const routeDefinitionParamsMock = { authz: authorizationMock.create(), license: licenseMock.create(), httpResources: httpResourcesMock.createRegistrar(), + getFeatures: jest.fn(), + getFeatureUsageService: jest.fn(), }), }; diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index e43072b95c9067..5721a2699d15ce 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Feature } from '../../../features/server'; import { CoreSetup, HttpResources, @@ -13,7 +14,7 @@ import { } from '../../../../../src/core/server'; import { SecurityLicense } from '../../common/licensing'; import { Authentication } from '../authentication'; -import { Authorization } from '../authorization'; +import { AuthorizationServiceSetup } from '../authorization'; import { ConfigType } from '../config'; import { defineAuthenticationRoutes } from './authentication'; @@ -23,6 +24,7 @@ import { defineIndicesRoutes } from './indices'; import { defineUsersRoutes } from './users'; import { defineRoleMappingRoutes } from './role_mapping'; import { defineViewRoutes } from './views'; +import { SecurityFeatureUsageServiceStart } from '../feature_usage'; /** * Describes parameters used to define HTTP routes. @@ -35,8 +37,10 @@ export interface RouteDefinitionParams { clusterClient: IClusterClient; config: ConfigType; authc: Authentication; - authz: Authorization; + authz: AuthorizationServiceSetup; license: SecurityLicense; + getFeatures: () => Promise; + getFeatureUsageService: () => SecurityFeatureUsageServiceStart; } export function defineRoutes(params: RouteDefinitionParams) { diff --git a/x-pack/plugins/security/server/saved_objects/index.ts b/x-pack/plugins/security/server/saved_objects/index.ts index 29fbe3af21b951..6acfd06a0309bd 100644 --- a/x-pack/plugins/security/server/saved_objects/index.ts +++ b/x-pack/plugins/security/server/saved_objects/index.ts @@ -11,13 +11,16 @@ import { SavedObjectsClient, } from '../../../../../src/core/server'; import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper'; -import { Authorization } from '../authorization'; +import { AuthorizationServiceSetup } from '../authorization'; import { SecurityAuditLogger } from '../audit'; import { SpacesService } from '../plugin'; interface SetupSavedObjectsParams { auditLogger: SecurityAuditLogger; - authz: Pick; + authz: Pick< + AuthorizationServiceSetup, + 'mode' | 'actions' | 'checkSavedObjectsPrivilegesWithRequest' + >; savedObjects: CoreSetup['savedObjects']; getSpacesService(): SpacesService | undefined; } diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index b5cd8f2dec0a3f..482794804685d9 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -8,6 +8,7 @@ export const APP_ID = 'securitySolution'; export const APP_NAME = 'Security'; export const APP_ICON = 'securityAnalyticsApp'; export const APP_PATH = `/app/security`; +export const ADD_DATA_PATH = `/app/home#/tutorial_directory/security`; export const DEFAULT_BYTES_FORMAT = 'format:bytes:defaultPattern'; export const DEFAULT_DATE_FORMAT = 'dateFormat'; export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz'; 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..b17a5aa28ac6a3 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, + }); + } } } @@ -834,7 +929,7 @@ export class EndpointDocGenerator { status: HostPolicyResponseActionStatus.success, }, { - name: 'load_malware_mode', + name: 'load_malware_model', message: 'Error deserializing EXE model; no valid malware model installed', status: HostPolicyResponseActionStatus.success, }, 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/alerts/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.test.tsx index e39ee38d71da7a..b82d1c0a36ab28 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.test.tsx @@ -9,7 +9,11 @@ import { shallow } from 'enzyme'; import { EuiLoadingSpinner } from '@elastic/eui'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; -import { esFilters, FilterManager } from '../../../../../../../../src/plugins/data/public'; +import { + esFilters, + FilterManager, + UI_SETTINGS, +} from '../../../../../../../../src/plugins/data/public'; import { SeverityBadge } from '../severity_badge'; import * as i18n from './translations'; @@ -29,7 +33,7 @@ import { ListItems } from './types'; const setupMock = coreMock.createSetup(); const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { switch (key) { - case 'filters:pinnedByDefault': + case UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT: return pinnedByDefault; default: throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.test.tsx index 1f474630fd6e3a..b8f81f6d7e5f7a 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.test.tsx @@ -13,7 +13,12 @@ import { getDescriptionItem, } from '.'; -import { esFilters, Filter, FilterManager } from '../../../../../../../../src/plugins/data/public'; +import { + esFilters, + Filter, + FilterManager, + UI_SETTINGS, +} from '../../../../../../../../src/plugins/data/public'; import { mockAboutStepRule, mockDefineStepRule, @@ -33,7 +38,7 @@ describe('description_step', () => { const setupMock = coreMock.createSetup(); const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { switch (key) { - case 'filters:pinnedByDefault': + case UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT: return pinnedByDefault; default: throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.tsx index ad71059984a8b0..778c6bd92bc731 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.tsx @@ -162,7 +162,6 @@ const StepRuleActionsComponent: FC = ({ {myStepData.throttle !== stepActionsDefaultValue.throttle ? ( <> - = ({ messageVariables: actionMessageParams, }} /> - ) : ( = ({ component={GhostFormField} /> )} + diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx index 9632ddfeadc0d2..0c58f5620964ba 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx @@ -9,12 +9,13 @@ import React from 'react'; import { useKibana } from '../../../common/lib/kibana'; import { EmptyPage } from '../../../common/components/empty_page'; import * as i18n from '../../../common/translations'; +import { ADD_DATA_PATH } from '../../../../common/constants'; export const DetectionEngineEmptyPage = React.memo(() => ( ( ({ caseId, caseData, fetchCase, updateCase, userCanCrud }) => { const basePath = window.location.origin + useBasePath(); - const caseLink = `${basePath}/app/siem#/case/${caseId}`; + const caseLink = `${basePath}/app/security#/case/${caseId}`; const search = useGetUrlSearch(navTabs.case); const [initLoadingData, setInitLoadingData] = useState(true); const { diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/translations.ts b/x-pack/plugins/security_solution/public/cases/components/case_view/translations.ts index 9523e2485a6107..b8219ad52f5b0a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/translations.ts @@ -143,7 +143,7 @@ export const CASE_REFRESH = i18n.translate('xpack.securitySolution.case.caseView export const EMAIL_SUBJECT = (caseTitle: string) => i18n.translate('xpack.securitySolution.case.caseView.emailSubject', { values: { caseTitle }, - defaultMessage: 'SIEM Case - {caseTitle}', + defaultMessage: 'Security Case - {caseTitle}', }); export const EMAIL_BODY = (caseUrl: string) => diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.test.tsx index 43d5351a5dce3c..67963c7487826a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.test.tsx @@ -52,7 +52,7 @@ describe('FieldMappingRow', () => { test('it pass the corrects props to mapping row', () => { const rows = wrapper.find(FieldMappingRow); rows.forEach((row, index) => { - expect(row.prop('siemField')).toEqual(mapping[index].source); + expect(row.prop('securitySolutionField')).toEqual(mapping[index].source); expect(row.prop('selectedActionType')).toEqual(mapping[index].actionType); expect(row.prop('selectedThirdParty')).toEqual(mapping[index].target); }); @@ -68,7 +68,7 @@ describe('FieldMappingRow', () => { const rows = newWrapper.find(FieldMappingRow); rows.forEach((row, index) => { - expect(row.prop('siemField')).toEqual(defaultMapping[index].source); + expect(row.prop('securitySolutionField')).toEqual(defaultMapping[index].source); expect(row.prop('selectedActionType')).toEqual(defaultMapping[index].actionType); expect(row.prop('selectedThirdParty')).toEqual(defaultMapping[index].target); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.tsx index 3d515941fc2f33..415faa96eeeddc 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.tsx @@ -132,7 +132,7 @@ const FieldMappingComponent: React.FC = ({ key={`${item.source}`} id={`${item.source}`} disabled={disabled} - siemField={item.source} + securitySolutionField={item.source} thirdPartyOptions={getThirdPartyOptions(item.source, selectedConnector.fields)} actionTypeOptions={actionTypeOptions} onChangeActionType={onChangeActionType} diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.test.tsx index 3787a43ff2d28a..a2acd0e20b6adc 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.test.tsx @@ -51,7 +51,7 @@ describe('FieldMappingRow', () => { const props: RowProps = { id: 'title', disabled: false, - siemField: 'title', + securitySolutionField: 'title', thirdPartyOptions, actionTypeOptions, onChangeActionType, diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.tsx index 922ea7222efce3..6e688b213f82c0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.tsx @@ -20,7 +20,7 @@ import { AllThirdPartyFields } from '../../../common/lib/connectors/types'; export interface RowProps { id: string; disabled: boolean; - siemField: CaseField; + securitySolutionField: CaseField; thirdPartyOptions: Array>; actionTypeOptions: Array>; onChangeActionType: (caseField: CaseField, newActionType: ActionType) => void; @@ -32,7 +32,7 @@ export interface RowProps { const FieldMappingRowComponent: React.FC = ({ id, disabled, - siemField, + securitySolutionField, thirdPartyOptions, actionTypeOptions, onChangeActionType, @@ -40,13 +40,15 @@ const FieldMappingRowComponent: React.FC = ({ selectedActionType, selectedThirdParty, }) => { - const siemFieldCapitalized = useMemo(() => capitalize(siemField), [siemField]); + const securitySolutionFieldCapitalized = useMemo(() => capitalize(securitySolutionField), [ + securitySolutionField, + ]); return ( - {siemFieldCapitalized} + {securitySolutionFieldCapitalized} @@ -58,7 +60,7 @@ const FieldMappingRowComponent: React.FC = ({ disabled={disabled} options={thirdPartyOptions} valueOfSelected={selectedThirdParty} - onChange={onChangeThirdParty.bind(null, siemField)} + onChange={onChangeThirdParty.bind(null, securitySolutionField)} data-test-subj={`case-configure-third-party-select-${id}`} /> @@ -67,7 +69,7 @@ const FieldMappingRowComponent: React.FC = ({ disabled={disabled} options={actionTypeOptions} valueOfSelected={selectedActionType} - onChange={onChangeActionType.bind(null, siemField)} + onChange={onChangeActionType.bind(null, securitySolutionField)} data-test-subj={`case-configure-action-type-select-${id}`} /> diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/translations.ts b/x-pack/plugins/security_solution/public/cases/components/configure_cases/translations.ts index c256c6dedb9189..9ef6ce2f3d4a93 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/translations.ts @@ -17,7 +17,7 @@ export const INCIDENT_MANAGEMENT_SYSTEM_DESC = i18n.translate( 'xpack.securitySolution.case.configureCases.incidentManagementSystemDesc', { defaultMessage: - 'You may optionally connect SIEM cases to an external incident management system of your choosing. This will allow you to push case data as an incident in your chosen third-party system.', + 'You may optionally connect Security cases to an external incident management system of your choosing. This will allow you to push case data as an incident in your chosen third-party system.', } ); @@ -53,7 +53,7 @@ export const CASE_CLOSURE_OPTIONS_DESC = i18n.translate( 'xpack.securitySolution.case.configureCases.caseClosureOptionsDesc', { defaultMessage: - 'Define how you wish SIEM cases to be closed. Automated case closures require an established connection to an external incident management system.', + 'Define how you wish Security cases to be closed. Automated case closures require an established connection to an external incident management system.', } ); @@ -67,21 +67,22 @@ export const CASE_CLOSURE_OPTIONS_LABEL = i18n.translate( export const CASE_CLOSURE_OPTIONS_MANUAL = i18n.translate( 'xpack.securitySolution.case.configureCases.caseClosureOptionsManual', { - defaultMessage: 'Manually close SIEM cases', + defaultMessage: 'Manually close Security cases', } ); export const CASE_CLOSURE_OPTIONS_NEW_INCIDENT = i18n.translate( 'xpack.securitySolution.case.configureCases.caseClosureOptionsNewIncident', { - defaultMessage: 'Automatically close SIEM cases when pushing new incident to external system', + defaultMessage: + 'Automatically close Security cases when pushing new incident to external system', } ); export const CASE_CLOSURE_OPTIONS_CLOSED_INCIDENT = i18n.translate( 'xpack.securitySolution.case.configureCases.caseClosureOptionsClosedIncident', { - defaultMessage: 'Automatically close SIEM cases when incident is closed in external system', + defaultMessage: 'Automatically close Security cases when incident is closed in external system', } ); @@ -96,14 +97,14 @@ export const FIELD_MAPPING_DESC = i18n.translate( 'xpack.securitySolution.case.configureCases.fieldMappingDesc', { defaultMessage: - 'Map SIEM case fields when pushing data to a third-party. Field mappings require an established connection to an external incident management system.', + 'Map Security case fields when pushing data to a third-party. Field mappings require an established connection to an external incident management system.', } ); export const FIELD_MAPPING_FIRST_COL = i18n.translate( 'xpack.securitySolution.case.configureCases.fieldMappingFirstCol', { - defaultMessage: 'SIEM case field', + defaultMessage: 'Security case field', } ); diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx index 8e07706d9c6ba1..ae9f1ec7469e41 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx @@ -18,7 +18,7 @@ const onSaveContent = jest.fn(); const timelineId = '1e10f150-949b-11ea-b63c-2bc51864784c'; const defaultProps = { - content: `A link to a timeline [timeline](http://localhost:5601/app/siem#/timelines?timeline=(id:'${timelineId}',isOpen:!t))`, + content: `A link to a timeline [timeline](http://localhost:5601/app/security#/timelines?timeline=(id:'${timelineId}',isOpen:!t))`, id: 'markdown-id', isEditable: false, onChangeEditable, diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/page_view.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/page_view.tsx index 759274e3a4ffa2..6fe15310fc88e7 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/page_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/page_view.tsx @@ -149,7 +149,9 @@ export const PageView = memo( )} )} - {tabs && {tabComponents}} + {tabComponents.length > 0 && ( + {tabComponents} + )} {bodyHeader && ( diff --git a/x-pack/plugins/security_solution/public/common/components/formatted_date/index.tsx b/x-pack/plugins/security_solution/public/common/components/formatted_date/index.tsx index ac4a3533853f73..936bd26adff7f8 100644 --- a/x-pack/plugins/security_solution/public/common/components/formatted_date/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/formatted_date/index.tsx @@ -75,14 +75,15 @@ PreferenceFormattedP1DTDate.displayName = 'PreferenceFormattedP1DTDate'; export const FormattedDate = React.memo<{ fieldName: string; value?: string | number | null; + className?: string; }>( - ({ value, fieldName }): JSX.Element => { + ({ value, fieldName, className = '' }): JSX.Element => { if (value == null) { return getOrEmptyTagFromValue(value); } const maybeDate = getMaybeDate(value); return maybeDate.isValid() ? ( - + ) : ( diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx index 06a69d0612ee8c..c9085b5953817e 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx @@ -18,6 +18,7 @@ import { MlPopover } from '../ml_popover/ml_popover'; import { SiemNavigation } from '../navigation'; import * as i18n from './translations'; import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; +import { ADD_DATA_PATH } from '../../../../common/constants'; const Wrapper = styled.header` ${({ theme }) => css` @@ -86,7 +87,7 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine {i18n.BUTTON_ADD_DATA} diff --git a/x-pack/plugins/security_solution/public/common/components/localized_date_tooltip/index.tsx b/x-pack/plugins/security_solution/public/common/components/localized_date_tooltip/index.tsx index 918ec70bd7407f..d8f8742cb43640 100644 --- a/x-pack/plugins/security_solution/public/common/components/localized_date_tooltip/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/localized_date_tooltip/index.tsx @@ -13,9 +13,11 @@ export const LocalizedDateTooltip = React.memo<{ children: React.ReactNode; date: Date; fieldName?: string; -}>(({ children, date, fieldName }) => ( + className?: string; +}>(({ children, date, fieldName, className = '' }) => ( {fieldName != null ? ( diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx index 3c0189625ee291..d930136b3c0c46 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx @@ -18,7 +18,7 @@ import { createStore, State, substateMiddlewareFactory } from '../../store'; import { alertMiddlewareFactory } from '../../../endpoint_alerts/store/middleware'; import { AppRootProvider } from './app_root_provider'; import { managementMiddlewareFactory } from '../../../management/store/middleware'; -import { hostMiddlewareFactory } from '../../../endpoint_hosts/store/middleware'; +import { createKibanaContextProviderMock } from '../kibana_react'; import { SUB_PLUGINS_REDUCER, mockGlobalState } from '..'; type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; @@ -57,10 +57,6 @@ export const createAppRootMockRenderer = (): AppContextTestRender => { const depsStart = depsStartMock(); const middlewareSpy = createSpyMiddleware(); const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, apolloClientObservable, [ - substateMiddlewareFactory( - (globalState) => globalState.hostList, - hostMiddlewareFactory(coreStart, depsStart) - ), substateMiddlewareFactory( (globalState) => globalState.alertList, alertMiddlewareFactory(coreStart, depsStart) @@ -68,11 +64,14 @@ export const createAppRootMockRenderer = (): AppContextTestRender => { ...managementMiddlewareFactory(coreStart, depsStart), middlewareSpy.actionSpyMiddleware, ]); + const MockKibanaContextProvider = createKibanaContextProviderMock(); const AppWrapper: React.FC<{ children: React.ReactElement }> = ({ children }) => ( - - {children} - + + + {children} + + ); const render: UiRender = (ui, options) => { return reactRender(ui, { diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 30dffa8dbf6bfd..4af39ade70d255 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -26,10 +26,8 @@ import { import { networkModel } from '../../network/store'; import { TimelineType, TimelineStatus } from '../../../common/types/timeline'; import { initialAlertListState } from '../../endpoint_alerts/store/reducer'; -import { initialHostListState } from '../../endpoint_hosts/store/reducer'; import { mockManagementState } from '../../management/store/reducer'; import { AlertListState } from '../../../common/endpoint_alerts/types'; -import { HostState } from '../../endpoint_hosts/types'; import { ManagementState } from '../../management/types'; export const mockGlobalState: State = { @@ -237,6 +235,5 @@ export const mockGlobalState: State = { * they are cast to mutable versions here. */ alertList: initialAlertListState as AlertListState, - hostList: initialHostListState as HostState, management: mockManagementState as ManagementState, }; diff --git a/x-pack/plugins/security_solution/public/common/mock/utils.ts b/x-pack/plugins/security_solution/public/common/mock/utils.ts index 1ff5cb8e734ec3..c71a9ada75eee5 100644 --- a/x-pack/plugins/security_solution/public/common/mock/utils.ts +++ b/x-pack/plugins/security_solution/public/common/mock/utils.ts @@ -11,9 +11,7 @@ import { managementReducer } from '../../management/store/reducer'; import { ManagementPluginReducer } from '../../management'; import { SubPluginsInitReducer } from '../store'; import { EndpointAlertsPluginReducer } from '../../endpoint_alerts'; -import { EndpointHostsPluginReducer } from '../../endpoint_hosts'; import { alertListReducer } from '../../endpoint_alerts/store/reducer'; -import { hostListReducer } from '../../endpoint_hosts/store/reducer'; interface Global extends NodeJS.Global { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -30,7 +28,6 @@ export const SUB_PLUGINS_REDUCER: SubPluginsInitReducer = { * These state's are wrapped in `Immutable`, but for compatibility with the overall app architecture, * they are cast to mutable versions here. */ - hostList: hostListReducer as EndpointHostsPluginReducer['hostList'], alertList: alertListReducer as EndpointAlertsPluginReducer['alertList'], management: managementReducer as ManagementPluginReducer['management'], }; diff --git a/x-pack/plugins/security_solution/public/common/store/actions.ts b/x-pack/plugins/security_solution/public/common/store/actions.ts index 58e4e2f363e92c..453191ebafce6c 100644 --- a/x-pack/plugins/security_solution/public/common/store/actions.ts +++ b/x-pack/plugins/security_solution/public/common/store/actions.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostAction } from '../../endpoint_hosts/store/action'; +import { HostAction } from '../../management/pages/endpoint_hosts/store/action'; import { AlertAction } from '../../endpoint_alerts/store/action'; import { PolicyListAction } from '../../management/pages/policy/store/policy_list'; import { PolicyDetailsAction } from '../../management/pages/policy/store/policy_details'; diff --git a/x-pack/plugins/security_solution/public/common/store/reducer.ts b/x-pack/plugins/security_solution/public/common/store/reducer.ts index ba85fbef860d7a..6aa9c6c0593666 100644 --- a/x-pack/plugins/security_solution/public/common/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/reducer.ts @@ -17,7 +17,6 @@ import { TimelinePluginReducer } from '../../timelines/store/timeline'; import { SecuritySubPlugins } from '../../app/types'; import { ManagementPluginReducer } from '../../management'; import { EndpointAlertsPluginReducer } from '../../endpoint_alerts'; -import { EndpointHostsPluginReducer } from '../../endpoint_hosts'; import { State } from './types'; import { AppAction } from './actions'; @@ -25,7 +24,6 @@ export type SubPluginsInitReducer = HostsPluginReducer & NetworkPluginReducer & TimelinePluginReducer & EndpointAlertsPluginReducer & - EndpointHostsPluginReducer & ManagementPluginReducer; /** diff --git a/x-pack/plugins/security_solution/public/common/store/types.ts b/x-pack/plugins/security_solution/public/common/store/types.ts index b9942979beb1e8..2b92451e30119b 100644 --- a/x-pack/plugins/security_solution/public/common/store/types.ts +++ b/x-pack/plugins/security_solution/public/common/store/types.ts @@ -17,7 +17,6 @@ import { DragAndDropState } from './drag_and_drop/reducer'; import { TimelinePluginState } from '../../timelines/store/timeline'; import { NetworkPluginState } from '../../network/store'; import { EndpointAlertsPluginState } from '../../endpoint_alerts'; -import { EndpointHostsPluginState } from '../../endpoint_hosts'; import { ManagementPluginState } from '../../management'; /** @@ -31,7 +30,6 @@ export type State = CombinedState< NetworkPluginState & TimelinePluginState & EndpointAlertsPluginState & - EndpointHostsPluginState & ManagementPluginState & { app: AppState; dragAndDrop: DragAndDropState; diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/index.ts b/x-pack/plugins/security_solution/public/endpoint_hosts/index.ts deleted file mode 100644 index bd1c5f96f8cdaa..00000000000000 --- a/x-pack/plugins/security_solution/public/endpoint_hosts/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Reducer } from 'redux'; -import { SecuritySubPluginWithStore } from '../app/types'; -import { endpointHostsRoutes } from './routes'; -import { hostListReducer } from './store/reducer'; -import { HostState } from './types'; -import { hostMiddlewareFactory } from './store/middleware'; -import { CoreStart } from '../../../../../src/core/public'; -import { StartPlugins } from '../types'; -import { substateMiddlewareFactory } from '../common/store'; -import { AppAction } from '../common/store/actions'; - -/** - * Internally, our state is sometimes immutable, ignore that in our external - * interface. - */ -export interface EndpointHostsPluginState { - hostList: HostState; -} - -/** - * Internally, we use `ImmutableReducer`, but we present a regular reducer - * externally for compatibility w/ regular redux. - */ -export interface EndpointHostsPluginReducer { - hostList: Reducer; -} - -export class EndpointHosts { - public setup() {} - - public start( - core: CoreStart, - plugins: StartPlugins - ): SecuritySubPluginWithStore<'hostList', HostState> { - const { data, ingestManager } = plugins; - const middleware = [ - substateMiddlewareFactory( - (globalState) => globalState.hostList, - hostMiddlewareFactory(core, { data, ingestManager }) - ), - ]; - return { - routes: endpointHostsRoutes(), - store: { - initialState: { hostList: undefined }, - /** - * Cast the ImmutableReducer to a regular reducer for compatibility with - * the subplugin architecture (which expects plain redux reducers.) - */ - reducer: { hostList: hostListReducer } as EndpointHostsPluginReducer, - middleware, - }, - }; - } -} diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts_empty_page.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts_empty_page.tsx index 3ab0cb1f748d68..a01e249561e5c5 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts_empty_page.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts_empty_page.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { EmptyPage } from '../../common/components/empty_page'; import { useKibana } from '../../common/lib/kibana'; import * as i18n from '../../common/translations'; +import { ADD_DATA_PATH } from '../../../common/constants'; export const HostsEmptyPage = React.memo(() => { const { http, docLinks } = useKibana().services; @@ -18,7 +19,7 @@ export const HostsEmptyPage = React.memo(() => { = Exclude extends never ? T1 : never; +type Exact = T extends Shape ? ExactKeys : never; + +/** + * Returns a string to be used in the URL as search query params. + * Ensures that when creating a URL query param string, that the given input strictly + * matches the expected interface (guards against possibly leaking internal state) + */ +const querystringStringify: ( + params: Exact +) => string = querystring.stringify; + +/** Make `selected_host` required */ +type EndpointDetailsUrlProps = Omit & + Required>; + +/** + * Input props for the `getManagementUrl()` method + */ export type GetManagementUrlProps = { /** * Exclude the URL prefix (everything to the left of where the router was mounted. @@ -22,8 +45,8 @@ export type GetManagementUrlProps = { */ excludePrefix?: boolean; } & ( - | { name: 'default' } - | { name: 'endpointList' } + | ({ name: 'default' | 'endpointList' } & HostIndexUIQueryParams) + | ({ name: 'endpointDetails' | 'endpointPolicyResponse' } & EndpointDetailsUrlProps) | { name: 'policyList' } | { name: 'policyDetails'; policyId: string } ); @@ -39,31 +62,47 @@ const URL_PREFIX = '#'; export const getManagementUrl = (props: GetManagementUrlProps): string => { let url = props.excludePrefix ? '' : URL_PREFIX; - switch (props.name) { - case 'default': - url += generatePath(MANAGEMENT_ROUTING_ROOT_PATH, { - pageName: SiemPageName.management, - }); - break; - case 'endpointList': + if (props.name === 'default' || props.name === 'endpointList') { + const { name, excludePrefix, ...queryParams } = props; + const urlQueryParams = querystringStringify( + queryParams + ); + + if (name === 'endpointList') { url += generatePath(MANAGEMENT_ROUTING_ENDPOINTS_PATH, { pageName: SiemPageName.management, tabName: ManagementSubTab.endpoints, }); - break; - case 'policyList': - url += generatePath(MANAGEMENT_ROUTING_POLICIES_PATH, { - pageName: SiemPageName.management, - tabName: ManagementSubTab.policies, - }); - break; - case 'policyDetails': - url += generatePath(MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, { + } else { + url += generatePath(MANAGEMENT_ROUTING_ROOT_PATH, { pageName: SiemPageName.management, - tabName: ManagementSubTab.policies, - policyId: props.policyId, }); - break; + } + + if (urlQueryParams) { + url += `?${urlQueryParams}`; + } + } else if (props.name === 'endpointDetails' || props.name === 'endpointPolicyResponse') { + const { name, excludePrefix, ...queryParams } = props; + queryParams.show = (props.name === 'endpointPolicyResponse' + ? 'policy_response' + : '') as HostIndexUIQueryParams['show']; + + url += `${generatePath(MANAGEMENT_ROUTING_ENDPOINTS_PATH, { + pageName: SiemPageName.management, + tabName: ManagementSubTab.endpoints, + })}?${querystringStringify(queryParams)}`; + } else if (props.name === 'policyList') { + url += generatePath(MANAGEMENT_ROUTING_POLICIES_PATH, { + pageName: SiemPageName.management, + tabName: ManagementSubTab.policies, + }); + } else if (props.name === 'policyDetails') { + url += generatePath(MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, { + pageName: SiemPageName.management, + tabName: ManagementSubTab.policies, + policyId: props.policyId, + }); } return url; diff --git a/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx b/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx index c6570da5cb5aab..5b140a53a363bf 100644 --- a/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx +++ b/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx @@ -13,7 +13,10 @@ import { getManagementUrl } from '..'; export const ManagementPageView = memo>((options) => { const { tabName } = useParams<{ tabName: ManagementSubTab }>(); - const tabs = useMemo((): PageViewProps['tabs'] => { + const tabs = useMemo((): PageViewProps['tabs'] | undefined => { + if (options.viewType === 'details') { + return undefined; + } return [ { name: i18n.translate('xpack.securitySolution.managementTabs.endpoints', { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx new file mode 100644 index 00000000000000..ff7f522b9bc528 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx @@ -0,0 +1,25 @@ +/* + * 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 { Switch, Route } from 'react-router-dom'; +import React, { memo } from 'react'; +import { HostList } from './view'; +import { MANAGEMENT_ROUTING_ENDPOINTS_PATH } from '../../common/constants'; +import { NotFoundPage } from '../../../app/404'; + +/** + * Provides the routing container for the endpoints related views + */ +export const EndpointsContainer = memo(() => { + return ( + + + + + ); +}); + +EndpointsContainer.displayName = 'EndpointsContainer'; diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/routes.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/routes.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/endpoint_hosts/routes.tsx rename to x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/routes.tsx diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts similarity index 87% rename from x-pack/plugins/security_solution/public/endpoint_hosts/store/action.ts rename to x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts index 9b38d7ce5a23ae..62a2d9e3205c26 100644 --- a/x-pack/plugins/security_solution/public/endpoint_hosts/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts @@ -4,8 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostResultList, HostInfo, GetHostPolicyResponse } from '../../../common/endpoint/types'; -import { ServerApiError } from '../../common/types'; +import { + HostResultList, + HostInfo, + GetHostPolicyResponse, +} from '../../../../../common/endpoint/types'; +import { ServerApiError } from '../../../../common/types'; interface ServerReturnedHostList { type: 'serverReturnedHostList'; diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/store/host_pagination.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts similarity index 89% rename from x-pack/plugins/security_solution/public/endpoint_hosts/store/host_pagination.test.ts rename to x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts index 17feacb0a767a1..b8eaa39c777529 100644 --- a/x-pack/plugins/security_solution/public/endpoint_hosts/store/host_pagination.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts @@ -8,10 +8,10 @@ import { CoreStart, HttpSetup } from 'kibana/public'; import { History, createBrowserHistory } from 'history'; import { applyMiddleware, Store, createStore } from 'redux'; -import { coreMock } from '../../../../../../src/core/public/mocks'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; -import { HostResultList, AppLocation } from '../../../common/endpoint/types'; -import { DepsStartMock, depsStartMock } from '../../common/mock/endpoint'; +import { HostResultList, AppLocation } from '../../../../../common/endpoint/types'; +import { DepsStartMock, depsStartMock } from '../../../../common/mock/endpoint'; import { hostMiddlewareFactory } from './middleware'; @@ -20,8 +20,11 @@ import { hostListReducer } from './reducer'; import { uiQueryParams } from './selectors'; import { mockHostResultList } from './mock_host_result_list'; import { HostState, HostIndexUIQueryParams } from '../types'; -import { MiddlewareActionSpyHelper, createSpyMiddleware } from '../../common/store/test_utils'; -import { urlFromQueryParams } from '../view/url_from_query_params'; +import { + MiddlewareActionSpyHelper, + createSpyMiddleware, +} from '../../../../common/store/test_utils'; +import { getManagementUrl } from '../../..'; describe('host list pagination: ', () => { let fakeCoreStart: jest.Mocked; @@ -53,7 +56,9 @@ describe('host list pagination: ', () => { queryParams = () => uiQueryParams(store.getState()); historyPush = (nextQueryParams: HostIndexUIQueryParams): void => { - return history.push(urlFromQueryParams(nextQueryParams)); + return history.push( + getManagementUrl({ name: 'endpointList', excludePrefix: true, ...nextQueryParams }) + ); }; }); @@ -67,7 +72,7 @@ describe('host list pagination: ', () => { type: 'userChangedUrl', payload: { ...history.location, - pathname: '/endpoint-hosts', + pathname: getManagementUrl({ name: 'endpointList', excludePrefix: true }), }, }); await waitForAction('serverReturnedHostList'); diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts similarity index 100% rename from x-pack/plugins/security_solution/public/endpoint_hosts/store/index.test.ts rename to x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts similarity index 82% rename from x-pack/plugins/security_solution/public/endpoint_hosts/store/middleware.test.ts rename to x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index 0959a3438aad9a..a6cd2ca3afac45 100644 --- a/x-pack/plugins/security_solution/public/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -5,19 +5,23 @@ */ import { CoreStart, HttpSetup } from 'kibana/public'; import { applyMiddleware, createStore, Store } from 'redux'; -import { coreMock } from '../../../../../../src/core/public/mocks'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { History, createBrowserHistory } from 'history'; -import { DepsStartMock, depsStartMock } from '../../common/mock/endpoint'; +import { DepsStartMock, depsStartMock } from '../../../../common/mock/endpoint'; -import { createSpyMiddleware, MiddlewareActionSpyHelper } from '../../common/store/test_utils'; -import { Immutable, HostResultList } from '../../../common/endpoint/types'; -import { AppAction } from '../../common/store/actions'; +import { + createSpyMiddleware, + MiddlewareActionSpyHelper, +} from '../../../../common/store/test_utils'; +import { Immutable, HostResultList } from '../../../../../common/endpoint/types'; +import { AppAction } from '../../../../common/store/actions'; import { mockHostResultList } from './mock_host_result_list'; import { listData } from './selectors'; import { HostState } from '../types'; import { hostListReducer } from './reducer'; import { hostMiddlewareFactory } from './middleware'; +import { getManagementUrl } from '../../..'; describe('host list middleware', () => { let fakeCoreStart: jest.Mocked; @@ -56,7 +60,7 @@ describe('host list middleware', () => { type: 'userChangedUrl', payload: { ...history.location, - pathname: '/endpoint-hosts', + pathname: getManagementUrl({ name: 'endpointList', excludePrefix: true }), }, }); await waitForAction('serverReturnedHostList'); diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts similarity index 95% rename from x-pack/plugins/security_solution/public/endpoint_hosts/store/middleware.ts rename to x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index dd9ab19a702eaa..85667c9f9fc376 100644 --- a/x-pack/plugins/security_solution/public/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostResultList } from '../../../common/endpoint/types'; -import { ImmutableMiddlewareFactory } from '../../common/store'; +import { HostResultList } from '../../../../../common/endpoint/types'; +import { ImmutableMiddlewareFactory } from '../../../../common/store'; import { isOnHostPage, hasSelectedHost, uiQueryParams, listData } from './selectors'; import { HostState } from '../types'; @@ -37,7 +37,7 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor }); } } - if (action.type === 'userChangedUrl' && hasSelectedHost(state) !== false) { + if (action.type === 'userChangedUrl' && hasSelectedHost(state) === true) { // If user navigated directly to a host details page, load the host list if (listData(state).length === 0) { const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(state); diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/store/mock_host_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts similarity index 89% rename from x-pack/plugins/security_solution/public/endpoint_hosts/store/mock_host_result_list.ts rename to x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts index a2c410b5dbd659..05af1ee062de6f 100644 --- a/x-pack/plugins/security_solution/public/endpoint_hosts/store/mock_host_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostInfo, HostResultList, HostStatus } from '../../../common/endpoint/types'; -import { EndpointDocGenerator } from '../../../common/endpoint/generate_data'; +import { HostInfo, HostResultList, HostStatus } from '../../../../../common/endpoint/types'; +import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; export const mockHostResultList: (options?: { total?: number; diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts similarity index 95% rename from x-pack/plugins/security_solution/public/endpoint_hosts/store/reducer.ts rename to x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index c0d5e6931db2b7..23682544ec423e 100644 --- a/x-pack/plugins/security_solution/public/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -6,9 +6,9 @@ import { isOnHostPage, hasSelectedHost } from './selectors'; import { HostState } from '../types'; -import { AppAction } from '../../common/store/actions'; -import { ImmutableReducer } from '../../common/store'; -import { Immutable } from '../../../common/endpoint/types'; +import { AppAction } from '../../../../common/store/actions'; +import { ImmutableReducer } from '../../../../common/store'; +import { Immutable } from '../../../../../common/endpoint/types'; export const initialHostListState: Immutable = { hosts: [], diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts similarity index 86% rename from x-pack/plugins/security_solution/public/endpoint_hosts/store/selectors.ts rename to x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index 05b265b49ea5d5..5e7cbc0ef58d34 100644 --- a/x-pack/plugins/security_solution/public/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -7,13 +7,15 @@ // eslint-disable-next-line import/no-nodejs-modules import querystring from 'querystring'; import { createSelector } from 'reselect'; +import { matchPath } from 'react-router-dom'; import { Immutable, HostPolicyResponseAppliedAction, HostPolicyResponseConfiguration, HostPolicyResponseActionStatus, -} from '../../../common/endpoint/types'; +} from '../../../../../common/endpoint/types'; import { HostState, HostIndexUIQueryParams } from '../types'; +import { MANAGEMENT_ROUTING_ENDPOINTS_PATH } from '../../../common/constants'; const PAGE_SIZES = Object.freeze([10, 20, 50]); @@ -96,8 +98,14 @@ export const policyResponseLoading = (state: Immutable): boolean => export const policyResponseError = (state: Immutable) => state.policyResponseError; -export const isOnHostPage = (state: Immutable) => - state.location ? state.location.pathname === '/endpoint-hosts' : false; +export const isOnHostPage = (state: Immutable) => { + return ( + matchPath(state.location?.pathname ?? '', { + path: MANAGEMENT_ROUTING_ENDPOINTS_PATH, + exact: true, + }) !== null + ); +}; export const uiQueryParams: ( state: Immutable @@ -117,11 +125,21 @@ export const uiQueryParams: ( ]; for (const key of keys) { - const value = query[key]; - if (typeof value === 'string') { - data[key] = value; - } else if (Array.isArray(value)) { - data[key] = value[value.length - 1]; + const value: string | undefined = + typeof query[key] === 'string' + ? (query[key] as string) + : Array.isArray(query[key]) + ? (query[key][query[key].length - 1] as string) + : undefined; + + if (value !== undefined) { + if (key === 'show') { + if (value === 'policy_response' || value === 'details') { + data[key] = value; + } + } else { + data[key] = value; + } } } diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts similarity index 92% rename from x-pack/plugins/security_solution/public/endpoint_hosts/types.ts rename to x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index 421903cb6e1ab6..4881342c065737 100644 --- a/x-pack/plugins/security_solution/public/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -10,8 +10,8 @@ import { HostMetadata, HostPolicyResponse, AppLocation, -} from '../../common/endpoint/types'; -import { ServerApiError } from '../common/types'; +} from '../../../../common/endpoint/types'; +import { ServerApiError } from '../../../common/types'; export interface HostState { /** list of host **/ @@ -53,5 +53,5 @@ export interface HostIndexUIQueryParams { /** Which page to show */ page_index?: string; /** show the policy response or host details */ - show?: string; + show?: 'policy_response' | 'details'; } diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/view/details/components/flyout_sub_header.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/flyout_sub_header.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/endpoint_hosts/view/details/components/flyout_sub_header.tsx rename to x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/flyout_sub_header.tsx diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/view/details/host_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx similarity index 83% rename from x-pack/plugins/security_solution/public/endpoint_hosts/view/details/host_details.tsx rename to x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx index a0d4e628091238..b05cdfb3be840e 100644 --- a/x-pack/plugins/security_solution/public/endpoint_hosts/view/details/host_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx @@ -16,14 +16,14 @@ import { import React, { memo, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { HostMetadata } from '../../../../common/endpoint/types'; +import { HostMetadata } from '../../../../../../common/endpoint/types'; import { useHostSelector, useHostLogsUrl } from '../hooks'; -import { urlFromQueryParams } from '../url_from_query_params'; import { policyResponseStatus, uiQueryParams } from '../../store/selectors'; import { POLICY_STATUS_TO_HEALTH_COLOR } from '../host_constants'; -import { FormattedDateAndTime } from '../../../common/components/endpoint/formatted_date_time'; -import { useNavigateByRouterEventHandler } from '../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; -import { LinkToApp } from '../../../common/components/endpoint/link_to_app'; +import { FormattedDateAndTime } from '../../../../../common/components/endpoint/formatted_date_time'; +import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; +import { LinkToApp } from '../../../../../common/components/endpoint/link_to_app'; +import { getManagementUrl } from '../../../..'; const HostIds = styled(EuiListGroupItem)` margin-top: 0; @@ -61,14 +61,24 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { ]; }, [details]); - const policyResponseUri = useMemo(() => { - return urlFromQueryParams({ - ...queryParams, - selected_host: details.host.id, - show: 'policy_response', - }); + const [policyResponseUri, policyResponseRoutePath] = useMemo(() => { + const { selected_host, show, ...currentUrlParams } = queryParams; + return [ + getManagementUrl({ + name: 'endpointPolicyResponse', + ...currentUrlParams, + selected_host: details.host.id, + }), + getManagementUrl({ + name: 'endpointPolicyResponse', + excludePrefix: true, + ...currentUrlParams, + selected_host: details.host.id, + }), + ]; }, [details.host.id, queryParams]); - const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseUri); + + const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath); const detailsResultsLower = useMemo(() => { return [ @@ -90,7 +100,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} { const history = useHistory(); @@ -115,24 +116,32 @@ const PolicyResponseFlyoutPanel = memo<{ const responseAttentionCount = useHostSelector(policyResponseFailedOrWarningActionCount); const loading = useHostSelector(policyResponseLoading); const error = useHostSelector(policyResponseError); - const detailsUri = useMemo( - () => - urlFromQueryParams({ + const [detailsUri, detailsRoutePath] = useMemo( + () => [ + getManagementUrl({ + name: 'endpointList', ...queryParams, selected_host: hostMeta.host.id, }), + getManagementUrl({ + name: 'endpointList', + excludePrefix: true, + ...queryParams, + selected_host: hostMeta.host.id, + }), + ], [hostMeta.host.id, queryParams] ); - const backToDetailsClickHandler = useNavigateByRouterEventHandler(detailsUri); + const backToDetailsClickHandler = useNavigateByRouterEventHandler(detailsRoutePath); const backButtonProp = useMemo((): FlyoutSubHeaderProps['backButton'] => { return { title: i18n.translate('xpack.securitySolution.endpoint.host.policyResponse.backLinkTitle', { defaultMessage: 'Endpoint Details', }), - href: `?${detailsUri.search}`, + href: detailsUri, onClick: backToDetailsClickHandler, }; - }, [backToDetailsClickHandler, detailsUri.search]); + }, [backToDetailsClickHandler, detailsUri]); return ( <> diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/view/details/policy_response.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/endpoint_hosts/view/details/policy_response.tsx rename to x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx index c6ecffe0fd51ac..8172165a77e78b 100644 --- a/x-pack/plugins/security_solution/public/endpoint_hosts/view/details/policy_response.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx @@ -19,7 +19,7 @@ import { Immutable, HostPolicyResponseAppliedAction, HostPolicyResponseConfiguration, -} from '../../../../common/endpoint/types'; +} from '../../../../../../common/endpoint/types'; /** * Nested accordion in the policy response detailing any concerned diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/view/details/policy_response_friendly_names.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response_friendly_names.ts similarity index 100% rename from x-pack/plugins/security_solution/public/endpoint_hosts/view/details/policy_response_friendly_names.ts rename to x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response_friendly_names.ts diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts similarity index 74% rename from x-pack/plugins/security_solution/public/endpoint_hosts/view/hooks.ts rename to x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts index 78fd679f818b64..ddba6d7344ce54 100644 --- a/x-pack/plugins/security_solution/public/endpoint_hosts/view/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts @@ -7,12 +7,18 @@ import { useSelector } from 'react-redux'; import { useMemo } from 'react'; import { HostState } from '../types'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { State } from '../../common/store/types'; +import { + MANAGEMENT_STORE_ENDPOINTS_NAMESPACE, + MANAGEMENT_STORE_GLOBAL_NAMESPACE, +} from '../../../common/constants'; +import { useKibana } from '../../../../common/lib/kibana'; +import { State } from '../../../../common/store'; export function useHostSelector(selector: (state: HostState) => TSelected) { return useSelector(function (state: State) { - return selector(state.hostList as HostState); + return selector( + state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_ENDPOINTS_NAMESPACE] as HostState + ); }); } diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/view/host_constants.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts similarity index 95% rename from x-pack/plugins/security_solution/public/endpoint_hosts/view/host_constants.ts rename to x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts index efad4e3a468d87..645a4896770eee 100644 --- a/x-pack/plugins/security_solution/public/endpoint_hosts/view/host_constants.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostStatus, HostPolicyResponseActionStatus } from '../../../common/endpoint/types'; +import { HostStatus, HostPolicyResponseActionStatus } from '../../../../../common/endpoint/types'; export const HOST_STATUS_TO_HEALTH_COLOR = Object.freeze< { diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx similarity index 97% rename from x-pack/plugins/security_solution/public/endpoint_hosts/view/index.test.tsx rename to x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 5e0e3e7e163e5f..7d84bb52238a2e 100644 --- a/x-pack/plugins/security_solution/public/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -9,14 +9,14 @@ import * as reactTestingLibrary from '@testing-library/react'; import { HostList } from './index'; import { mockHostDetailsApiResult, mockHostResultList } from '../store/mock_host_result_list'; -import { AppContextTestRender, createAppRootMockRenderer } from '../../common/mock/endpoint'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { HostInfo, HostStatus, HostPolicyResponseActionStatus, -} from '../../../common/endpoint/types'; -import { EndpointDocGenerator } from '../../../common/endpoint/generate_data'; -import { AppAction } from '../../common/store/actions'; +} from '../../../../../common/endpoint/types'; +import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; +import { AppAction } from '../../../../common/store/actions'; describe('when on the hosts page', () => { const docGenerator = new EndpointDocGenerator(); @@ -202,7 +202,7 @@ describe('when on the hosts page', () => { const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); expect(policyStatusLink).not.toBeNull(); expect(policyStatusLink.getAttribute('href')).toEqual( - '?page_index=0&page_size=10&selected_host=1&show=policy_response' + '#/management/endpoints?page_index=0&page_size=10&selected_host=1&show=policy_response' ); }); it('should update the URL when policy status link is clicked', async () => { @@ -381,7 +381,7 @@ describe('when on the hosts page', () => { const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton'); expect(subHeaderBackLink.textContent).toBe('Endpoint Details'); expect(subHeaderBackLink.getAttribute('href')).toBe( - '?page_index=0&page_size=10&selected_host=1' + '#/management/endpoints?page_index=0&page_size=10&selected_host=1' ); }); it('should update URL when back to details link is clicked', async () => { diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx similarity index 68% rename from x-pack/plugins/security_solution/public/endpoint_hosts/view/index.tsx rename to x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 1fafb969e6e6c4..125723e9bcea66 100644 --- a/x-pack/plugins/security_solution/public/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -22,17 +22,19 @@ import { createStructuredSelector } from 'reselect'; import { HostDetailsFlyout } from './details'; import * as selectors from '../store/selectors'; import { useHostSelector } from './hooks'; -import { urlFromQueryParams } from './url_from_query_params'; import { HOST_STATUS_TO_HEALTH_COLOR } from './host_constants'; -import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler'; -import { CreateStructuredSelector } from '../../common/store'; -import { Immutable, HostInfo } from '../../../common/endpoint/types'; -import { PageView } from '../../common/components/endpoint/page_view'; +import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; +import { CreateStructuredSelector } from '../../../../common/store'; +import { Immutable, HostInfo } from '../../../../../common/endpoint/types'; +import { SpyRoute } from '../../../../common/utils/route/spy_routes'; +import { ManagementPageView } from '../../../components/management_page_view'; +import { getManagementUrl } from '../../..'; +import { FormattedDate } from '../../../../common/components/formatted_date'; const HostLink = memo<{ name: string; href: string; - route: ReturnType; + route: string; }>(({ name, href, route }) => { const clickHandler = useNavigateByRouterEventHandler(route); @@ -77,8 +79,11 @@ export const HostList = () => { const onTableChange = useCallback( ({ page }: { page: { index: number; size: number } }) => { const { index, size } = page; + // FIXME: PT: if host details is open, table is not displaying correct number of rows history.push( - urlFromQueryParams({ + getManagementUrl({ + name: 'endpointList', + excludePrefix: true, ...queryParams, page_index: JSON.stringify(index), page_size: JSON.stringify(size), @@ -89,22 +94,34 @@ export const HostList = () => { ); const columns: Array>> = useMemo(() => { + const lastActiveColumnName = i18n.translate('xpack.securitySolution.endpointList.lastActive', { + defaultMessage: 'Last Active', + }); + return [ { field: 'metadata.host', - name: i18n.translate('xpack.securitySolution.endpoint.host.list.hostname', { + name: i18n.translate('xpack.securitySolution.endpointList.hostname', { defaultMessage: 'Hostname', }), render: ({ hostname, id }: HostInfo['metadata']['host']) => { - const newQueryParams = urlFromQueryParams({ ...queryParams, selected_host: id }); - return ( - - ); + const toRoutePath = getManagementUrl({ + ...queryParams, + name: 'endpointDetails', + selected_host: id, + excludePrefix: true, + }); + const toRouteUrl = getManagementUrl({ + ...queryParams, + name: 'endpointDetails', + selected_host: id, + }); + return ; }, }, { field: 'host_status', - name: i18n.translate('xpack.securitySolution.endpoint.host.list.hostStatus', { + name: i18n.translate('xpack.securitySolution.endpointList.hostStatus', { defaultMessage: 'Host Status', }), // eslint-disable-next-line react/display-name @@ -116,7 +133,7 @@ export const HostList = () => { className="eui-textTruncate" > @@ -126,7 +143,7 @@ export const HostList = () => { }, { field: '', - name: i18n.translate('xpack.securitySolution.endpoint.host.list.policy', { + name: i18n.translate('xpack.securitySolution.endpointList.policy', { defaultMessage: 'Policy', }), truncateText: true, @@ -137,7 +154,7 @@ export const HostList = () => { }, { field: '', - name: i18n.translate('xpack.securitySolution.endpoint.host.list.policyStatus', { + name: i18n.translate('xpack.securitySolution.endpointList.policyStatus', { defaultMessage: 'Policy Status', }), // eslint-disable-next-line react/display-name @@ -145,7 +162,7 @@ export const HostList = () => { return ( @@ -154,7 +171,7 @@ export const HostList = () => { }, { field: '', - name: i18n.translate('xpack.securitySolution.endpoint.host.list.alerts', { + name: i18n.translate('xpack.securitySolution.endpointList.alerts', { defaultMessage: 'Alerts', }), dataType: 'number', @@ -164,14 +181,14 @@ export const HostList = () => { }, { field: 'metadata.host.os.name', - name: i18n.translate('xpack.securitySolution.endpoint.host.list.os', { + name: i18n.translate('xpack.securitySolution.endpointList.os', { defaultMessage: 'Operating System', }), truncateText: true, }, { field: 'metadata.host.ip', - name: i18n.translate('xpack.securitySolution.endpoint.host.list.ip', { + name: i18n.translate('xpack.securitySolution.endpointList.ip', { defaultMessage: 'IP Address', }), // eslint-disable-next-line react/display-name @@ -189,35 +206,38 @@ export const HostList = () => { }, { field: 'metadata.agent.version', - name: i18n.translate('xpack.securitySolution.endpoint.host.list.endpointVersion', { + name: i18n.translate('xpack.securitySolution.endpointList.endpointVersion', { defaultMessage: 'Version', }), }, { - field: '', - name: i18n.translate('xpack.securitySolution.endpoint.host.list.lastActive', { - defaultMessage: 'Last Active', - }), - dataType: 'date', - render: () => { - return 'xxxx'; + field: 'metadata.@timestamp', + name: lastActiveColumnName, + render(dateValue: HostInfo['metadata']['@timestamp']) { + return ( + + ); }, }, ]; }, [queryParams]); return ( - {hasSelectedHost && } @@ -232,6 +252,7 @@ export const HostList = () => { pagination={paginationSetup} onChange={onTableChange} /> - + + ); }; diff --git a/x-pack/plugins/security_solution/public/endpoint_hosts/view/url_from_query_params.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/url_from_query_params.ts similarity index 89% rename from x-pack/plugins/security_solution/public/endpoint_hosts/view/url_from_query_params.ts rename to x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/url_from_query_params.ts index e3728d63aea7c7..a14f1d0d0dd6c3 100644 --- a/x-pack/plugins/security_solution/public/endpoint_hosts/view/url_from_query_params.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/url_from_query_params.ts @@ -8,7 +8,7 @@ import querystring from 'querystring'; import { HostIndexUIQueryParams } from '../types'; -import { AppLocation } from '../../../common/endpoint/types'; +import { AppLocation } from '../../../../../common/endpoint/types'; export function urlFromQueryParams(queryParams: HostIndexUIQueryParams): Partial { const search = querystring.stringify(queryParams); diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx index aba482db865191..588b2677632349 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx @@ -6,34 +6,27 @@ import React, { memo } from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; -import { SpyRoute } from '../../common/utils/route/spy_routes'; import { PolicyContainer } from './policy'; import { MANAGEMENT_ROUTING_ENDPOINTS_PATH, MANAGEMENT_ROUTING_POLICIES_PATH, MANAGEMENT_ROUTING_ROOT_PATH, } from '../common/constants'; -import { ManagementPageView } from '../components/management_page_view'; import { NotFoundPage } from '../../app/404'; - -const TmpEndpoints = () => { - return ( - -

{'Endpoints will go here'}

- -
- ); -}; +import { EndpointsContainer } from './endpoint_hosts'; +import { getManagementUrl } from '..'; export const ManagementContainer = memo(() => { return ( - + } + render={() => ( + + )} /> diff --git a/x-pack/plugins/security_solution/public/management/store/middleware.ts b/x-pack/plugins/security_solution/public/management/store/middleware.ts index c8eb27e35f9dd9..9ca170cce8b3de 100644 --- a/x-pack/plugins/security_solution/public/management/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/store/middleware.ts @@ -11,16 +11,34 @@ import { } from '../../common/store'; import { policyListMiddlewareFactory } from '../pages/policy/store/policy_list'; import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details'; +import { + MANAGEMENT_STORE_ENDPOINTS_NAMESPACE, + MANAGEMENT_STORE_GLOBAL_NAMESPACE, + MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, + MANAGEMENT_STORE_POLICY_LIST_NAMESPACE, +} from '../common/constants'; +import { hostMiddlewareFactory } from '../pages/endpoint_hosts/store/middleware'; + +const policyListSelector = (state: State) => + state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]; +const policyDetailsSelector = (state: State) => + state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]; +const endpointsSelector = (state: State) => + state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]; export const managementMiddlewareFactory: SecuritySubPluginMiddlewareFactory = ( coreStart, depsStart ) => { - const listSelector = (state: State) => state.management.policyList; - const detailSelector = (state: State) => state.management.policyDetails; - return [ - substateMiddlewareFactory(listSelector, policyListMiddlewareFactory(coreStart, depsStart)), - substateMiddlewareFactory(detailSelector, policyDetailsMiddlewareFactory(coreStart, depsStart)), + substateMiddlewareFactory( + policyListSelector, + policyListMiddlewareFactory(coreStart, depsStart) + ), + substateMiddlewareFactory( + policyDetailsSelector, + policyDetailsMiddlewareFactory(coreStart, depsStart) + ), + substateMiddlewareFactory(endpointsSelector, hostMiddlewareFactory(coreStart, depsStart)), ]; }; diff --git a/x-pack/plugins/security_solution/public/management/store/reducer.ts b/x-pack/plugins/security_solution/public/management/store/reducer.ts index 64b2ab5c05f96a..e00b3ec9e0f5ba 100644 --- a/x-pack/plugins/security_solution/public/management/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/store/reducer.ts @@ -14,18 +14,24 @@ import { initialPolicyListState, } from '../pages/policy/store/policy_list/reducer'; import { + MANAGEMENT_STORE_ENDPOINTS_NAMESPACE, MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, MANAGEMENT_STORE_POLICY_LIST_NAMESPACE, } from '../common/constants'; import { ImmutableCombineReducers } from '../../common/store'; import { Immutable } from '../../../common/endpoint/types'; import { ManagementState } from '../types'; +import { hostListReducer, initialHostListState } from '../pages/endpoint_hosts/store/reducer'; const immutableCombineReducers: ImmutableCombineReducers = combineReducers; +/** + * Returns the initial state of the store for the SIEM Management section + */ export const mockManagementState: Immutable = { - policyList: initialPolicyListState(), - policyDetails: initialPolicyDetailsState(), + [MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: initialPolicyListState(), + [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(), + [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: initialHostListState, }; /** @@ -34,4 +40,6 @@ export const mockManagementState: Immutable = { export const managementReducer = immutableCombineReducers({ [MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: policyListReducer, [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: policyDetailsReducer, + // @ts-ignore + [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: hostListReducer, }); diff --git a/x-pack/plugins/security_solution/public/management/types.ts b/x-pack/plugins/security_solution/public/management/types.ts index eeeafb4cbe1509..fd6b9f6e6a8245 100644 --- a/x-pack/plugins/security_solution/public/management/types.ts +++ b/x-pack/plugins/security_solution/public/management/types.ts @@ -7,6 +7,7 @@ import { CombinedState } from 'redux'; import { SiemPageName } from '../app/types'; import { PolicyListState, PolicyDetailsState } from './pages/policy/types'; +import { HostState } from './pages/endpoint_hosts/types'; /** * The type for the management store global namespace. Used mostly internally to reference @@ -17,6 +18,7 @@ export type ManagementStoreGlobalNamespace = 'management'; export type ManagementState = CombinedState<{ policyList: PolicyListState; policyDetails: PolicyDetailsState; + endpoints: HostState; }>; /** 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/public/network/pages/network_empty_page.tsx b/x-pack/plugins/security_solution/public/network/pages/network_empty_page.tsx index 5fd6832e1990f8..dce3f85797f121 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network_empty_page.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network_empty_page.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { useKibana } from '../../common/lib/kibana'; import { EmptyPage } from '../../common/components/empty_page'; import * as i18n from '../../common/translations'; +import { ADD_DATA_PATH } from '../../../common/constants'; export const NetworkEmptyPage = React.memo(() => { const { http, docLinks } = useKibana().services; @@ -18,7 +19,7 @@ export const NetworkEmptyPage = React.memo(() => { { const { http, docLinks } = useKibana().services; @@ -18,7 +19,7 @@ const OverviewEmptyComponent: React.FC = () => { { @@ -37,7 +39,7 @@ export const Summary = React.memo(() => {
), data: ( - + ; - 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/security_solution/server/lib/detection_engine/rules/update_rules_notifications.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules_notifications.ts index 8fceb8ef720b5b..0dfe68f132b06d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules_notifications.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules_notifications.ts @@ -45,8 +45,5 @@ export const updateRulesNotifications = async ({ interval: ruleActions.alertThrottle, }); - // TODO: Workaround for https://github.com/elastic/kibana/issues/67290 - await alertsClient.updateApiKey({ id: ruleAlertId }); - return ruleActions; }; diff --git a/x-pack/plugins/security_solution/server/lib/framework/kibana_framework_adapter.ts b/x-pack/plugins/security_solution/server/lib/framework/kibana_framework_adapter.ts index 9a7b56eed366de..6d9e9b13bc356a 100644 --- a/x-pack/plugins/security_solution/server/lib/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/framework/kibana_framework_adapter.ts @@ -15,7 +15,7 @@ import { RequestHandlerContext, KibanaRequest, } from '../../../../../../src/core/server'; -import { IndexPatternsFetcher } from '../../../../../../src/plugins/data/server'; +import { IndexPatternsFetcher, UI_SETTINGS } from '../../../../../../src/plugins/data/server'; import { AuthenticatedUser } from '../../../../security/common/model'; import { SetupPlugins } from '../../plugin'; @@ -43,10 +43,10 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { params: Record ) { const { elasticsearch, uiSettings } = req.context.core; - const includeFrozen = await uiSettings.client.get('search:includeFrozen'); + const includeFrozen = await uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); const maxConcurrentShardRequests = endpoint === 'msearch' - ? await uiSettings.client.get('courier:maxConcurrentShardRequests') + ? await uiSettings.client.get(UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS) : 0; return elasticsearch.legacy.client.callAsCurrentUser(endpoint, { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5b017c6565c040..9d3bc008106c67 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -496,6 +496,81 @@ "dashboard.topNave.viewConfigDescription": "編集をキャンセルして表示限定モードに切り替えます", "dashboard.urlWasRemovedInSixZeroWarningMessage": "URL「dashboard/create」は6.0で廃止されました。ブックマークを更新してください。", "dashboard.visitVisualizeAppLinkText": "可視化アプリにアクセス", + "data.advancedSettings.courier.batchSearchesText": "無効の場合、ダッシュボードパネルは個々に読み込まれ、検索リクエストはユーザーが移動するか\n クエリを更新すると停止します。有効の場合、ダッシュボードパネルはすべてのデータが読み込まれると同時に読み込まれ、\n 検索は停止しません。", + "data.advancedSettings.courier.batchSearchesTextDeprecation": "この設定はサポートが終了し、Kibana 8.0 では削除されます。", + "data.advancedSettings.courier.batchSearchesTitle": "同時検索のバッチ処理", + "data.advancedSettings.courier.customRequestPreference.requestPreferenceLinkText": "リクエスト設定", + "data.advancedSettings.courier.customRequestPreferenceText": "{setRequestReferenceSetting} が {customSettingValue} に設定されている時に使用される {requestPreferenceLink} です。", + "data.advancedSettings.courier.customRequestPreferenceTitle": "カスタムリクエスト設定", + "data.advancedSettings.courier.ignoreFilterText": "この構成は、似ていないインデックスにアクセスするビジュアライゼーションを含むダッシュボードのサポートを強化します。無効にすると、すべてのフィルターがすべてのビジュアライゼーションに適用されます。有効にすると、ビジュアライゼーションのインデックスにフィルター対象のフィールドが含まれていない場合、ビジュアライゼーションの際にフィルターが無視されます。", + "data.advancedSettings.courier.ignoreFilterTitle": "フィルターの無視", + "data.advancedSettings.courier.maxRequestsText": "Kibana から送信された _msearch requests リクエストに使用される {maxRequestsLink} 設定を管理します。この構成を無効にして Elasticsearch のデフォルトを使用するには、0 に設定します。", + "data.advancedSettings.courier.maxRequestsTitle": "最大同時シャードリクエスト", + "data.advancedSettings.courier.requestPreferenceCustom": "カスタム", + "data.advancedSettings.courier.requestPreferenceNone": "なし", + "data.advancedSettings.courier.requestPreferenceSessionId": "セッション ID", + "data.advancedSettings.courier.requestPreferenceText": "どのシャードが検索リクエストを扱うかを設定できます。
    \n
  • {sessionId}: 同じシャードのすべての検索リクエストを実行するため、オペレーションを制限します。\n これにはリクエスト間でシャードのキャッシュを共有できるというメリットがあります。
  • \n
  • {custom}: 独自の設定が可能になります。\n couriercustomRequestPreference で設定値をカスタマイズします。
  • \n
  • {none}: 設定されていないことを意味します。\n これにより、リクエストが全シャードコピー間に分散されるため、パフォーマンスが改善される可能性があります。\n ただし、シャードによって更新ステータスが異なる場合があるため、結果に矛盾が生じる可能性があります。
  • \n
", + "data.advancedSettings.courier.requestPreferenceTitle": "リクエスト設定", + "data.advancedSettings.defaultIndexText": "インデックスが設定されていない時にアクセスするインデックスです", + "data.advancedSettings.defaultIndexTitle": "デフォルトのインデックス", + "data.advancedSettings.pinFiltersText": "フィルターがデフォルトでグローバル (ピン付けされた状態) になるかの設定です", + "data.advancedSettings.pinFiltersTitle": "フィルターをデフォルトでピン付けする", + "data.advancedSettings.query.allowWildcardsText": "設定すると、クエリ句の頭に * が使えるようになります。現在クエリバーで実験的クエリ機能が有効になっている場合にのみ適用されます。基本的な Lucene クエリでリーディングワイルドカードを無効にするには、{queryStringOptionsPattern} を使用します。", + "data.advancedSettings.query.allowWildcardsTitle": "クエリでリーディングワイルドカードを許可する", + "data.advancedSettings.query.queryStringOptions.optionsLinkText": "オプション", + "data.advancedSettings.query.queryStringOptionsText": "Lucene クエリ文字列パーサーの {optionsLink}「{queryLanguage}」が {luceneLanguage} に設定されている時にのみ使用されます。", + "data.advancedSettings.query.queryStringOptionsTitle": "クエリ文字列のオプション", + "data.advancedSettings.searchQueryLanguageKql": "KQL", + "data.advancedSettings.searchQueryLanguageLucene": "Lucene", + "data.advancedSettings.searchQueryLanguageText": "クエリバーで使用されるクエリ言語です。KQL は Kibana 用に特別に開発された新しい言語です。", + "data.advancedSettings.searchQueryLanguageTitle": "クエリ言語", + "data.advancedSettings.shortenFieldsText": "長いフィールドを短くします。例: foo.bar.baz の代わりに f.b.baz と表示", + "data.advancedSettings.shortenFieldsTitle": "フィールドの短縮", + "data.advancedSettings.sortOptions.optionsLinkText": "オプション", + "data.advancedSettings.sortOptionsText": "Elasticsearch の並べ替えパラメーターの {optionsLink}", + "data.advancedSettings.sortOptionsTitle": "並べ替えオプション", + "data.advancedSettings.format.bytesFormat.numeralFormatLinkText": "数字フォーマット", + "data.advancedSettings.format.bytesFormatText": "「バイト」フォーマットのデフォルト {numeralFormatLink} です", + "data.advancedSettings.format.bytesFormatTitle": "バイトフォーマット", + "data.advancedSettings.format.currencyFormat.numeralFormatLinkText": "数字フォーマット", + "data.advancedSettings.format.currencyFormatText": "「通貨」フォーマットのデフォルト {numeralFormatLink} です", + "data.advancedSettings.format.currencyFormatTitle": "通貨フォーマット", + "data.advancedSettings.format.defaultTypeMapText": "各フィールドタイプにデフォルトで使用するフォーマット名のマップです。フィールドタイプが特に指定されていない場合は {defaultFormat} が使用されます", + "data.advancedSettings.format.defaultTypeMapTitle": "フィールドタイプフォーマット名", + "data.advancedSettings.format.formattingLocale.numeralLanguageLinkText": "数字言語", + "data.advancedSettings.format.formattingLocaleText": "{numeralLanguageLink} ロケール", + "data.advancedSettings.format.formattingLocaleTitle": "フォーマットロケール", + "data.advancedSettings.format.numberFormat.numeralFormatLinkText": "数字フォーマット", + "data.advancedSettings.format.numberFormatText": "「数字」フォーマットのデフォルト {numeralFormatLink} です", + "data.advancedSettings.format.numberFormatTitle": "数字フォーマット", + "data.advancedSettings.format.percentFormat.numeralFormatLinkText": "数字フォーマット", + "data.advancedSettings.format.percentFormatText": "「パーセント」フォーマットのデフォルト {numeralFormatLink} です", + "data.advancedSettings.format.percentFormatTitle": "パーセントフォーマット", + "data.advancedSettings.histogram.barTargetText": "日付ヒストグラムで「自動」間隔を使用する際、この数に近いバーの作成を試みます", + "data.advancedSettings.histogram.barTargetTitle": "目標バー数", + "data.advancedSettings.histogram.maxBarsText": "日付ヒストグラムに表示されるバーの数の上限です。必要に応じて値をスケーリングしてください", + "data.advancedSettings.histogram.maxBarsTitle": "最高バー数", + "data.advancedSettings.historyLimitText": "履歴があるフィールド (例: クエリインプット) に個の数の最近の値が表示されます", + "data.advancedSettings.historyLimitTitle": "履歴制限数", + "data.advancedSettings.indexPatternPlaceholderText": "「管理 > インデックスパターン > インデックスパターンを作成」で使用される「インデックスパターン名」フィールドのプレースホルダーです。", + "data.advancedSettings.indexPatternPlaceholderTitle": "インデックスパターンのプレースホルダー", + "data.advancedSettings.suggestFilterValuesText": "フィルターエディターがフィールドの値の候補を表示しないようにするには、このプロパティを false にしてください。", + "data.advancedSettings.suggestFilterValuesTitle": "フィルターエディターの候補値", + "data.advancedSettings.timepicker.last15Minutes": "過去 15 分間", + "data.advancedSettings.timepicker.last1Hour": "過去 1 時間", + "data.advancedSettings.timepicker.last1Year": "過去 1 年間", + "data.advancedSettings.timepicker.last24Hours": "過去 24 時間", + "data.advancedSettings.timepicker.last30Days": "過去 30 日間", + "data.advancedSettings.timepicker.last30Minutes": "過去 30 分間", + "data.advancedSettings.timepicker.last7Days": "過去 7 日間", + "data.advancedSettings.timepicker.last90Days": "過去 90 日間", + "data.advancedSettings.timepicker.quickRanges.acceptedFormatsLinkText": "対応フォーマット", + "data.advancedSettings.timepicker.quickRangesText": "タイムピッカーのクイックセクションに表示される範囲のリストです。それぞれのオブジェクトに「開始」、「終了」({acceptedFormatsLink} を参照)、「表示」(表示するタイトル) が含まれるオブジェクトの配列です。", + "data.advancedSettings.timepicker.quickRangesTitle": "タイムピッカーのクイック範囲", + "data.advancedSettings.timepicker.refreshIntervalDefaultsText": "時間フィルターのデフォルト更新間隔", + "data.advancedSettings.timepicker.refreshIntervalDefaultsTitle": "タイムピッカーの更新間隔", + "data.advancedSettings.timepicker.thisWeek": "今週", + "data.advancedSettings.timepicker.today": "今日", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} と {lt} {to}", "data.common.kql.errors.endOfInputText": "インプットの終わり", "data.common.kql.errors.fieldNameText": "フィールド名", @@ -1233,9 +1308,9 @@ "home.addData.metrics.nameTitle": "メトリック", "home.addData.sampleDataLink": "データセットと Kibana ダッシュボードを読み込む", "home.addData.sampleDataTitle": "サンプルデータの追加", - "home.addData.siem.addSiemEventsButtonLabel": "イベントを追加", - "home.addData.siem.nameDescription": "即利用可能なビジュアライゼーションで、セキュリティイベントをまとめてインタラクティブな調査を可能にします。", - "home.addData.siem.nameTitle": "SIEM", + "home.addData.securitySolution.addSecurityEventsButtonLabel": "イベントを追加", + "home.addData.securitySolution.nameDescription": "即利用可能なビジュアライゼーションで、セキュリティイベントをまとめてインタラクティブな調査を可能にします。", + "home.addData.securitySolution.nameTitle": "Security", "home.addData.title.observability": "オブザーバビリティ", "home.addData.title.security": "セキュリティ", "home.addData.uploadFileLink": "CSV、NDJSON、またはログファイルをインポート", @@ -1367,7 +1442,7 @@ "home.tutorial.tabs.loggingTitle": "ログ", "home.tutorial.tabs.metricsTitle": "メトリック", "home.tutorial.tabs.sampleDataTitle": "サンプルデータ", - "home.tutorial.tabs.siemTitle": "SIEM", + "home.tutorial.tabs.securitySolutionTitle": "Security", "home.tutorial.unexpectedStatusCheckStateErrorDescription": "予期せぬステータス確認ステータス {statusCheckState}", "home.tutorial.unhandledInstructionTypeErrorDescription": "予期せぬ指示タイプ {visibleInstructions}", "home.tutorials.activemqLogs.artifacts.dashboards.linkLabel": "ActiveMQ アプリケーションイベント", @@ -1390,7 +1465,7 @@ "home.tutorials.apacheMetrics.longDescription": "Metricbeat モジュール「apache」は、Apache 2 HTTP サーバーから内部メトリックを取得します。[詳細]({learnMoreLink})。", "home.tutorials.apacheMetrics.nameTitle": "Apache メトリック", "home.tutorials.apacheMetrics.shortDescription": "Apache 2 HTTP サーバーから内部メトリックを取得します。", - "home.tutorials.auditbeat.artifacts.dashboards.linkLabel": "SIEM アプリ", + "home.tutorials.auditbeat.artifacts.dashboards.linkLabel": "Security アプリ", "home.tutorials.auditbeat.longDescription": "Auditbeat を使用してホストから監査データを収集します。これらにはプロセス、ユーザー、ログイン、ソケット情報、ファイルアクセス、その他が含まれます。[詳細]({learnMoreLink})。", "home.tutorials.auditbeat.nameTitle": "Auditbeat", "home.tutorials.auditbeat.shortDescription": "ホストから監査データを収集します。", @@ -1409,7 +1484,7 @@ "home.tutorials.cephMetrics.longDescription": "Metricbeat モジュール「ceph」は、Ceph から内部メトリックを取得します。[詳細]({learnMoreLink})。", "home.tutorials.cephMetrics.nameTitle": "Ceph メトリック", "home.tutorials.cephMetrics.shortDescription": "Ceph サーバーから内部メトリックを取得します。", - "home.tutorials.ciscoLogs.artifacts.dashboards.linkLabel": "SIEM アプリ", + "home.tutorials.ciscoLogs.artifacts.dashboards.linkLabel": "Security アプリ", "home.tutorials.ciscoLogs.longDescription": "これは Cisco ネットワークデバイスのログ用のモジュールです。現在、syslog 経由またはファイルから読み込まれた Cisco ASA ファイアウォールログの「asa」ファイルセットをサポートしています。[詳細]({learnMoreLink})。", "home.tutorials.ciscoLogs.nameTitle": "Cisco", "home.tutorials.ciscoLogs.shortDescription": "Cisco ASA ファイアウォールからのログを収集・解析します。", @@ -1739,7 +1814,7 @@ "home.tutorials.elasticsearchMetrics.longDescription": "Metricbeat モジュール「elasticsearch」は、Elasticsearch から内部メトリックを取得します。[詳細]({learnMoreLink})。", "home.tutorials.elasticsearchMetrics.nameTitle": "Elasticsearch メトリック", "home.tutorials.elasticsearchMetrics.shortDescription": "Elasticsearch から内部メトリックを取得します。", - "home.tutorials.envoyproxyLogs.artifacts.dashboards.linkLabel": "SIEM アプリ", + "home.tutorials.envoyproxyLogs.artifacts.dashboards.linkLabel": "Security アプリ", "home.tutorials.envoyproxyLogs.longDescription": "これは [Envoy proxy access log](https://www.envoyproxy.io/docs/envoy/v1.10.0/configuration/access_log) 用の Filebeatモジュールです。Kubernetes でのスタンドアロンのデプロイメントと Envoy プロキシデプロイメントの両方をサポートします。[詳細]({learnMoreLink})。", "home.tutorials.envoyproxyLogs.nameTitle": "Envoyproxy", "home.tutorials.envoyproxyLogs.shortDescription": "Envoy プロキシからのログを収集・解析します。", @@ -1770,7 +1845,7 @@ "home.tutorials.iisLogs.longDescription": "「iis」Filebeat モジュールが、Nginx HTTP サーバーにより作成されたアクセスとエラーのログをパースします。[詳細]({learnMoreLink})。", "home.tutorials.iisLogs.nameTitle": "IIS ログ", "home.tutorials.iisLogs.shortDescription": "IIS HTTP サーバーにより作成されたアクセスとエラーのログを収集しパースします。", - "home.tutorials.iptablesLogs.artifacts.dashboards.linkLabel": "SIEM アプリ", + "home.tutorials.iptablesLogs.artifacts.dashboards.linkLabel": "Security アプリ", "home.tutorials.iptablesLogs.longDescription": "これは iptables と ip6tables ログ用のモジュールです。ネットワーク上で受信した syslog ログ経由や、ファイルからのログをパースします。また、ルールセット名、ルール番号、トラフィックに実行されたアクション (許可/拒否) を含む、Ubiquiti ファイアウォールにより追加された接頭辞も認識できます。[詳細]({learnMoreLink})。", "home.tutorials.iptablesLogs.nameTitle": "Iptables / Ubiquiti", "home.tutorials.iptablesLogs.shortDescription": "iptables と ip6tables ログ、または Ubiqiti からのログを収集・解析します。", @@ -1942,7 +2017,7 @@ "home.tutorials.vsphereMetrics.longDescription": "「vsphere」Metricbeat モジュールは、vSphere クラスターから内部メトリックを取得します。 [詳細]({learnMoreLink})。", "home.tutorials.vsphereMetrics.nameTitle": "vSphere メトリック", "home.tutorials.vsphereMetrics.shortDescription": "vSphere から内部メトリックを取得します。", - "home.tutorials.windowsEventLogs.artifacts.application.label": "SIEM アプリ", + "home.tutorials.windowsEventLogs.artifacts.application.label": "Security アプリ", "home.tutorials.windowsEventLogs.longDescription": "Winlogbeat を使用して Windows イベントログからログを収集します。[詳細]({learnMoreLink})。", "home.tutorials.windowsEventLogs.nameTitle": "Windows イベントログ", "home.tutorials.windowsEventLogs.shortDescription": "Windows イベントログからイベントを取得します。", @@ -2047,25 +2122,10 @@ "inspector.requests.statisticsTabLabel": "統計", "inspector.title": "インスペクター", "inspector.view": "{viewName} を表示", - "kbn.advancedSettings.courier.batchSearchesText": "無効の場合、ダッシュボードパネルは個々に読み込まれ、検索リクエストはユーザーが移動するか\n クエリを更新すると停止します。有効の場合、ダッシュボードパネルはすべてのデータが読み込まれると同時に読み込まれ、\n 検索は停止しません。", - "kbn.advancedSettings.courier.batchSearchesTextDeprecation": "この設定はサポートが終了し、Kibana 8.0 では削除されます。", - "kbn.advancedSettings.courier.batchSearchesTitle": "同時検索のバッチ処理", - "kbn.advancedSettings.courier.customRequestPreference.requestPreferenceLinkText": "リクエスト設定", - "kbn.advancedSettings.courier.customRequestPreferenceText": "{setRequestReferenceSetting} が {customSettingValue} に設定されている時に使用される {requestPreferenceLink} です。", - "kbn.advancedSettings.courier.customRequestPreferenceTitle": "カスタムリクエスト設定", - "kbn.advancedSettings.courier.ignoreFilterText": "この構成は、似ていないインデックスにアクセスするビジュアライゼーションを含むダッシュボードのサポートを強化します。無効にすると、すべてのフィルターがすべてのビジュアライゼーションに適用されます。有効にすると、ビジュアライゼーションのインデックスにフィルター対象のフィールドが含まれていない場合、ビジュアライゼーションの際にフィルターが無視されます。", - "kbn.advancedSettings.courier.ignoreFilterTitle": "フィルターの無視", - "kbn.advancedSettings.courier.maxRequestsText": "Kibana から送信された _msearch requests リクエストに使用される {maxRequestsLink} 設定を管理します。この構成を無効にして Elasticsearch のデフォルトを使用するには、0 に設定します。", - "kbn.advancedSettings.courier.maxRequestsTitle": "最大同時シャードリクエスト", - "kbn.advancedSettings.courier.requestPreferenceCustom": "カスタム", - "kbn.advancedSettings.courier.requestPreferenceNone": "なし", - "kbn.advancedSettings.courier.requestPreferenceSessionId": "セッション ID", - "kbn.advancedSettings.courier.requestPreferenceText": "どのシャードが検索リクエストを扱うかを設定できます。
    \n
  • {sessionId}: 同じシャードのすべての検索リクエストを実行するため、オペレーションを制限します。\n これにはリクエスト間でシャードのキャッシュを共有できるというメリットがあります。
  • \n
  • {custom}: 独自の設定が可能になります。\n couriercustomRequestPreference で設定値をカスタマイズします。
  • \n
  • {none}: 設定されていないことを意味します。\n これにより、リクエストが全シャードコピー間に分散されるため、パフォーマンスが改善される可能性があります。\n ただし、シャードによって更新ステータスが異なる場合があるため、結果に矛盾が生じる可能性があります。
  • \n
", - "kbn.advancedSettings.courier.requestPreferenceTitle": "リクエスト設定", - "kbn.advancedSettings.csv.quoteValuesText": "csv エクスポートに値を引用するかどうかです", - "kbn.advancedSettings.csv.quoteValuesTitle": "CSV の値を引用", - "kbn.advancedSettings.csv.separatorText": "エクスポートされた値をこの文字列で区切ります", - "kbn.advancedSettings.csv.separatorTitle": "CSV セパレーター", + "share.advancedSettings.csv.quoteValuesText": "csv エクスポートに値を引用するかどうかです", + "share.advancedSettings.csv.quoteValuesTitle": "CSV の値を引用", + "share.advancedSettings.csv.separatorText": "エクスポートされた値をこの文字列で区切ります", + "share.advancedSettings.csv.separatorTitle": "CSV セパレーター", "kbn.advancedSettings.darkModeText": "Kibana UI のダークモードを有効にします。この設定を適用するにはページの更新が必要です。", "kbn.advancedSettings.darkModeTitle": "ダークモード", "kbn.advancedSettings.dateFormat.dayOfWeekText": "週の初めの曜日を設定します", @@ -2081,38 +2141,11 @@ "kbn.advancedSettings.dateNanosFormatText": "Elasticsearch の {dateNanosLink} データタイプに使用されます", "kbn.advancedSettings.dateNanosFormatTitle": "ナノ秒フォーマットでの日付", "kbn.advancedSettings.dateNanosLinkTitle": "date_nanos", - "kbn.advancedSettings.defaultIndexText": "インデックスが設定されていない時にアクセスするインデックスです", - "kbn.advancedSettings.defaultIndexTitle": "デフォルトのインデックス", "kbn.advancedSettings.defaultRoute.defaultRouteIsRelativeValidationMessage": "相対 URL でなければなりません。", "kbn.advancedSettings.defaultRoute.defaultRouteText": "この設定は、Kibana 起動時のデフォルトのルートを設定します。この設定で、Kibana 起動時のランディングページを変更できます。経路は相対 URL でなければなりません。", "kbn.advancedSettings.defaultRoute.defaultRouteTitle": "デフォルトのルート", "kbn.advancedSettings.disableAnimationsText": "Kibana UI の不要なアニメーションをオフにします。変更を適用するにはページを更新してください。", "kbn.advancedSettings.disableAnimationsTitle": "アニメーションを無効にする", - "kbn.advancedSettings.format.bytesFormat.numeralFormatLinkText": "数字フォーマット", - "kbn.advancedSettings.format.bytesFormatText": "「バイト」フォーマットのデフォルト {numeralFormatLink} です", - "kbn.advancedSettings.format.bytesFormatTitle": "バイトフォーマット", - "kbn.advancedSettings.format.currencyFormat.numeralFormatLinkText": "数字フォーマット", - "kbn.advancedSettings.format.currencyFormatText": "「通貨」フォーマットのデフォルト {numeralFormatLink} です", - "kbn.advancedSettings.format.currencyFormatTitle": "通貨フォーマット", - "kbn.advancedSettings.format.defaultTypeMapText": "各フィールドタイプにデフォルトで使用するフォーマット名のマップです。フィールドタイプが特に指定されていない場合は {defaultFormat} が使用されます", - "kbn.advancedSettings.format.defaultTypeMapTitle": "フィールドタイプフォーマット名", - "kbn.advancedSettings.format.formattingLocale.numeralLanguageLinkText": "数字言語", - "kbn.advancedSettings.format.formattingLocaleText": "{numeralLanguageLink} ロケール", - "kbn.advancedSettings.format.formattingLocaleTitle": "フォーマットロケール", - "kbn.advancedSettings.format.numberFormat.numeralFormatLinkText": "数字フォーマット", - "kbn.advancedSettings.format.numberFormatText": "「数字」フォーマットのデフォルト {numeralFormatLink} です", - "kbn.advancedSettings.format.numberFormatTitle": "数字フォーマット", - "kbn.advancedSettings.format.percentFormat.numeralFormatLinkText": "数字フォーマット", - "kbn.advancedSettings.format.percentFormatText": "「パーセント」フォーマットのデフォルト {numeralFormatLink} です", - "kbn.advancedSettings.format.percentFormatTitle": "パーセントフォーマット", - "kbn.advancedSettings.histogram.barTargetText": "日付ヒストグラムで「自動」間隔を使用する際、この数に近いバーの作成を試みます", - "kbn.advancedSettings.histogram.barTargetTitle": "目標バー数", - "kbn.advancedSettings.histogram.maxBarsText": "日付ヒストグラムに表示されるバーの数の上限です。必要に応じて値をスケーリングしてください", - "kbn.advancedSettings.histogram.maxBarsTitle": "最高バー数", - "kbn.advancedSettings.historyLimitText": "履歴があるフィールド (例: クエリインプット) に個の数の最近の値が表示されます", - "kbn.advancedSettings.historyLimitTitle": "履歴制限数", - "kbn.advancedSettings.indexPatternPlaceholderText": "「管理 > インデックスパターン > インデックスパターンを作成」で使用される「インデックスパターン名」フィールドのプレースホルダーです。", - "kbn.advancedSettings.indexPatternPlaceholderTitle": "インデックスパターンのプレースホルダー", "kbn.advancedSettings.maxCellHeightText": "表のセルが使用する高さの上限です。この切り捨てを無効にするには 0 に設定します", "kbn.advancedSettings.maxCellHeightTitle": "表のセルの高さの上限", "kbn.advancedSettings.notifications.banner.markdownLinkText": "マークダウン対応", @@ -2130,43 +2163,10 @@ "kbn.advancedSettings.pageNavigationLegacy": "レガシー", "kbn.advancedSettings.pageNavigationModern": "モダン", "kbn.advancedSettings.pageNavigationName": "サイドナビゲーションスタイル", - "kbn.advancedSettings.pinFiltersText": "フィルターがデフォルトでグローバル (ピン付けされた状態) になるかの設定です", - "kbn.advancedSettings.pinFiltersTitle": "フィルターをデフォルトでピン付けする", - "kbn.advancedSettings.query.allowWildcardsText": "設定すると、クエリ句の頭に * が使えるようになります。現在クエリバーで実験的クエリ機能が有効になっている場合にのみ適用されます。基本的な Lucene クエリでリーディングワイルドカードを無効にするには、{queryStringOptionsPattern} を使用します。", - "kbn.advancedSettings.query.allowWildcardsTitle": "クエリでリーディングワイルドカードを許可する", - "kbn.advancedSettings.query.queryStringOptions.optionsLinkText": "オプション", - "kbn.advancedSettings.query.queryStringOptionsText": "Lucene クエリ文字列パーサーの {optionsLink}「{queryLanguage}」が {luceneLanguage} に設定されている時にのみ使用されます。", - "kbn.advancedSettings.query.queryStringOptionsTitle": "クエリ文字列のオプション", - "kbn.advancedSettings.searchQueryLanguageKql": "KQL", - "kbn.advancedSettings.searchQueryLanguageLucene": "Lucene", - "kbn.advancedSettings.searchQueryLanguageText": "クエリバーで使用されるクエリ言語です。KQL は Kibana 用に特別に開発された新しい言語です。", - "kbn.advancedSettings.searchQueryLanguageTitle": "クエリ言語", - "kbn.advancedSettings.shortenFieldsText": "長いフィールドを短くします。例: foo.bar.baz の代わりに f.b.baz と表示", - "kbn.advancedSettings.shortenFieldsTitle": "フィールドの短縮", - "kbn.advancedSettings.sortOptions.optionsLinkText": "オプション", - "kbn.advancedSettings.sortOptionsText": "Elasticsearch の並べ替えパラメーターの {optionsLink}", - "kbn.advancedSettings.sortOptionsTitle": "並べ替えオプション", "kbn.advancedSettings.storeUrlText": "URL は長くなりすぎてブラウザが対応できない場合があります。セッションストレージに URL の一部を保存することがで この問題に対処できるかテストしています。結果を教えてください!", "kbn.advancedSettings.storeUrlTitle": "セッションストレージに URL を格納", - "kbn.advancedSettings.suggestFilterValuesText": "フィルターエディターがフィールドの値の候補を表示しないようにするには、このプロパティを false にしてください。", - "kbn.advancedSettings.suggestFilterValuesTitle": "フィルターエディターの候補値", - "kbn.advancedSettings.timepicker.last15Minutes": "過去 15 分間", - "kbn.advancedSettings.timepicker.last1Hour": "過去 1 時間", - "kbn.advancedSettings.timepicker.last1Year": "過去 1 年間", - "kbn.advancedSettings.timepicker.last24Hours": "過去 24 時間", - "kbn.advancedSettings.timepicker.last30Days": "過去 30 日間", - "kbn.advancedSettings.timepicker.last30Minutes": "過去 30 分間", - "kbn.advancedSettings.timepicker.last7Days": "過去 7 日間", - "kbn.advancedSettings.timepicker.last90Days": "過去 90 日間", - "kbn.advancedSettings.timepicker.quickRanges.acceptedFormatsLinkText": "対応フォーマット", - "kbn.advancedSettings.timepicker.quickRangesText": "タイムピッカーのクイックセクションに表示される範囲のリストです。それぞれのオブジェクトに「開始」、「終了」({acceptedFormatsLink} を参照)、「表示」(表示するタイトル) が含まれるオブジェクトの配列です。", - "kbn.advancedSettings.timepicker.quickRangesTitle": "タイムピッカーのクイック範囲", - "kbn.advancedSettings.timepicker.refreshIntervalDefaultsText": "時間フィルターのデフォルト更新間隔", - "kbn.advancedSettings.timepicker.refreshIntervalDefaultsTitle": "タイムピッカーの更新間隔", - "kbn.advancedSettings.timepicker.thisWeek": "今週", "kbn.advancedSettings.timepicker.timeDefaultsText": "時間フィルターが選択されずに Kibana が起動した際に使用される時間フィルターです", "kbn.advancedSettings.timepicker.timeDefaultsTitle": "デフォルトのタイムピッカー", - "kbn.advancedSettings.timepicker.today": "今日", "kbn.advancedSettings.visualization.showRegionMapWarningsText": "用語がマップの形に合わない場合に地域マップに警告を表示するかどうかです。", "kbn.advancedSettings.visualization.showRegionMapWarningsTitle": "地域マップに警告を表示", "kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "ディメンションの説明", @@ -2175,8 +2175,8 @@ "kbn.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText": "プロパティ", "kbn.advancedSettings.visualization.tileMap.wmsDefaultsText": "座標マップの WMS マップサーバーサポートのデフォルトの {propertiesLink} です。", "kbn.advancedSettings.visualization.tileMap.wmsDefaultsTitle": "デフォルトの WMS プロパティ", - "kbn.advancedSettings.visualizeEnableLabsText": "ユーザーが実験的なビジュアライゼーションを作成、表示、編集できるようになります。無効の場合、\n ユーザーは本番準備が整ったビジュアライゼーションのみを利用できます。", - "kbn.advancedSettings.visualizeEnableLabsTitle": "実験的なビジュアライゼーションを有効にする", + "visualizations.advancedSettings.visualizeEnableLabsText": "ユーザーが実験的なビジュアライゼーションを作成、表示、編集できるようになります。無効の場合、\n ユーザーは本番準備が整ったビジュアライゼーションのみを利用できます。", + "visualizations.advancedSettings.visualizeEnableLabsTitle": "実験的なビジュアライゼーションを有効にする", "kibana_legacy.bigUrlWarningNotificationMessage": "{advancedSettingsLink}で{storeInSessionStorageParam}オプションを有効にするか、オンスクリーンビジュアルを簡素化してください。", "kibana_legacy.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高度な設定", "kibana_legacy.bigUrlWarningNotificationTitle": "URLが大きく、Kibanaの動作が停止する可能性があります", @@ -3971,14 +3971,14 @@ "xpack.actions.serverSideErrors.unavailableLicenseErrorMessage": "現時点でライセンス情報を入手できないため、アクションタイプ {actionTypeId} は無効です。", "xpack.actions.serverSideErrors.unavailableLicenseInformationErrorMessage": "グラフを利用できません。現在ライセンス情報が利用できません。", "xpack.actions.urlWhitelistConfigurationError": "target {field} \"{value}\" は Kibana 構成 xpack.actions.whitelistedHosts にはホワイトリスト化されていません。", - "xpack.advancedUiActions.components.actionWizard.changeButton": "変更", - "xpack.advancedUiActions.customizePanelTimeRange.modal.addToPanelButtonTitle": "パネルに追加", - "xpack.advancedUiActions.customizePanelTimeRange.modal.cancelButtonTitle": "キャンセル", - "xpack.advancedUiActions.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel": "時間範囲", - "xpack.advancedUiActions.customizePanelTimeRange.modal.removeButtonTitle": "削除", - "xpack.advancedUiActions.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle": "更新", - "xpack.advancedUiActions.customizeTimeRange.modal.headerTitle": "パネルの時間範囲のカスタマイズ", - "xpack.advancedUiActions.customizeTimeRangeMenuItem.displayName": "時間範囲のカスタマイズ", + "xpack.uiActionsEnhanced.components.actionWizard.changeButton": "変更", + "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle": "パネルに追加", + "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle": "キャンセル", + "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel": "時間範囲", + "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.removeButtonTitle": "削除", + "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle": "更新", + "xpack.uiActionsEnhanced.customizeTimeRange.modal.headerTitle": "パネルの時間範囲のカスタマイズ", + "xpack.uiActionsEnhanced.customizeTimeRangeMenuItem.displayName": "時間範囲のカスタマイズ", "xpack.alerts.alertNavigationRegistry.get.missingNavigationError": "「{consumer}」内のアラートタイプ「{alertType}」のナビゲーションは登録されていません。", "xpack.alerts.alertNavigationRegistry.register.duplicateDefaultError": "「{consumer}」内のデフォルトナビゲーションは既に登録されています。", "xpack.alerts.alertNavigationRegistry.register.duplicateNavigationError": "「{consumer}」内のアラートタイプ「{alertType}」のナビゲーションは既に登録されています。", @@ -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": "テンプレート名は小文字でなければなりません。", @@ -13442,7 +13413,7 @@ "xpack.securitySolution.andOrBadge.and": "AND", "xpack.securitySolution.andOrBadge.or": "OR", "xpack.securitySolution.anomaliesTable.table.anomaliesDescription": "異常", - "xpack.securitySolution.anomaliesTable.table.anomaliesTooltip": "異常表は SIEM グローバル KQL 検索でフィルタリングできません。", + "xpack.securitySolution.anomaliesTable.table.anomaliesTooltip": "異常表は Security グローバル KQL 検索でフィルタリングできません。", "xpack.securitySolution.anomaliesTable.table.showingDescription": "表示中", "xpack.securitySolution.anomaliesTable.table.unit": "{totalCount, plural, =1 {anomaly} other {anomalies}}", "xpack.securitySolution.auditd.abortedAuditStartupDescription": "中断された監査のスタートアップ", @@ -13654,7 +13625,7 @@ "xpack.securitySolution.case.caseView.editConnector": "外部インシデント管理システムを変更", "xpack.securitySolution.case.caseView.editTagsLinkAria": "クリックすると、タグを編集します", "xpack.securitySolution.case.caseView.emailBody": "ケースリファレンス: {caseUrl}", - "xpack.securitySolution.case.caseView.emailSubject": "SIEM ケース - {caseTitle}", + "xpack.securitySolution.case.caseView.emailSubject": "Security ケース - {caseTitle}", "xpack.securitySolution.case.caseView.errorsPushServiceCallOutTitle": "ケースを外部システムにプッシュするには、以下が必要です。", "xpack.securitySolution.case.caseView.fieldRequiredError": "必須フィールド", "xpack.securitySolution.case.caseView.goToDocumentationButton": "ドキュメンテーションを表示", @@ -13695,14 +13666,14 @@ "xpack.securitySolution.case.configureCases.caseClosureOptionsClosedIncident": "新しいインシデントが外部システムで閉じたときにSIEMケースを自動的に閉じる", "xpack.securitySolution.case.configureCases.caseClosureOptionsDesc": "SIEMケースの終了のしかたを定義します。自動ケース終了のためには、外部のインシデント管理システムへの接続を確立する必要がいります。", "xpack.securitySolution.case.configureCases.caseClosureOptionsLabel": "ケース終了オプション", - "xpack.securitySolution.case.configureCases.caseClosureOptionsManual": "SIEM ケースを手動で閉じる", + "xpack.securitySolution.case.configureCases.caseClosureOptionsManual": "Security ケースを手動で閉じる", "xpack.securitySolution.case.configureCases.caseClosureOptionsNewIncident": "新しいインシデントを外部システムにプッシュするときにSIEMケースを自動的に閉じる", "xpack.securitySolution.case.configureCases.caseClosureOptionsTitle": "ケースのクローズ", "xpack.securitySolution.case.configureCases.fieldMappingDesc": "データをサードパーティにプッシュするときにSIEMケースフィールドをマップします。フィールドマッピングのためには、外部のインシデント管理システムへの接続を確立する必要があります。", "xpack.securitySolution.case.configureCases.fieldMappingEditAppend": "末尾に追加", "xpack.securitySolution.case.configureCases.fieldMappingEditNothing": "何もしない", "xpack.securitySolution.case.configureCases.fieldMappingEditOverwrite": "上書き", - "xpack.securitySolution.case.configureCases.fieldMappingFirstCol": "SIEM ケースフィールド", + "xpack.securitySolution.case.configureCases.fieldMappingFirstCol": "Security ケースフィールド", "xpack.securitySolution.case.configureCases.fieldMappingSecondCol": "外部インシデントフィールド", "xpack.securitySolution.case.configureCases.fieldMappingThirdCol": "編集時と更新時", "xpack.securitySolution.case.configureCases.fieldMappingTitle": "フィールドマッピング", @@ -13743,7 +13714,7 @@ "xpack.securitySolution.case.connectors.jira.requiredProjectKeyTextField": "プロジェクトキーが必要です", "xpack.securitySolution.case.connectors.jira.selectMessageText": "JiraでSIEMケースデータを更新するか、新しいインシデントにプッシュ", "xpack.securitySolution.case.connectors.servicenow.actionTypeTitle": "ServiceNow", - "xpack.securitySolution.case.connectors.servicenow.selectMessageText": "ServiceNow で SIEM ケースデータをb\\更新するか、または新しいインシデントにプッシュする", + "xpack.securitySolution.case.connectors.servicenow.selectMessageText": "ServiceNow で Security ケースデータをb\\更新するか、または新しいインシデントにプッシュする", "xpack.securitySolution.case.createCase.descriptionFieldRequiredError": "説明が必要です。", "xpack.securitySolution.case.createCase.fieldTagsHelpText": "このケースの 1 つ以上のカスタム識別タグを入力します。新しいタグを開始するには、各タグの後でEnterを押します。", "xpack.securitySolution.case.createCase.titleFieldRequiredError": "タイトルが必要です。", @@ -13756,7 +13727,7 @@ "xpack.securitySolution.chart.allOthersGroupingLabel": "その他すべて", "xpack.securitySolution.chart.dataAllValuesZerosTitle": "すべての値はゼロを返します", "xpack.securitySolution.chart.dataNotAvailableTitle": "チャートデータが利用できません", - "xpack.securitySolution.chrome.help.appName": "SIEM", + "xpack.securitySolution.chrome.help.appName": "Security", "xpack.securitySolution.chrome.helpMenu.documentation": "SIEMドキュメンテーション", "xpack.securitySolution.chrome.helpMenu.documentation.ecs": "ECSドキュメンテーション", "xpack.securitySolution.clipboard.copied": "コピー完了", @@ -13817,10 +13788,10 @@ "xpack.securitySolution.components.mlPopup.errors.createJobFailureTitle": "ジョブ作成エラー", "xpack.securitySolution.components.mlPopup.errors.startJobFailureTitle": "ジョブ開始エラー", "xpack.securitySolution.components.mlPopup.hooks.errors.indexPatternFetchFailureTitle": "インデックスパターン取得エラー", - "xpack.securitySolution.components.mlPopup.hooks.errors.siemJobFetchFailureTitle": "SIEM ジョブ取得エラー", + "xpack.securitySolution.components.mlPopup.hooks.errors.siemJobFetchFailureTitle": "Security ジョブ取得エラー", "xpack.securitySolution.components.mlPopup.jobsTable.createCustomJobButtonLabel": "カスタムジョブを作成", "xpack.securitySolution.components.mlPopup.jobsTable.jobNameColumn": "ジョブ名", - "xpack.securitySolution.components.mlPopup.jobsTable.noItemsDescription": "SIEM 機械学習ジョブが見つかりませんでした", + "xpack.securitySolution.components.mlPopup.jobsTable.noItemsDescription": "Security 機械学習ジョブが見つかりませんでした", "xpack.securitySolution.components.mlPopup.jobsTable.runJobColumn": "ジョブを実行", "xpack.securitySolution.components.mlPopup.jobsTable.tagsColumn": "グループ", "xpack.securitySolution.components.mlPopup.licenseButtonLabel": "ライセンスの管理", @@ -13830,7 +13801,7 @@ "xpack.securitySolution.components.mlPopup.moduleNotCompatibleTitle": "{incompatibleJobCount} 件が {incompatibleJobCount, plural, =1 {job} other {jobs}} 現在利用できません", "xpack.securitySolution.components.mlPopup.showingLabel": "{filterResultsLength} 件の{filterResultsLength, plural, one {ジョブ} other {ジョブ}}を表示中", "xpack.securitySolution.components.mlPopup.upgradeButtonLabel": "サブスクリプションオプション", - "xpack.securitySolution.components.mlPopup.upgradeDescription": "SIEM の異常検出機能にアクセスするには、ライセンスをプラチナに更新するか、30 日間の無料トライアルを開始するか、AWS、GCP、または Azure で{cloudLink} にサインアップしてください。その後、機械学習ジョブを実行して異常を表示できます。", + "xpack.securitySolution.components.mlPopup.upgradeDescription": "Security の異常検出機能にアクセスするには、ライセンスをプラチナに更新するか、30 日間の無料トライアルを開始するか、AWS、GCP、または Azure で{cloudLink} にサインアップしてください。その後、機械学習ジョブを実行して異常を表示できます。", "xpack.securitySolution.components.mlPopup.upgradeTitle": "E lastic Platinum へのアップグレード", "xpack.securitySolution.components.stepDefineRule.ruleTypeField.subscriptionsLink": "プラチナサブスクリプション", "xpack.securitySolution.containers.anomalies.errorFetchingAnomaliesData": "異常データをクエリできませんでした", @@ -13885,7 +13856,7 @@ "xpack.securitySolution.detectionEngine.components.importRuleModal.importRuleTitle": "ルールのインポート", "xpack.securitySolution.detectionEngine.components.importRuleModal.initialPromptTextDescription": "有効なrules_export.ndjsonファイルを選択するか、ドラッグしてドロップします", "xpack.securitySolution.detectionEngine.components.importRuleModal.overwriteDescription": "保存されたオブジェクトを同じルールIDで自動的に上書きします", - "xpack.securitySolution.detectionEngine.components.importRuleModal.selectRuleDescription": "インポートする SIEM ルール (検出エンジンビューからエクスポートしたもの) を選択します", + "xpack.securitySolution.detectionEngine.components.importRuleModal.selectRuleDescription": "インポートする Security ルール (検出エンジンビューからエクスポートしたもの) を選択します", "xpack.securitySolution.detectionEngine.components.importRuleModal.successfullyImportedRulesTitle": "{totalRules} {totalRules, plural, =1 {ルール} other {ルール}}を正常にインポートしました", "xpack.securitySolution.detectionEngine.createRule. stepScheduleRule.completeWithActivatingTitle": "ルールの作成と有効化", "xpack.securitySolution.detectionEngine.createRule. stepScheduleRule.completeWithoutActivatingTitle": "有効化せずにルールを作成", @@ -13969,7 +13940,7 @@ "xpack.securitySolution.detectionEngine.editRule.saveChangeTitle": "変更を保存", "xpack.securitySolution.detectionEngine.emptyActionPrimary": "セットアップの手順を表示", "xpack.securitySolution.detectionEngine.emptyActionSecondary": "ドキュメントに移動", - "xpack.securitySolution.detectionEngine.emptyTitle": "SIEMアプリケーションの検出エンジンに関連したインデックスがないようです", + "xpack.securitySolution.detectionEngine.emptyTitle": "Securityアプリケーションの検出エンジンに関連したインデックスがないようです", "xpack.securitySolution.detectionEngine.goToDocumentationButton": "ドキュメンテーションを表示", "xpack.securitySolution.detectionEngine.headerPage.pageBadgeLabel": "ベータ", "xpack.securitySolution.detectionEngine.headerPage.pageBadgeTooltip": "検出はまだベータ段階です。Kibana repoで問題やバグを報告して、製品の改善にご協力ください。", @@ -14454,7 +14425,7 @@ "xpack.securitySolution.header.editableTitle.editButtonAria": "クリックすると {title} を編集できます", "xpack.securitySolution.header.editableTitle.save": "保存", "xpack.securitySolution.headerGlobal.buttonAddData": "データの追加", - "xpack.securitySolution.headerGlobal.siem": "SIEM", + "xpack.securitySolution.headerGlobal.siem": "Security", "xpack.securitySolution.headerPage.pageSubtitle": "前回のイベント: {beat}", "xpack.securitySolution.hooks.useAddToTimeline.addedFieldMessage": "{fieldOrValue}をタイムラインに追加しました", "xpack.securitySolution.host.details.architectureLabel": "アーキテクチャー", @@ -14704,7 +14675,7 @@ "xpack.securitySolution.overview.endgameRegistryTitle": "レジストリ", "xpack.securitySolution.overview.endgameSecurityTitle": "セキュリティ", "xpack.securitySolution.overview.eventsTitle": "イベント数", - "xpack.securitySolution.overview.feedbackText": "Elastic SIEM に関するご意見やご提案は、お気軽に {feedback}", + "xpack.securitySolution.overview.feedbackText": "Elastic Security に関するご意見やご提案は、お気軽に {feedback}", "xpack.securitySolution.overview.feedbackText.feedbackLinkText": "フィードバックをオンラインで送信", "xpack.securitySolution.overview.feedbackTitle": "フィードバック", "xpack.securitySolution.overview.filebeatCiscoTitle": "Cisco", @@ -14732,7 +14703,7 @@ "xpack.securitySolution.overview.packetBeatFlowTitle": "フロー", "xpack.securitySolution.overview.packetbeatTLSTitle": "TLS", "xpack.securitySolution.overview.pageSubtitle": "Elastic Stackによるセキュリティ情報とイベント管理", - "xpack.securitySolution.overview.pageTitle": "SIEM", + "xpack.securitySolution.overview.pageTitle": "Security", "xpack.securitySolution.overview.recentCasesSidebarTitle": "最近のケース", "xpack.securitySolution.overview.recentlyCreatedCasesButtonLabel": "最近作成したケース", "xpack.securitySolution.overview.recentTimelinesSidebarTitle": "最近のタイムライン", @@ -14740,7 +14711,7 @@ "xpack.securitySolution.overview.startedText": "セキュリティ情報およびイベント管理(SIEM)へようこそ。はじめに{docs}や{data}をご参照ください。今後の機能に関する情報やチュートリアルは、{siemSolution} ページをお見逃しなく。", "xpack.securitySolution.overview.startedText.dataLinkText": "投入データ", "xpack.securitySolution.overview.startedText.docsLinkText": "ドキュメンテーション", - "xpack.securitySolution.overview.startedText.siemSolutionLinkText": "SIEM ソリューション", + "xpack.securitySolution.overview.startedText.siemSolutionLinkText": "Security ソリューション", "xpack.securitySolution.overview.startedTitle": "はじめて使う", "xpack.securitySolution.overview.topNLabel": "トップ{fieldName}", "xpack.securitySolution.overview.viewAlertsButtonLabel": "アラートを表示", @@ -14840,7 +14811,7 @@ "xpack.securitySolution.timeline.body.renderers.endgame.usingLogonTypeDescription": "ログオンタイプを使用して", "xpack.securitySolution.timeline.body.renderers.endgame.viaDescription": "経由", "xpack.securitySolution.timeline.body.renderers.endgame.withSpecialPrivilegesDescription": "割り当てられた特別な権限", - "xpack.securitySolution.timeline.callOut.unauthorized.message.description": "SIEM アプリケーションでタイムラインを自動保存するにはパーミッションが必要ですが、引き続きタイムラインを使用してセキュリティイベントの検索とフィルタリングを行うことはできます。", + "xpack.securitySolution.timeline.callOut.unauthorized.message.description": "Security アプリケーションでタイムラインを自動保存するにはパーミッションが必要ですが、引き続きタイムラインを使用してセキュリティイベントの検索とフィルタリングを行うことはできます。", "xpack.securitySolution.timeline.categoryTooltip": "カテゴリー", "xpack.securitySolution.timeline.defaultTimelineDescription": "新しいタイムラインを作成するときにデフォルトで提供されるタイムライン。", "xpack.securitySolution.timeline.defaultTimelineTitle": "デフォルトの空白タイムライン", @@ -14909,7 +14880,7 @@ "xpack.securitySolution.timelines.components.importTimelineModal.importTitle": "タイムラインをインポート...", "xpack.securitySolution.timelines.components.importTimelineModal.initialPromptTextDescription": "有効な timelines_export.ndjson ファイルを選択するか、またはドラッグアンドドロップします", "xpack.securitySolution.timelines.components.importTimelineModal.overwriteDescription": "保存されたオブジェクトを同じタイムライン ID で自動的に上書きします", - "xpack.securitySolution.timelines.components.importTimelineModal.selectTimelineDescription": "インポートする SIEM ルール (タイムラインビューからエクスポートしたもの) を選択します", + "xpack.securitySolution.timelines.components.importTimelineModal.selectTimelineDescription": "インポートする Security ルール (タイムラインビューからエクスポートしたもの) を選択します", "xpack.securitySolution.timelines.components.importTimelineModal.successfullyImportedTimelinesTitle": "{totalCount} {totalCount, plural, =1 {タイムライン} other {タイムライン}}のインポートが正常に完了しました", "xpack.securitySolution.timelines.components.tabs.templatesTitle": "テンプレート", "xpack.securitySolution.timelines.components.tabs.timelinesTitle": "タイムライン", @@ -14917,13 +14888,13 @@ "xpack.securitySolution.topN.allEventsSelectLabel": "すべてのイベント", "xpack.securitySolution.topN.closeButtonLabel": "閉じる", "xpack.securitySolution.topN.rawEventsSelectLabel": "未加工イベント", - "xpack.securitySolution.uiSettings.defaultAnomalyScoreDescription": "

機械学習ジョブの異常がこの値を超えると SIEM アプリに表示されます。

有効な値:0 ~ 100。

", + "xpack.securitySolution.uiSettings.defaultAnomalyScoreDescription": "

機械学習ジョブの異常がこの値を超えると Security アプリに表示されます。

有効な値:0 ~ 100。

", "xpack.securitySolution.uiSettings.defaultAnomalyScoreLabel": "デフォルトの異常しきい値", - "xpack.securitySolution.uiSettings.defaultIndexDescription": "

SIEM アプリがイベントを収集する Elasticsearch インデックスのコンマ区切りのリストです。

", + "xpack.securitySolution.uiSettings.defaultIndexDescription": "

Security アプリがイベントを収集する Elasticsearch インデックスのコンマ区切りのリストです。

", "xpack.securitySolution.uiSettings.defaultIndexLabel": "デフォルトのインデックス", - "xpack.securitySolution.uiSettings.defaultRefreshIntervalDescription": "

SIEM 時間フィルターのミリ単位のデフォルトの更新間隔です。

", + "xpack.securitySolution.uiSettings.defaultRefreshIntervalDescription": "

Security 時間フィルターのミリ単位のデフォルトの更新間隔です。

", "xpack.securitySolution.uiSettings.defaultRefreshIntervalLabel": "タイムピッカーの更新間隔", - "xpack.securitySolution.uiSettings.defaultTimeRangeDescription": "

SIEM 時間フィルダーのデフォルトの期間です。

", + "xpack.securitySolution.uiSettings.defaultTimeRangeDescription": "

Security 時間フィルダーのデフォルトの期間です。

", "xpack.securitySolution.uiSettings.defaultTimeRangeLabel": "デフォルトのタイムピッカー", "xpack.securitySolution.uiSettings.enableNewsFeedDescription": "

ニュースフィードを有効にします

", "xpack.securitySolution.uiSettings.enableNewsFeedLabel": "ニュースフィード", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index cee6860a58b5a4..6992d4004e24e6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -496,6 +496,81 @@ "dashboard.topNave.viewConfigDescription": "取消编辑并切换到仅查看模式", "dashboard.urlWasRemovedInSixZeroWarningMessage": "6.0 中已移除 url“dashboard/create”。请更新您的书签。", "dashboard.visitVisualizeAppLinkText": "访问 Visualize 应用", + "data.advancedSettings.courier.batchSearchesText": "禁用时,仪表板面板将分别加载,用户离开时或更新查询时,\n 搜索请求将终止。启用时,仪表板面板将一起加载并加载所有数据,\n 搜索将不会终止。", + "data.advancedSettings.courier.batchSearchesTextDeprecation": "此设置已过时,将在 Kibana 8.0 中移除。", + "data.advancedSettings.courier.batchSearchesTitle": "批处理并发搜索", + "data.advancedSettings.courier.customRequestPreference.requestPreferenceLinkText": "请求首选项", + "data.advancedSettings.courier.customRequestPreferenceText": "将 “{setRequestReferenceSetting} 设置为 {customSettingValue} 时,将使用 “{requestPreferenceLink}”。", + "data.advancedSettings.courier.customRequestPreferenceTitle": "定制请求首选项", + "data.advancedSettings.courier.ignoreFilterText": "此配置增强对包含可视化的仪表板访问不同索引的支持。禁用时,将向所有可视化应用所有筛选。启用时,如果可视化的索引不包含筛选字段,则会为该可视化忽略筛选。", + "data.advancedSettings.courier.ignoreFilterTitle": "忽略筛选", + "data.advancedSettings.courier.maxRequestsText": "控制用于 Kibana 发送的 _msearch 请求的 “{maxRequestsLink}” 设置。设置为 0 可禁用此配置并使用 Elasticsearch 默认值。", + "data.advancedSettings.courier.maxRequestsTitle": "最大并发分片请求数", + "data.advancedSettings.courier.requestPreferenceCustom": "定制", + "data.advancedSettings.courier.requestPreferenceNone": "无", + "data.advancedSettings.courier.requestPreferenceSessionId": "会话 ID", + "data.advancedSettings.courier.requestPreferenceText": "允许您设置用于处理搜索请求的分片。
    \n
  • {sessionId}:仅限在相同分片上执行操作中的所有搜索请求。\n 这有利于在各个请求之间复用分片缓存。
  • \n
  • {custom}:允许您定义自己的首选项。\n 使用 courier:customRequestPreference 定制首选项值。
  • \n
  • {none}:表示不设置首选项。\n 这可能会提供更佳的性能,因此请求可以在所有分片副本上进行分配。\n 不过,结果可能会不一致,因为不同的分片可能处于不同的刷新状态。
  • \n
", + "data.advancedSettings.courier.requestPreferenceTitle": "请求首选项", + "data.advancedSettings.defaultIndexText": "未设置索引时要访问的索引", + "data.advancedSettings.defaultIndexTitle": "默认索引", + "data.advancedSettings.format.bytesFormat.numeralFormatLinkText": "数值格式", + "data.advancedSettings.format.bytesFormatText": "“字节”格式的默认{numeralFormatLink}", + "data.advancedSettings.format.bytesFormatTitle": "字节格式", + "data.advancedSettings.format.currencyFormat.numeralFormatLinkText": "数值格式", + "data.advancedSettings.format.currencyFormatText": "“货币”格式的默认{numeralFormatLink}", + "data.advancedSettings.format.currencyFormatTitle": "货币格式", + "data.advancedSettings.format.defaultTypeMapText": "要默认用于每个字段类型的格式名称的映射。如果未显式提及字段类型,则将使用{defaultFormat}", + "data.advancedSettings.format.defaultTypeMapTitle": "字段类型格式名称", + "data.advancedSettings.format.formattingLocale.numeralLanguageLinkText": "数值语言", + "data.advancedSettings.format.formattingLocaleText": "{numeralLanguageLink}区域设置", + "data.advancedSettings.format.formattingLocaleTitle": "格式区域设置", + "data.advancedSettings.format.numberFormat.numeralFormatLinkText": "数值格式", + "data.advancedSettings.format.numberFormatText": "“数字”格式的默认{numeralFormatLink}", + "data.advancedSettings.format.numberFormatTitle": "数字格式", + "data.advancedSettings.format.percentFormat.numeralFormatLinkText": "数值格式", + "data.advancedSettings.format.percentFormatText": "“百分比”格式的默认{numeralFormatLink}", + "data.advancedSettings.format.percentFormatTitle": "百分比格式", + "data.advancedSettings.histogram.barTargetText": "在日期直方图中使用“auto”时尝试生成大约此数目的条形", + "data.advancedSettings.histogram.barTargetTitle": "目标条形数", + "data.advancedSettings.histogram.maxBarsText": "在日期直方图中不要显示超过该数目的条形", + "data.advancedSettings.histogram.maxBarsTitle": "最大条形数", + "data.advancedSettings.historyLimitText": "在具有历史记录(例如查询输入)的字段中,显示此数目的最近值", + "data.advancedSettings.historyLimitTitle": "历史记录限制", + "data.advancedSettings.indexPatternPlaceholderText": "在“管理 > 索引模式 > 创建索引模式”中“索引模式名称”的占位符。", + "data.advancedSettings.indexPatternPlaceholderTitle": "索引模式占位符", + "data.advancedSettings.pinFiltersText": "筛选是否默认有全局状态(被固定)", + "data.advancedSettings.pinFiltersTitle": "默认固定筛选", + "data.advancedSettings.query.allowWildcardsText": "设置后,将允许 * 用作查询语句的第一个字符。当前仅在查询栏中启用实验性查询功能时才会应用。要在基本 lucene 查询中禁用前导通配符,请使用“{queryStringOptionsPattern}”。", + "data.advancedSettings.query.allowWildcardsTitle": "在查询中允许前导通配符", + "data.advancedSettings.query.queryStringOptions.optionsLinkText": "选项", + "data.advancedSettings.query.queryStringOptionsText": "lucene 查询字符串解析器的{optionsLink}。只有将“{queryLanguage}”设置为 {luceneLanguage} 时才会使用。", + "data.advancedSettings.query.queryStringOptionsTitle": "查询字符串选项", + "data.advancedSettings.searchQueryLanguageKql": "KQL", + "data.advancedSettings.searchQueryLanguageLucene": "Lucene", + "data.advancedSettings.searchQueryLanguageText": "查询栏使用的查询语言。KQL 是专门为 Kibana 打造的新型语言。", + "data.advancedSettings.searchQueryLanguageTitle": "查询语言", + "data.advancedSettings.shortenFieldsText": "缩短长字段,例如,不显示 foo.bar.baz,而显示 f.b.baz", + "data.advancedSettings.shortenFieldsTitle": "缩短字段", + "data.advancedSettings.sortOptions.optionsLinkText": "选项", + "data.advancedSettings.sortOptionsText": "Elasticsearch 排序参数的{optionsLink}", + "data.advancedSettings.sortOptionsTitle": "排序选项", + "data.advancedSettings.suggestFilterValuesText": "将此属性设置 false 以阻止筛选编辑器建议字段的值。", + "data.advancedSettings.suggestFilterValuesTitle": "筛选编辑器建议值", + "data.advancedSettings.timepicker.last15Minutes": "过去 15 分钟", + "data.advancedSettings.timepicker.last1Hour": "过去 1 小时", + "data.advancedSettings.timepicker.last1Year": "过去 1 年", + "data.advancedSettings.timepicker.last24Hours": "过去 24 小时", + "data.advancedSettings.timepicker.last30Days": "过去 30 天", + "data.advancedSettings.timepicker.last30Minutes": "过去 30 分钟", + "data.advancedSettings.timepicker.last7Days": "过去 7 天", + "data.advancedSettings.timepicker.last90Days": "过去 90 天", + "data.advancedSettings.timepicker.quickRanges.acceptedFormatsLinkText": "接受的格式", + "data.advancedSettings.timepicker.quickRangesText": "要在时间选取器的“速选”部分中显示的范围列表。这应该是对象数组,每个对象包含“from”、“to”(请参阅“{acceptedFormatsLink}”)和“display”(要显示的标题)。", + "data.advancedSettings.timepicker.quickRangesTitle": "时间筛选速选范围", + "data.advancedSettings.timepicker.refreshIntervalDefaultsText": "时间筛选的默认刷新时间间隔", + "data.advancedSettings.timepicker.refreshIntervalDefaultsTitle": "时间筛选刷新时间间隔", + "data.advancedSettings.timepicker.thisWeek": "本周", + "data.advancedSettings.timepicker.today": "今日", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} 和 {lt} {to}", "data.common.kql.errors.endOfInputText": "输入结束", "data.common.kql.errors.fieldNameText": "字段名称", @@ -1236,9 +1311,9 @@ "home.addData.metrics.nameTitle": "指标", "home.addData.sampleDataLink": "加载数据集和 Kibana 仪表板", "home.addData.sampleDataTitle": "添加样例数据", - "home.addData.siem.addSiemEventsButtonLabel": "添加事件", - "home.addData.siem.nameDescription": "集中安全事件,以通过即用型可视化实现交互式调查。", - "home.addData.siem.nameTitle": "SIEM", + "home.addData.securitySolution.addSecurityEventsButtonLabel": "添加事件", + "home.addData.securitySolution.nameDescription": "集中安全事件,以通过即用型可视化实现交互式调查。", + "home.addData.securitySolution.nameTitle": "Security", "home.addData.title.observability": "可观测性", "home.addData.title.security": "安全", "home.addData.uploadFileLink": "导入 CSV、NDJSON 或日志文件", @@ -1370,7 +1445,7 @@ "home.tutorial.tabs.loggingTitle": "日志", "home.tutorial.tabs.metricsTitle": "指标", "home.tutorial.tabs.sampleDataTitle": "样例数据", - "home.tutorial.tabs.siemTitle": "SIEM", + "home.tutorial.tabs.securitySolutionTitle": "Security", "home.tutorial.unexpectedStatusCheckStateErrorDescription": "意外的状态检查状态 {statusCheckState}", "home.tutorial.unhandledInstructionTypeErrorDescription": "未处理的指令类型 {visibleInstructions}", "home.tutorials.activemqLogs.artifacts.dashboards.linkLabel": "ActiveMQ 应用程序事件", @@ -1393,7 +1468,7 @@ "home.tutorials.apacheMetrics.longDescription": "Metricbeat 模块 `apache` 从 Apache 2 HTTP 服务器提取内部指标。[了解详情]({learnMoreLink})。", "home.tutorials.apacheMetrics.nameTitle": "Apache 指标", "home.tutorials.apacheMetrics.shortDescription": "从 Apache 2 HTTP 服务器提取内部指标。", - "home.tutorials.auditbeat.artifacts.dashboards.linkLabel": "SIEM 应用", + "home.tutorials.auditbeat.artifacts.dashboards.linkLabel": "Security 应用", "home.tutorials.auditbeat.longDescription": "使用 Auditbeat 从主机收集审计数据。其中包括进程、用户、登录、套接字信息、文件访问等等。[了解详情]({learnMoreLink})。", "home.tutorials.auditbeat.nameTitle": "Auditbeat", "home.tutorials.auditbeat.shortDescription": "从主机收集审计数据。", @@ -1412,7 +1487,7 @@ "home.tutorials.cephMetrics.longDescription": "Metricbeat 模块 `ceph` 从 Ceph 提取内部指标。[了解详情]({learnMoreLink})。", "home.tutorials.cephMetrics.nameTitle": "Ceph 指标", "home.tutorials.cephMetrics.shortDescription": "从 Ceph 服务器提取内部指标。", - "home.tutorials.ciscoLogs.artifacts.dashboards.linkLabel": "SIEM 应用", + "home.tutorials.ciscoLogs.artifacts.dashboards.linkLabel": "Security 应用", "home.tutorials.ciscoLogs.longDescription": "这是用于 Cisco 网络设备日志的模块。当前支持“asa”文件集,该文件集用于通过 Syslog 接收或从文件读取的 Cisco ASA 防火墙日志。[了解详情]({learnMoreLink})。", "home.tutorials.ciscoLogs.nameTitle": "Cisco", "home.tutorials.ciscoLogs.shortDescription": "收集并解析从 Cisco ASA 防火墙接收的日志。", @@ -1742,7 +1817,7 @@ "home.tutorials.elasticsearchMetrics.longDescription": "Metricbeat 模块 `elasticsearch` 从 Elasticsearch 提取内部指标。[了解详情]({learnMoreLink})。", "home.tutorials.elasticsearchMetrics.nameTitle": "Elasticsearch 指标", "home.tutorials.elasticsearchMetrics.shortDescription": "从 Elasticsearch 提取内部指标。", - "home.tutorials.envoyproxyLogs.artifacts.dashboards.linkLabel": "SIEM 应用", + "home.tutorials.envoyproxyLogs.artifacts.dashboards.linkLabel": "Security 应用", "home.tutorials.envoyproxyLogs.longDescription": "这是用于 [Envoy 代理访问日志](https://www.envoyproxy.io/docs/envoy/v1.10.0/configuration/access_log)的 Filebeat 模块。其在 Kubernetes 中既支持独立部署,又支持 Envoy 代理部署。[了解详情]({learnMoreLink})。", "home.tutorials.envoyproxyLogs.nameTitle": "Envoyproxy", "home.tutorials.envoyproxyLogs.shortDescription": "收集并解析从 Envoy 代理接收的日志。", @@ -1773,7 +1848,7 @@ "home.tutorials.iisLogs.longDescription": "Filebeat 模块 `iis` 解析 IIS HTTP 服务器创建的访问和错误日志。[了解详情]({learnMoreLink})。", "home.tutorials.iisLogs.nameTitle": "IIS 日志", "home.tutorials.iisLogs.shortDescription": "收集并解析 IIS HTTP 服务器创建的访问和错误日志。", - "home.tutorials.iptablesLogs.artifacts.dashboards.linkLabel": "SIEM 应用", + "home.tutorials.iptablesLogs.artifacts.dashboards.linkLabel": "Security 应用", "home.tutorials.iptablesLogs.longDescription": "这是用于 iptables 和 ip6tables 日志的模块。其解析在网络上通过 Syslog 或从文件中接收的日志。另外,其识别某些 Ubiquiti 防火墙添加的前缀,该前缀包含规则集名称、规则编号和对流量执行的操作 (allow/deny)。[了解详情]({learnMoreLink})。", "home.tutorials.iptablesLogs.nameTitle": "Iptables / Ubiquiti", "home.tutorials.iptablesLogs.shortDescription": "从 Ubiqiti 防火墙收集并解析 iptables 和 ip6tables 日志。", @@ -1945,7 +2020,7 @@ "home.tutorials.vsphereMetrics.longDescription": "Metricbeat 模块 `vsphere` 从 vSphere 集群提取内部指标。[了解详情]({learnMoreLink})。", "home.tutorials.vsphereMetrics.nameTitle": "vSphere 指标", "home.tutorials.vsphereMetrics.shortDescription": "从 vSphere 提取内部指标。", - "home.tutorials.windowsEventLogs.artifacts.application.label": "SIEM 应用", + "home.tutorials.windowsEventLogs.artifacts.application.label": "Security 应用", "home.tutorials.windowsEventLogs.longDescription": "使用 Winlogbeat 从 Windows 事件日志收集日志。[了解详情]({learnMoreLink})。", "home.tutorials.windowsEventLogs.nameTitle": "Windows 事件日志", "home.tutorials.windowsEventLogs.shortDescription": "从 Windows 事件日志提取日志。", @@ -2050,25 +2125,10 @@ "inspector.requests.statisticsTabLabel": "统计信息", "inspector.title": "检查器", "inspector.view": "视图:{viewName}", - "kbn.advancedSettings.courier.batchSearchesText": "禁用时,仪表板面板将分别加载,用户离开时或更新查询时,\n 搜索请求将终止。启用时,仪表板面板将一起加载并加载所有数据,\n 搜索将不会终止。", - "kbn.advancedSettings.courier.batchSearchesTextDeprecation": "此设置已过时,将在 Kibana 8.0 中移除。", - "kbn.advancedSettings.courier.batchSearchesTitle": "批处理并发搜索", - "kbn.advancedSettings.courier.customRequestPreference.requestPreferenceLinkText": "请求首选项", - "kbn.advancedSettings.courier.customRequestPreferenceText": "将 “{setRequestReferenceSetting} 设置为 {customSettingValue} 时,将使用 “{requestPreferenceLink}”。", - "kbn.advancedSettings.courier.customRequestPreferenceTitle": "定制请求首选项", - "kbn.advancedSettings.courier.ignoreFilterText": "此配置增强对包含可视化的仪表板访问不同索引的支持。禁用时,将向所有可视化应用所有筛选。启用时,如果可视化的索引不包含筛选字段,则会为该可视化忽略筛选。", - "kbn.advancedSettings.courier.ignoreFilterTitle": "忽略筛选", - "kbn.advancedSettings.courier.maxRequestsText": "控制用于 Kibana 发送的 _msearch 请求的 “{maxRequestsLink}” 设置。设置为 0 可禁用此配置并使用 Elasticsearch 默认值。", - "kbn.advancedSettings.courier.maxRequestsTitle": "最大并发分片请求数", - "kbn.advancedSettings.courier.requestPreferenceCustom": "定制", - "kbn.advancedSettings.courier.requestPreferenceNone": "无", - "kbn.advancedSettings.courier.requestPreferenceSessionId": "会话 ID", - "kbn.advancedSettings.courier.requestPreferenceText": "允许您设置用于处理搜索请求的分片。
    \n
  • {sessionId}:仅限在相同分片上执行操作中的所有搜索请求。\n 这有利于在各个请求之间复用分片缓存。
  • \n
  • {custom}:允许您定义自己的首选项。\n 使用 courier:customRequestPreference 定制首选项值。
  • \n
  • {none}:表示不设置首选项。\n 这可能会提供更佳的性能,因此请求可以在所有分片副本上进行分配。\n 不过,结果可能会不一致,因为不同的分片可能处于不同的刷新状态。
  • \n
", - "kbn.advancedSettings.courier.requestPreferenceTitle": "请求首选项", - "kbn.advancedSettings.csv.quoteValuesText": "在 csv 导出中是否应使用引号引起值?", - "kbn.advancedSettings.csv.quoteValuesTitle": "使用引号引起 CSV 值", - "kbn.advancedSettings.csv.separatorText": "使用此字符串分隔导出的值", - "kbn.advancedSettings.csv.separatorTitle": "CSV 分隔符", + "share.advancedSettings.csv.quoteValuesText": "在 csv 导出中是否应使用引号引起值?", + "share.advancedSettings.csv.quoteValuesTitle": "使用引号引起 CSV 值", + "share.advancedSettings.csv.separatorText": "使用此字符串分隔导出的值", + "share.advancedSettings.csv.separatorTitle": "CSV 分隔符", "kbn.advancedSettings.darkModeText": "为 Kibana UI 启用深色模式需要刷新页面,才能应用设置。", "kbn.advancedSettings.darkModeTitle": "深色模式", "kbn.advancedSettings.dateFormat.dayOfWeekText": "一周从哪一日开始?", @@ -2084,38 +2144,11 @@ "kbn.advancedSettings.dateNanosFormatText": "用于 Elasticsearch 的 {dateNanosLink} 数据类型", "kbn.advancedSettings.dateNanosFormatTitle": "纳秒格式的日期", "kbn.advancedSettings.dateNanosLinkTitle": "date_nanos", - "kbn.advancedSettings.defaultIndexText": "未设置索引时要访问的索引", - "kbn.advancedSettings.defaultIndexTitle": "默认索引", "kbn.advancedSettings.defaultRoute.defaultRouteIsRelativeValidationMessage": "必须是相对 URL。", "kbn.advancedSettings.defaultRoute.defaultRouteText": "此设置指定打开 Kibana 时的默认路由。您可以使用此设置修改打开 Kibana 时的登陆页面。路由必须是相对 URL。", "kbn.advancedSettings.defaultRoute.defaultRouteTitle": "默认路由", "kbn.advancedSettings.disableAnimationsText": "在 Kibana UI 中关闭所有没有必要的动画。刷新页面以应用更改。", "kbn.advancedSettings.disableAnimationsTitle": "禁用动画", - "kbn.advancedSettings.format.bytesFormat.numeralFormatLinkText": "数值格式", - "kbn.advancedSettings.format.bytesFormatText": "“字节”格式的默认{numeralFormatLink}", - "kbn.advancedSettings.format.bytesFormatTitle": "字节格式", - "kbn.advancedSettings.format.currencyFormat.numeralFormatLinkText": "数值格式", - "kbn.advancedSettings.format.currencyFormatText": "“货币”格式的默认{numeralFormatLink}", - "kbn.advancedSettings.format.currencyFormatTitle": "货币格式", - "kbn.advancedSettings.format.defaultTypeMapText": "要默认用于每个字段类型的格式名称的映射。如果未显式提及字段类型,则将使用{defaultFormat}", - "kbn.advancedSettings.format.defaultTypeMapTitle": "字段类型格式名称", - "kbn.advancedSettings.format.formattingLocale.numeralLanguageLinkText": "数值语言", - "kbn.advancedSettings.format.formattingLocaleText": "{numeralLanguageLink}区域设置", - "kbn.advancedSettings.format.formattingLocaleTitle": "格式区域设置", - "kbn.advancedSettings.format.numberFormat.numeralFormatLinkText": "数值格式", - "kbn.advancedSettings.format.numberFormatText": "“数字”格式的默认{numeralFormatLink}", - "kbn.advancedSettings.format.numberFormatTitle": "数字格式", - "kbn.advancedSettings.format.percentFormat.numeralFormatLinkText": "数值格式", - "kbn.advancedSettings.format.percentFormatText": "“百分比”格式的默认{numeralFormatLink}", - "kbn.advancedSettings.format.percentFormatTitle": "百分比格式", - "kbn.advancedSettings.histogram.barTargetText": "在日期直方图中使用“auto”时尝试生成大约此数目的条形", - "kbn.advancedSettings.histogram.barTargetTitle": "目标条形数", - "kbn.advancedSettings.histogram.maxBarsText": "在日期直方图中不要显示超过该数目的条形", - "kbn.advancedSettings.histogram.maxBarsTitle": "最大条形数", - "kbn.advancedSettings.historyLimitText": "在具有历史记录(例如查询输入)的字段中,显示此数目的最近值", - "kbn.advancedSettings.historyLimitTitle": "历史记录限制", - "kbn.advancedSettings.indexPatternPlaceholderText": "在“管理 > 索引模式 > 创建索引模式”中“索引模式名称”的占位符。", - "kbn.advancedSettings.indexPatternPlaceholderTitle": "索引模式占位符", "kbn.advancedSettings.maxCellHeightText": "表中单元格应占用的最大高度。设置为 0 可禁用截短", "kbn.advancedSettings.maxCellHeightTitle": "最大表单元格高度", "kbn.advancedSettings.notifications.banner.markdownLinkText": "Markdown 受支持", @@ -2133,43 +2166,10 @@ "kbn.advancedSettings.pageNavigationLegacy": "旧版", "kbn.advancedSettings.pageNavigationModern": "现代", "kbn.advancedSettings.pageNavigationName": "侧边导航样式", - "kbn.advancedSettings.pinFiltersText": "筛选是否默认有全局状态(被固定)", - "kbn.advancedSettings.pinFiltersTitle": "默认固定筛选", - "kbn.advancedSettings.query.allowWildcardsText": "设置后,将允许 * 用作查询语句的第一个字符。当前仅在查询栏中启用实验性查询功能时才会应用。要在基本 lucene 查询中禁用前导通配符,请使用“{queryStringOptionsPattern}”。", - "kbn.advancedSettings.query.allowWildcardsTitle": "在查询中允许前导通配符", - "kbn.advancedSettings.query.queryStringOptions.optionsLinkText": "选项", - "kbn.advancedSettings.query.queryStringOptionsText": "lucene 查询字符串解析器的{optionsLink}。只有将“{queryLanguage}”设置为 {luceneLanguage} 时才会使用。", - "kbn.advancedSettings.query.queryStringOptionsTitle": "查询字符串选项", - "kbn.advancedSettings.searchQueryLanguageKql": "KQL", - "kbn.advancedSettings.searchQueryLanguageLucene": "Lucene", - "kbn.advancedSettings.searchQueryLanguageText": "查询栏使用的查询语言。KQL 是专门为 Kibana 打造的新型语言。", - "kbn.advancedSettings.searchQueryLanguageTitle": "查询语言", - "kbn.advancedSettings.shortenFieldsText": "缩短长字段,例如,不显示 foo.bar.baz,而显示 f.b.baz", - "kbn.advancedSettings.shortenFieldsTitle": "缩短字段", - "kbn.advancedSettings.sortOptions.optionsLinkText": "选项", - "kbn.advancedSettings.sortOptionsText": "Elasticsearch 排序参数的{optionsLink}", - "kbn.advancedSettings.sortOptionsTitle": "排序选项", "kbn.advancedSettings.storeUrlText": "URL 有时会变得过长,以使得某些浏览器无法处理。为此,我们正在测试将 URL 的各个组成部分存储在会话存储中是否会有帮助。请告知我们这样做的效果!", "kbn.advancedSettings.storeUrlTitle": "将 URL 存储在会话存储中", - "kbn.advancedSettings.suggestFilterValuesText": "将此属性设置 false 以阻止筛选编辑器建议字段的值。", - "kbn.advancedSettings.suggestFilterValuesTitle": "筛选编辑器建议值", - "kbn.advancedSettings.timepicker.last15Minutes": "过去 15 分钟", - "kbn.advancedSettings.timepicker.last1Hour": "过去 1 小时", - "kbn.advancedSettings.timepicker.last1Year": "过去 1 年", - "kbn.advancedSettings.timepicker.last24Hours": "过去 24 小时", - "kbn.advancedSettings.timepicker.last30Days": "过去 30 天", - "kbn.advancedSettings.timepicker.last30Minutes": "过去 30 分钟", - "kbn.advancedSettings.timepicker.last7Days": "过去 7 天", - "kbn.advancedSettings.timepicker.last90Days": "过去 90 天", - "kbn.advancedSettings.timepicker.quickRanges.acceptedFormatsLinkText": "接受的格式", - "kbn.advancedSettings.timepicker.quickRangesText": "要在时间选取器的“速选”部分中显示的范围列表。这应该是对象数组,每个对象包含“from”、“to”(请参阅“{acceptedFormatsLink}”)和“display”(要显示的标题)。", - "kbn.advancedSettings.timepicker.quickRangesTitle": "时间筛选速选范围", - "kbn.advancedSettings.timepicker.refreshIntervalDefaultsText": "时间筛选的默认刷新时间间隔", - "kbn.advancedSettings.timepicker.refreshIntervalDefaultsTitle": "时间筛选刷新时间间隔", - "kbn.advancedSettings.timepicker.thisWeek": "本周", "kbn.advancedSettings.timepicker.timeDefaultsText": "未使用时间筛选启动 Kibana 时要使用的时间筛选选择", "kbn.advancedSettings.timepicker.timeDefaultsTitle": "时间筛选默认值", - "kbn.advancedSettings.timepicker.today": "今日", "kbn.advancedSettings.visualization.showRegionMapWarningsText": "词无法联接到地图上的形状时,区域地图是否显示警告。", "kbn.advancedSettings.visualization.showRegionMapWarningsTitle": "显示区域地图警告", "kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "单元格维度的解释", @@ -2178,8 +2178,8 @@ "kbn.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText": "属性", "kbn.advancedSettings.visualization.tileMap.wmsDefaultsText": "坐标地图中 WMS 地图服务器支持的默认{propertiesLink}", "kbn.advancedSettings.visualization.tileMap.wmsDefaultsTitle": "默认 WMS 属性", - "kbn.advancedSettings.visualizeEnableLabsText": "允许用户创建、查看和编辑实验性可视化。如果禁用,\n 仅被视为生产就绪的可视化可供用户使用。", - "kbn.advancedSettings.visualizeEnableLabsTitle": "启用实验性可视化", + "visualizations.advancedSettings.visualizeEnableLabsText": "允许用户创建、查看和编辑实验性可视化。如果禁用,\n 仅被视为生产就绪的可视化可供用户使用。", + "visualizations.advancedSettings.visualizeEnableLabsTitle": "启用实验性可视化", "kibana_legacy.bigUrlWarningNotificationMessage": "在{advancedSettingsLink}中启用“{storeInSessionStorageParam}”选项或简化屏幕视觉效果。", "kibana_legacy.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高级设置", "kibana_legacy.bigUrlWarningNotificationTitle": "URL 过长,Kibana 可能无法工作", @@ -3974,14 +3974,14 @@ "xpack.actions.serverSideErrors.unavailableLicenseErrorMessage": "操作类型 {actionTypeId} 已禁用,因为许可证信息当前不可用。", "xpack.actions.serverSideErrors.unavailableLicenseInformationErrorMessage": "操作不可用 - 许可信息当前不可用。", "xpack.actions.urlWhitelistConfigurationError": "目标 {field}“{value}”在 Kibana 配置 xpack.actions.whitelistedHosts 中未列入白名单", - "xpack.advancedUiActions.components.actionWizard.changeButton": "更改", - "xpack.advancedUiActions.customizePanelTimeRange.modal.addToPanelButtonTitle": "添加到面板", - "xpack.advancedUiActions.customizePanelTimeRange.modal.cancelButtonTitle": "取消", - "xpack.advancedUiActions.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel": "时间范围", - "xpack.advancedUiActions.customizePanelTimeRange.modal.removeButtonTitle": "删除", - "xpack.advancedUiActions.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle": "更新", - "xpack.advancedUiActions.customizeTimeRange.modal.headerTitle": "定制面板时间范围", - "xpack.advancedUiActions.customizeTimeRangeMenuItem.displayName": "定制时间范围", + "xpack.uiActionsEnhanced.components.actionWizard.changeButton": "更改", + "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle": "添加到面板", + "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle": "取消", + "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel": "时间范围", + "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.removeButtonTitle": "删除", + "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle": "更新", + "xpack.uiActionsEnhanced.customizeTimeRange.modal.headerTitle": "定制面板时间范围", + "xpack.uiActionsEnhanced.customizeTimeRangeMenuItem.displayName": "定制时间范围", "xpack.alerts.alertNavigationRegistry.get.missingNavigationError": "在“{consumer}”内针对告警类型“{alertType}”的导航未注册。", "xpack.alerts.alertNavigationRegistry.register.duplicateDefaultError": "“{consumer}”内的默认导航已注册。", "xpack.alerts.alertNavigationRegistry.register.duplicateNavigationError": "在“{consumer}”内针对告警类型“{alertType}”的导航已注册。", @@ -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": "模板名称必须小写。", @@ -13447,7 +13418,7 @@ "xpack.securitySolution.andOrBadge.and": "AND", "xpack.securitySolution.andOrBadge.or": "OR", "xpack.securitySolution.anomaliesTable.table.anomaliesDescription": "异常", - "xpack.securitySolution.anomaliesTable.table.anomaliesTooltip": "异常表无法通过 SIEM 全局 KQL 搜索进行筛选。", + "xpack.securitySolution.anomaliesTable.table.anomaliesTooltip": "异常表无法通过 Security 全局 KQL 搜索进行筛选。", "xpack.securitySolution.anomaliesTable.table.showingDescription": "显示", "xpack.securitySolution.anomaliesTable.table.unit": "{totalCount, plural, =1 {个异常} other {个异常}}", "xpack.securitySolution.auditd.abortedAuditStartupDescription": "已中止审计启动", @@ -13659,7 +13630,7 @@ "xpack.securitySolution.case.caseView.editConnector": "更改外部事件管理系统", "xpack.securitySolution.case.caseView.editTagsLinkAria": "单击可编辑标记", "xpack.securitySolution.case.caseView.emailBody": "案例参考:{caseUrl}", - "xpack.securitySolution.case.caseView.emailSubject": "SIEM 案例 - {caseTitle}", + "xpack.securitySolution.case.caseView.emailSubject": "Security 案例 - {caseTitle}", "xpack.securitySolution.case.caseView.errorsPushServiceCallOutTitle": "要将案例发送到外部系统,您需要:", "xpack.securitySolution.case.caseView.fieldRequiredError": "必填字段", "xpack.securitySolution.case.caseView.goToDocumentationButton": "查看文档", @@ -13697,22 +13668,22 @@ "xpack.securitySolution.case.configure.successSaveToast": "已保存外部连接设置", "xpack.securitySolution.case.configureCases.addNewConnector": "添加新连接器", "xpack.securitySolution.case.configureCases.cancelButton": "取消", - "xpack.securitySolution.case.configureCases.caseClosureOptionsClosedIncident": "在外部系统中关闭事件时自动关闭 SIEM 案例", - "xpack.securitySolution.case.configureCases.caseClosureOptionsDesc": "定义关闭 SIEM 案例的方式。要自动关闭案例,需要与外部事件管理系统建立连接。", + "xpack.securitySolution.case.configureCases.caseClosureOptionsClosedIncident": "在外部系统中关闭事件时自动关闭 Security 案例", + "xpack.securitySolution.case.configureCases.caseClosureOptionsDesc": "定义关闭 Security 案例的方式。要自动关闭案例,需要与外部事件管理系统建立连接。", "xpack.securitySolution.case.configureCases.caseClosureOptionsLabel": "案例关闭选项", - "xpack.securitySolution.case.configureCases.caseClosureOptionsManual": "手动关闭 SIEM 案例", - "xpack.securitySolution.case.configureCases.caseClosureOptionsNewIncident": "将新事件推送到外部系统时自动关闭 SIEM 案例", + "xpack.securitySolution.case.configureCases.caseClosureOptionsManual": "手动关闭 Security 案例", + "xpack.securitySolution.case.configureCases.caseClosureOptionsNewIncident": "将新事件推送到外部系统时自动关闭 Security 案例", "xpack.securitySolution.case.configureCases.caseClosureOptionsTitle": "案例关闭", - "xpack.securitySolution.case.configureCases.fieldMappingDesc": "将数据推送到第三方时映射 SIEM 案例字段。字段映射需要与外部事件管理系统建立连接。", + "xpack.securitySolution.case.configureCases.fieldMappingDesc": "将数据推送到第三方时映射 Security 案例字段。字段映射需要与外部事件管理系统建立连接。", "xpack.securitySolution.case.configureCases.fieldMappingEditAppend": "追加", "xpack.securitySolution.case.configureCases.fieldMappingEditNothing": "无内容", "xpack.securitySolution.case.configureCases.fieldMappingEditOverwrite": "覆盖", - "xpack.securitySolution.case.configureCases.fieldMappingFirstCol": "SIEM 案例字段", + "xpack.securitySolution.case.configureCases.fieldMappingFirstCol": "Security 案例字段", "xpack.securitySolution.case.configureCases.fieldMappingSecondCol": "外部事件字段", "xpack.securitySolution.case.configureCases.fieldMappingThirdCol": "编辑和更新时", "xpack.securitySolution.case.configureCases.fieldMappingTitle": "字段映射", "xpack.securitySolution.case.configureCases.headerTitle": "配置案例", - "xpack.securitySolution.case.configureCases.incidentManagementSystemDesc": "您可能会根据需要将 SIEM 案例连接到选择的外部事件管理系统。这将允许您将案例数据作为事件推送到所选第三方系统。", + "xpack.securitySolution.case.configureCases.incidentManagementSystemDesc": "您可能会根据需要将 Security 案例连接到选择的外部事件管理系统。这将允许您将案例数据作为事件推送到所选第三方系统。", "xpack.securitySolution.case.configureCases.incidentManagementSystemLabel": "事件管理系统", "xpack.securitySolution.case.configureCases.incidentManagementSystemTitle": "连接到外部事件管理系统", "xpack.securitySolution.case.configureCases.mappingFieldComments": "注释", @@ -13746,9 +13717,9 @@ "xpack.securitySolution.case.connectors.jira.actionTypeTitle": "Jira", "xpack.securitySolution.case.connectors.jira.projectKey": "项目键", "xpack.securitySolution.case.connectors.jira.requiredProjectKeyTextField": "项目键必填。", - "xpack.securitySolution.case.connectors.jira.selectMessageText": "将 SIEM 案例数据推送或更新到 Jira 中的新问题", + "xpack.securitySolution.case.connectors.jira.selectMessageText": "将 Security 案例数据推送或更新到 Jira 中的新问题", "xpack.securitySolution.case.connectors.servicenow.actionTypeTitle": "ServiceNow", - "xpack.securitySolution.case.connectors.servicenow.selectMessageText": "将 SIEM 案例数据推送或更新到 ServiceNow 中的新事件", + "xpack.securitySolution.case.connectors.servicenow.selectMessageText": "将 Security 案例数据推送或更新到 ServiceNow 中的新事件", "xpack.securitySolution.case.createCase.descriptionFieldRequiredError": "描述必填。", "xpack.securitySolution.case.createCase.fieldTagsHelpText": "为此案例键入一个或多个定制识别标记。在每个标记后按 Enter 键可开始新的标记。", "xpack.securitySolution.case.createCase.titleFieldRequiredError": "标题必填。", @@ -13761,8 +13732,8 @@ "xpack.securitySolution.chart.allOthersGroupingLabel": "所有其他", "xpack.securitySolution.chart.dataAllValuesZerosTitle": "所有值返回零", "xpack.securitySolution.chart.dataNotAvailableTitle": "图表数据不可用", - "xpack.securitySolution.chrome.help.appName": "SIEM", - "xpack.securitySolution.chrome.helpMenu.documentation": "SIEM 文档", + "xpack.securitySolution.chrome.help.appName": "Security", + "xpack.securitySolution.chrome.helpMenu.documentation": "Security 文档", "xpack.securitySolution.chrome.helpMenu.documentation.ecs": "ECS 文档", "xpack.securitySolution.clipboard.copied": "已复制", "xpack.securitySolution.clipboard.copy": "复制", @@ -13779,7 +13750,7 @@ "xpack.securitySolution.components.embeddables.embeddedMap.serverLayerLabel": "服务器点", "xpack.securitySolution.components.embeddables.embeddedMap.sourceLayerLabel": "源点", "xpack.securitySolution.components.embeddables.indexPatternsMissingPrompt.errorButtonLabel": "配置索引模式", - "xpack.securitySolution.components.embeddables.indexPatternsMissingPrompt.errorDescription1": "要显示地图数据,必须使用匹配的全局模式定义 SIEM 索引 ({defaultIndex}) 和 Kibana 索引模式。使用 {beats} 时,可以在主机上运行 {setup} 命令,以自动创建索引模式。例如:{example}。", + "xpack.securitySolution.components.embeddables.indexPatternsMissingPrompt.errorDescription1": "要显示地图数据,必须使用匹配的全局模式定义 Security 索引 ({defaultIndex}) 和 Kibana 索引模式。使用 {beats} 时,可以在主机上运行 {setup} 命令,以自动创建索引模式。例如:{example}。", "xpack.securitySolution.components.embeddables.indexPatternsMissingPrompt.errorDescription2": "还可以在 Kibana 中配置索引模式。", "xpack.securitySolution.components.embeddables.indexPatternsMissingPrompt.errorTitle": "未配置所需的索引模式", "xpack.securitySolution.components.embeddables.mapToolTip.errorTitle": "加载地图特征时出错", @@ -13817,15 +13788,15 @@ "xpack.securitySolution.components.mlPopover.jobsTable.filters.searchFilterPlaceholder": "例如 rare_process_linux", "xpack.securitySolution.components.mlPopover.jobsTable.filters.showAllJobsLabel": "Elastic 作业", "xpack.securitySolution.components.mlPopover.jobsTable.filters.showSiemJobsLabel": "定制作业", - "xpack.securitySolution.components.mlPopup.anomalyDetectionDescription": "运行下面的任意 Machine Learning 作业以准备创建将产生已检测异常信号的信号检测规则以及查看整个 SIEM 应用程序内的异常事件。我们提供一系列常见检测作业帮助您入门。如果您希望添加自己的定制 ML 作业,请从 {machineLearning} 应用程序中创建并将它们添加到“SIEM”组。", + "xpack.securitySolution.components.mlPopup.anomalyDetectionDescription": "运行下面的任意 Machine Learning 作业以准备创建将产生已检测异常信号的信号检测规则以及查看整个 Security 应用程序内的异常事件。我们提供一系列常见检测作业帮助您入门。如果您希望添加自己的定制 ML 作业,请从 {machineLearning} 应用程序中创建并将它们添加到“SIEM”组。", "xpack.securitySolution.components.mlPopup.cloudLink": "云部署", "xpack.securitySolution.components.mlPopup.errors.createJobFailureTitle": "创建作业失败", "xpack.securitySolution.components.mlPopup.errors.startJobFailureTitle": "启动作业失败", "xpack.securitySolution.components.mlPopup.hooks.errors.indexPatternFetchFailureTitle": "索引模式提取失败", - "xpack.securitySolution.components.mlPopup.hooks.errors.siemJobFetchFailureTitle": "SIEM 作业提取失败", + "xpack.securitySolution.components.mlPopup.hooks.errors.siemJobFetchFailureTitle": "Security 作业提取失败", "xpack.securitySolution.components.mlPopup.jobsTable.createCustomJobButtonLabel": "创建定制作业", "xpack.securitySolution.components.mlPopup.jobsTable.jobNameColumn": "作业名称", - "xpack.securitySolution.components.mlPopup.jobsTable.noItemsDescription": "未找到任何 SIEM Machine Learning 作业", + "xpack.securitySolution.components.mlPopup.jobsTable.noItemsDescription": "未找到任何 Security Machine Learning 作业", "xpack.securitySolution.components.mlPopup.jobsTable.runJobColumn": "运行作业", "xpack.securitySolution.components.mlPopup.jobsTable.tagsColumn": "组", "xpack.securitySolution.components.mlPopup.licenseButtonLabel": "管理许可", @@ -13835,7 +13806,7 @@ "xpack.securitySolution.components.mlPopup.moduleNotCompatibleTitle": "{incompatibleJobCount} {incompatibleJobCount, plural, =1 {job} other {jobs}}当前不可用。", "xpack.securitySolution.components.mlPopup.showingLabel": "显示:{filterResultsLength} 个 {filterResultsLength, plural, one {作业} other {作业}}", "xpack.securitySolution.components.mlPopup.upgradeButtonLabel": "订阅选项", - "xpack.securitySolution.components.mlPopup.upgradeDescription": "要访问 SIEM 的异常检测功能,必须将您的许可更新到白金级、开始 30 天免费试用或在 AWS、GCP 或 Azurein 实施{cloudLink}。然后便可以运行 Machine Learning 作业并查看异常。", + "xpack.securitySolution.components.mlPopup.upgradeDescription": "要访问 Security 的异常检测功能,必须将您的许可更新到白金级、开始 30 天免费试用或在 AWS、GCP 或 Azurein 实施{cloudLink}。然后便可以运行 Machine Learning 作业并查看异常。", "xpack.securitySolution.components.mlPopup.upgradeTitle": "升级 Elastic 白金级", "xpack.securitySolution.components.stepDefineRule.ruleTypeField.subscriptionsLink": "白金级订阅", "xpack.securitySolution.containers.anomalies.errorFetchingAnomaliesData": "无法查询异常数据", @@ -13890,7 +13861,7 @@ "xpack.securitySolution.detectionEngine.components.importRuleModal.importRuleTitle": "导入规则", "xpack.securitySolution.detectionEngine.components.importRuleModal.initialPromptTextDescription": "选择或拖放有效的 rules_export.ndjson 文件", "xpack.securitySolution.detectionEngine.components.importRuleModal.overwriteDescription": "自动覆盖具有相同规则 ID 的已保存对象", - "xpack.securitySolution.detectionEngine.components.importRuleModal.selectRuleDescription": "选择要导入的 SIEM 规则(如从检测引擎视图导出的)", + "xpack.securitySolution.detectionEngine.components.importRuleModal.selectRuleDescription": "选择要导入的 Security 规则(如从检测引擎视图导出的)", "xpack.securitySolution.detectionEngine.components.importRuleModal.successfullyImportedRulesTitle": "已成功导入 {totalRules} 个{totalRules, plural, =1 {规则} other {规则}}", "xpack.securitySolution.detectionEngine.createRule. stepScheduleRule.completeWithActivatingTitle": "创建并激活规则", "xpack.securitySolution.detectionEngine.createRule. stepScheduleRule.completeWithoutActivatingTitle": "创建规则但不激活", @@ -13937,8 +13908,8 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.importTimelineModalTitle": "从已保存时间线导入查询", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.importTimelineQueryButton": "从已保存时间线导入查询", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.indicesCustomDescription": "提供定制的索引列表", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.indicesFromConfigDescription": "使用 SIEM 高级设置的 Elasticsearch 索引", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.indicesHelperDescription": "输入要运行此规则的 Elasticsearch 索引的模式。默认情况下,将包括 SIEM 高级设置中定义的索引模式。", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.indicesFromConfigDescription": "使用 Security 高级设置的 Elasticsearch 索引", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.indicesHelperDescription": "输入要运行此规则的 Elasticsearch 索引的模式。默认情况下,将包括 Security 高级设置中定义的索引模式。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.machineLearningJobIdHelpText": "我们提供若干可让您入门的常规作业。要添加自己的定制规则,在 {machineLearning} 应用程序中请将一组“siem”分配给这些作业,以使它们显示在此处。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.machineLearningJobIdRequired": "Machine Learning 作业必填。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.mlEnableJobWarningTitle": "此 ML 作业当前未运行。在激活此规则之前请通过“ML 作业设置”设置此作业以使其运行。", @@ -13974,7 +13945,7 @@ "xpack.securitySolution.detectionEngine.editRule.saveChangeTitle": "保存更改", "xpack.securitySolution.detectionEngine.emptyActionPrimary": "查看设置说明", "xpack.securitySolution.detectionEngine.emptyActionSecondary": "前往文档", - "xpack.securitySolution.detectionEngine.emptyTitle": "似乎您没有与 SIEM 应用程序的检测引擎相关的索引", + "xpack.securitySolution.detectionEngine.emptyTitle": "似乎您没有与 Security 应用程序的检测引擎相关的索引", "xpack.securitySolution.detectionEngine.goToDocumentationButton": "查看文档", "xpack.securitySolution.detectionEngine.headerPage.pageBadgeLabel": "公测版", "xpack.securitySolution.detectionEngine.headerPage.pageBadgeTooltip": "“检测”仍为公测版。请通过在 Kibana 存储库中报告问题或错误,帮助我们改进产品。", @@ -14350,7 +14321,7 @@ "xpack.securitySolution.detectionEngine.rules.optionalFieldDescription": "可选", "xpack.securitySolution.detectionEngine.rules.pageTitle": "信号检测规则", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.createOwnRuletButton": "创建自己的规则", - "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage": "Elastic SIEM 提供预构建检测规则,它们运行在后台并在条件满足时创建信号。默认情况下,所有预构建规则处于禁用状态,请选择您要激活的规则。", + "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage": "Elastic Security 提供预构建检测规则,它们运行在后台并在条件满足时创建信号。默认情况下,所有预构建规则处于禁用状态,请选择您要激活的规则。", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptTitle": "加载 Elastic 预构建检测规则", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.loadPreBuiltButton": "加载预构建检测规则", "xpack.securitySolution.detectionEngine.rules.releaseNotesHelp": "发行说明", @@ -14459,7 +14430,7 @@ "xpack.securitySolution.header.editableTitle.editButtonAria": "通过单击,可以编辑 {title}", "xpack.securitySolution.header.editableTitle.save": "保存", "xpack.securitySolution.headerGlobal.buttonAddData": "添加数据", - "xpack.securitySolution.headerGlobal.siem": "SIEM", + "xpack.securitySolution.headerGlobal.siem": "Security", "xpack.securitySolution.headerPage.pageSubtitle": "最后事件:{beat}", "xpack.securitySolution.hooks.useAddToTimeline.addedFieldMessage": "已将 {fieldOrValue} 添加到时间线", "xpack.securitySolution.host.details.architectureLabel": "架构", @@ -14647,7 +14618,7 @@ "xpack.securitySolution.networkTopNFlowTable.rows": "{numRows} {numRows, plural, =0 {行} =1 {行} other {行}}", "xpack.securitySolution.networkTopNFlowTable.sourceIps": "源 IP", "xpack.securitySolution.networkTopNFlowTable.unit": "{totalCount, plural, =1 {个 IP} other {个 IP}}", - "xpack.securitySolution.newsFeed.advancedSettingsLinkTitle": "SIEM 高级设置", + "xpack.securitySolution.newsFeed.advancedSettingsLinkTitle": "Security 高级设置", "xpack.securitySolution.newsFeed.noNewsMessage": "您当前的新闻源 URL 未返回最近的新闻。要更新 URL 或禁用安全新闻,您可以通过", "xpack.securitySolution.notes.addANotePlaceholder": "添加备注", "xpack.securitySolution.notes.addedANoteLabel": "已添加备注", @@ -14709,7 +14680,7 @@ "xpack.securitySolution.overview.endgameRegistryTitle": "注册表", "xpack.securitySolution.overview.endgameSecurityTitle": "安全性", "xpack.securitySolution.overview.eventsTitle": "事件计数", - "xpack.securitySolution.overview.feedbackText": "如果您对 Elastic SIEM 体验有任何建议,请随时{feedback}。", + "xpack.securitySolution.overview.feedbackText": "如果您对 Elastic Security 体验有任何建议,请随时{feedback}。", "xpack.securitySolution.overview.feedbackText.feedbackLinkText": "在线提交反馈", "xpack.securitySolution.overview.feedbackTitle": "反馈", "xpack.securitySolution.overview.filebeatCiscoTitle": "Cisco", @@ -14737,15 +14708,15 @@ "xpack.securitySolution.overview.packetBeatFlowTitle": "流", "xpack.securitySolution.overview.packetbeatTLSTitle": "TLS", "xpack.securitySolution.overview.pageSubtitle": "Elastic Stack 的安全信息和事件管理功能", - "xpack.securitySolution.overview.pageTitle": "SIEM", + "xpack.securitySolution.overview.pageTitle": "Security", "xpack.securitySolution.overview.recentCasesSidebarTitle": "最近案例", "xpack.securitySolution.overview.recentlyCreatedCasesButtonLabel": "最近创建的案例", "xpack.securitySolution.overview.recentTimelinesSidebarTitle": "最近的时间线", "xpack.securitySolution.overview.showTopTooltip": "显示热门{fieldName}", - "xpack.securitySolution.overview.startedText": "欢迎使用安全信息和事件管理 (SIEM)。首先,查看我们的 {docs} 或 {data}。有关即将推出的功能和教程,确保查看我们的{siemSolution}页。", + "xpack.securitySolution.overview.startedText": "欢迎使用安全信息和事件管理 (Security)。首先,查看我们的 {docs} 或 {data}。有关即将推出的功能和教程,确保查看我们的{siemSolution}页。", "xpack.securitySolution.overview.startedText.dataLinkText": "正在采集数据", "xpack.securitySolution.overview.startedText.docsLinkText": "文档", - "xpack.securitySolution.overview.startedText.siemSolutionLinkText": "SIEM 解决方案", + "xpack.securitySolution.overview.startedText.siemSolutionLinkText": "Security 解决方案", "xpack.securitySolution.overview.startedTitle": "入门", "xpack.securitySolution.overview.topNLabel": "热门{fieldName}", "xpack.securitySolution.overview.viewAlertsButtonLabel": "查看告警", @@ -14754,7 +14725,7 @@ "xpack.securitySolution.overview.winlogbeatSecurityTitle": "安全", "xpack.securitySolution.pages.common.emptyActionPrimary": "使用 Beats 添加数据", "xpack.securitySolution.pages.common.emptyActionSecondary": "查看入门指南", - "xpack.securitySolution.pages.common.emptyMessage": "要开始使用安全信息和事件管理 (SIEM),您将需要将 SIEM 相关数据以 Elastic Common Schema (ECS) 格式添加到 Elastic Stack。较为轻松的入门方式是安装并配置我们称作 Beats 的数据采集器。让我们现在就动手!", + "xpack.securitySolution.pages.common.emptyMessage": "要开始使用安全信息和事件管理 (Security),您将需要将 Security 相关数据以 Elastic Common Schema (ECS) 格式添加到 Elastic Stack。较为轻松的入门方式是安装并配置我们称作 Beats 的数据采集器。让我们现在就动手!", "xpack.securitySolution.pages.common.emptyTitle": "欢迎使用 SIEM。让我们教您如何入门。", "xpack.securitySolution.pages.fourohfour.noContentFoundDescription": "未找到任何内容", "xpack.securitySolution.paginatedTable.rowsButtonLabel": "每页行数", @@ -14845,7 +14816,7 @@ "xpack.securitySolution.timeline.body.renderers.endgame.usingLogonTypeDescription": "使用登录类型", "xpack.securitySolution.timeline.body.renderers.endgame.viaDescription": "通过", "xpack.securitySolution.timeline.body.renderers.endgame.withSpecialPrivilegesDescription": "使用特殊权限,", - "xpack.securitySolution.timeline.callOut.unauthorized.message.description": "您需要在 SIEM 内自动保存时间线的权限,但您可以继续使用该时间线搜索和筛选安全事件", + "xpack.securitySolution.timeline.callOut.unauthorized.message.description": "您需要在 Security 内自动保存时间线的权限,但您可以继续使用该时间线搜索和筛选安全事件", "xpack.securitySolution.timeline.categoryTooltip": "类别", "xpack.securitySolution.timeline.defaultTimelineDescription": "创建新时间线时默认提供的时间线。", "xpack.securitySolution.timeline.defaultTimelineTitle": "默认空白时间线", @@ -14914,7 +14885,7 @@ "xpack.securitySolution.timelines.components.importTimelineModal.importTitle": "导入时间线……", "xpack.securitySolution.timelines.components.importTimelineModal.initialPromptTextDescription": "选择或拖放有效的 rules_export.ndjson 文件", "xpack.securitySolution.timelines.components.importTimelineModal.overwriteDescription": "自动覆盖具有相同时间线 ID 的已保存对象", - "xpack.securitySolution.timelines.components.importTimelineModal.selectTimelineDescription": "选择要导入的 SIEM 时间线(如从“时间线”视图导出的)", + "xpack.securitySolution.timelines.components.importTimelineModal.selectTimelineDescription": "选择要导入的 Security 时间线(如从“时间线”视图导出的)", "xpack.securitySolution.timelines.components.importTimelineModal.successfullyImportedTimelinesTitle": "已成功导入 {totalCount} 条{totalCount, plural, =1 {时间线} other {时间线}}", "xpack.securitySolution.timelines.components.tabs.templatesTitle": "模板", "xpack.securitySolution.timelines.components.tabs.timelinesTitle": "时间线", @@ -14924,11 +14895,11 @@ "xpack.securitySolution.topN.rawEventsSelectLabel": "原始事件", "xpack.securitySolution.uiSettings.defaultAnomalyScoreDescription": "

在显示异常之前要超过的默认异常分数阈值。

有效值:0 到 100。

", "xpack.securitySolution.uiSettings.defaultAnomalyScoreLabel": "默认异常阈值", - "xpack.securitySolution.uiSettings.defaultIndexDescription": "

SIEM 应用要从其中搜索事件的 Elasticsearch 索引逗号分隔列表。

", + "xpack.securitySolution.uiSettings.defaultIndexDescription": "

Security 应用要从其中搜索事件的 Elasticsearch 索引逗号分隔列表。

", "xpack.securitySolution.uiSettings.defaultIndexLabel": "默认索引", - "xpack.securitySolution.uiSettings.defaultRefreshIntervalDescription": "

SIEM 时间筛选的默认刷新时间间隔(毫秒)。

", + "xpack.securitySolution.uiSettings.defaultRefreshIntervalDescription": "

Security 时间筛选的默认刷新时间间隔(毫秒)。

", "xpack.securitySolution.uiSettings.defaultRefreshIntervalLabel": "时间筛选刷新时间间隔", - "xpack.securitySolution.uiSettings.defaultTimeRangeDescription": "

SIEM 时间筛选中的默认时间期间。

", + "xpack.securitySolution.uiSettings.defaultTimeRangeDescription": "

Security 时间筛选中的默认时间期间。

", "xpack.securitySolution.uiSettings.defaultTimeRangeLabel": "时间筛选默认值", "xpack.securitySolution.uiSettings.enableNewsFeedDescription": "

启用新闻源

", "xpack.securitySolution.uiSettings.enableNewsFeedLabel": "新闻源", 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/plugins/advanced_ui_actions/kibana.json b/x-pack/plugins/ui_actions_enhanced/kibana.json similarity index 60% rename from x-pack/plugins/advanced_ui_actions/kibana.json rename to x-pack/plugins/ui_actions_enhanced/kibana.json index 45907e2d8b6022..027004f165c3b8 100644 --- a/x-pack/plugins/advanced_ui_actions/kibana.json +++ b/x-pack/plugins/ui_actions_enhanced/kibana.json @@ -1,7 +1,7 @@ { - "id": "advancedUiActions", + "id": "uiActionsEnhanced", "version": "kibana", - "configPath": ["xpack", "advanced_ui_actions"], + "configPath": ["xpack", "ui_actions_enhanced"], "requiredPlugins": [ "embeddable", "uiActions" diff --git a/x-pack/plugins/advanced_ui_actions/public/can_inherit_time_range.test.ts b/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/can_inherit_time_range.test.ts rename to x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/can_inherit_time_range.ts b/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/can_inherit_time_range.ts rename to x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.scss b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.scss similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.scss rename to x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.scss diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.story.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.story.tsx similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.story.tsx rename to x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.story.tsx diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.test.tsx rename to x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx rename to x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/i18n.ts b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts similarity index 85% rename from x-pack/plugins/advanced_ui_actions/public/components/action_wizard/i18n.ts rename to x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts index a315184bf68efa..3e7e211dc7738c 100644 --- a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/i18n.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; export const txtChangeButton = i18n.translate( - 'xpack.advancedUiActions.components.actionWizard.changeButton', + 'xpack.uiActionsEnhanced.components.actionWizard.changeButton', { defaultMessage: 'Change', } diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/index.ts b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/index.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/components/action_wizard/index.ts rename to x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/index.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx rename to x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx diff --git a/x-pack/plugins/advanced_ui_actions/public/components/index.ts b/x-pack/plugins/ui_actions_enhanced/public/components/index.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/components/index.ts rename to x-pack/plugins/ui_actions_enhanced/public/components/index.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.test.ts b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.test.ts rename to x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.tsx b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx similarity index 98% rename from x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.tsx rename to x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx index c0cd8d5540db2b..4da4d648bc0ec2 100644 --- a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx @@ -61,7 +61,7 @@ export class CustomTimeRangeAction implements ActionByType - {i18n.translate('xpack.advancedUiActions.customizeTimeRange.modal.headerTitle', { + {i18n.translate('xpack.uiActionsEnhanced.customizeTimeRange.modal.headerTitle', { defaultMessage: 'Customize panel time range', })} @@ -104,7 +104,7 @@ export class CustomizeTimeRangeModal extends Component {i18n.translate( - 'xpack.advancedUiActions.customizePanelTimeRange.modal.removeButtonTitle', + 'xpack.uiActionsEnhanced.customizePanelTimeRange.modal.removeButtonTitle', { defaultMessage: 'Remove', } @@ -152,7 +152,7 @@ export class CustomizeTimeRangeModal extends Component {i18n.translate( - 'xpack.advancedUiActions.customizePanelTimeRange.modal.cancelButtonTitle', + 'xpack.uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle', { defaultMessage: 'Cancel', } @@ -163,13 +163,13 @@ export class CustomizeTimeRangeModal extends Component {this.state.inheritTimeRange ? i18n.translate( - 'xpack.advancedUiActions.customizePanelTimeRange.modal.addToPanelButtonTitle', + 'xpack.uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle', { defaultMessage: 'Add to panel', } ) : i18n.translate( - 'xpack.advancedUiActions.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle', + 'xpack.uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle', { defaultMessage: 'Update', } diff --git a/x-pack/plugins/advanced_ui_actions/public/does_inherit_time_range.ts b/x-pack/plugins/ui_actions_enhanced/public/does_inherit_time_range.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/does_inherit_time_range.ts rename to x-pack/plugins/ui_actions_enhanced/public/does_inherit_time_range.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/drilldowns/drilldown_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/drilldowns/drilldown_definition.ts rename to x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/drilldowns/index.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/index.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/drilldowns/index.ts rename to x-pack/plugins/ui_actions_enhanced/public/drilldowns/index.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/action_factory.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/dynamic_actions/action_factory.ts rename to x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/action_factory_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/dynamic_actions/action_factory_definition.ts rename to x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_manager.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_manager.test.ts rename to x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_manager.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_manager.ts rename to x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_manager_state.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager_state.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_manager_state.ts rename to x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager_state.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_storage.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_storage.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_storage.ts rename to x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_storage.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/index.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/index.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/dynamic_actions/index.ts rename to x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/index.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/types.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/dynamic_actions/types.ts rename to x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/index.ts b/x-pack/plugins/ui_actions_enhanced/public/index.ts similarity index 91% rename from x-pack/plugins/advanced_ui_actions/public/index.ts rename to x-pack/plugins/ui_actions_enhanced/public/index.ts index 024cfe5530b971..a3cfddb31d6635 100644 --- a/x-pack/plugins/advanced_ui_actions/public/index.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/index.ts @@ -19,8 +19,8 @@ export { export { ActionWizard } from './components'; export { - ActionFactoryDefinition as AdvancedUiActionsActionFactoryDefinition, - ActionFactory as AdvancedUiActionsActionFactory, + ActionFactoryDefinition as UiActionsEnhancedActionFactoryDefinition, + ActionFactory as UiActionsEnhancedActionFactory, SerializedAction as UiActionsEnhancedSerializedAction, SerializedEvent as UiActionsEnhancedSerializedEvent, AbstractActionStorage as UiActionsEnhancedAbstractActionStorage, diff --git a/x-pack/plugins/advanced_ui_actions/public/mocks.ts b/x-pack/plugins/ui_actions_enhanced/public/mocks.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/mocks.ts rename to x-pack/plugins/ui_actions_enhanced/public/mocks.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/plugin.ts b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts similarity index 93% rename from x-pack/plugins/advanced_ui_actions/public/plugin.ts rename to x-pack/plugins/ui_actions_enhanced/public/plugin.ts index f042130158aecf..d79996d5ecc1b4 100644 --- a/x-pack/plugins/advanced_ui_actions/public/plugin.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts @@ -11,6 +11,7 @@ import { Plugin, } from '../../../../src/core/public'; import { createReactOverlays } from '../../../../src/plugins/kibana_react/public'; +import { UI_SETTINGS } from '../../../../src/plugins/data/public'; import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { CONTEXT_MENU_TRIGGER, @@ -72,7 +73,9 @@ export class AdvancedUiActionsPublicPlugin public start(core: CoreStart, { uiActions }: StartDependencies): StartContract { const dateFormat = core.uiSettings.get('dateFormat') as string; - const commonlyUsedRanges = core.uiSettings.get('timepicker:quickRanges') as CommonlyUsedRange[]; + const commonlyUsedRanges = core.uiSettings.get( + UI_SETTINGS.TIMEPICKER_QUICK_RANGES + ) as CommonlyUsedRange[]; const { openModal } = createReactOverlays(core); const timeRangeAction = new CustomTimeRangeAction({ openModal, diff --git a/x-pack/plugins/advanced_ui_actions/public/services/index.ts b/x-pack/plugins/ui_actions_enhanced/public/services/index.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/services/index.ts rename to x-pack/plugins/ui_actions_enhanced/public/services/index.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/services/ui_actions_service_enhancements.test.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/services/ui_actions_service_enhancements.test.ts rename to x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/services/ui_actions_service_enhancements.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/services/ui_actions_service_enhancements.ts rename to x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/test_helpers/index.ts b/x-pack/plugins/ui_actions_enhanced/public/test_helpers/index.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/test_helpers/index.ts rename to x-pack/plugins/ui_actions_enhanced/public/test_helpers/index.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_container.ts b/x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_container.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_container.ts rename to x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_container.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable.ts b/x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_embeddable.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable.ts rename to x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_embeddable.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts b/x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_embeddable_factory.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts rename to x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_embeddable_factory.ts diff --git a/x-pack/plugins/advanced_ui_actions/public/types.ts b/x-pack/plugins/ui_actions_enhanced/public/types.ts similarity index 100% rename from x-pack/plugins/advanced_ui_actions/public/types.ts rename to x-pack/plugins/ui_actions_enhanced/public/types.ts diff --git a/x-pack/plugins/advanced_ui_actions/scripts/storybook.js b/x-pack/plugins/ui_actions_enhanced/scripts/storybook.js similarity index 93% rename from x-pack/plugins/advanced_ui_actions/scripts/storybook.js rename to x-pack/plugins/ui_actions_enhanced/scripts/storybook.js index 3da0a3b37bfafa..2a192fc56469e3 100644 --- a/x-pack/plugins/advanced_ui_actions/scripts/storybook.js +++ b/x-pack/plugins/ui_actions_enhanced/scripts/storybook.js @@ -8,6 +8,6 @@ import { join } from 'path'; // eslint-disable-next-line require('@kbn/storybook').runStorybookCli({ - name: 'advanced_ui_actions', + name: 'ui_actions_enhanced', storyGlobs: [join(__dirname, '..', 'public', 'components', '**', '*.story.tsx')], }); diff --git a/x-pack/plugins/uptime/README.md b/x-pack/plugins/uptime/README.md index 10c1fc0edcd00d..54bf48e8d3c862 100644 --- a/x-pack/plugins/uptime/README.md +++ b/x-pack/plugins/uptime/README.md @@ -55,7 +55,7 @@ In another shell, from **~kibana/x-pack**: #### API tests If instead you need to run API tests, start up the test server and then in another shell, from **~kibana/x-pack**: -`node ../scripts/functional_test_runner.js --config test/api_integration/config.js --grep="{TEST_NAME}"`. +`node ../scripts/functional_test_runner.js --config test/api_integration/config.ts --grep="{TEST_NAME}"`. You can update snapshots by prefixing the runner command with `env UPDATE_UPTIME_FIXTURES=1` diff --git a/x-pack/plugins/watcher/public/legacy/time_buckets.js b/x-pack/plugins/watcher/public/legacy/time_buckets.js index c1674bc68ab42c..9df254d2bb1e60 100644 --- a/x-pack/plugins/watcher/public/legacy/time_buckets.js +++ b/x-pack/plugins/watcher/public/legacy/time_buckets.js @@ -6,7 +6,7 @@ import _ from 'lodash'; import moment from 'moment'; -import { search, FIELD_FORMAT_IDS } from '../../../../../src/plugins/data/public'; +import { search, FIELD_FORMAT_IDS, UI_SETTINGS } from '../../../../../src/plugins/data/public'; import { calcAutoIntervalLessThan, calcAutoIntervalNear } from './calc_auto_interval'; import { convertDurationToNormalizedEsInterval, @@ -219,14 +219,14 @@ TimeBuckets.prototype.getInterval = function (useNormalizedEsInterval = true) { function readInterval() { const interval = self._i; if (moment.isDuration(interval)) return interval; - return calcAutoIntervalNear(self.getConfig('histogram:barTarget'), Number(duration)); + return calcAutoIntervalNear(self.getConfig(UI_SETTINGS.HISTOGRAM_BAR_TARGET), Number(duration)); } // check to see if the interval should be scaled, and scale it if so function maybeScaleInterval(interval) { if (!self.hasBounds()) return interval; - const maxLength = self.getConfig('histogram:maxBars'); + const maxLength = self.getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS); const approxLen = duration / interval; let scaled; diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index fc78e1b80bc681..4392299a78e72e 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -11,8 +11,9 @@ const alwaysImportedTests = [ require.resolve('../test/functional/config_security_trial.ts'), ]; const onlyNotInCoverageTests = [ - require.resolve('../test/api_integration/config_security_basic.js'), - require.resolve('../test/api_integration/config.js'), + require.resolve('../test/api_integration/config_security_basic.ts'), + require.resolve('../test/api_integration/config_security_trial.ts'), + require.resolve('../test/api_integration/config.ts'), require.resolve('../test/alerting_api_integration/basic/config.ts'), require.resolve('../test/alerting_api_integration/spaces_only/config.ts'), require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'), diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index e6b9c0bf5409f5..19aee29e9b36d6 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -40,7 +40,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) return async ({ readConfigFile }: FtrConfigProviderContext) => { const xPackApiIntegrationTestsConfig = await readConfigFile( - require.resolve('../../api_integration/config.js') + require.resolve('../../api_integration/config.ts') ); const servers = { ...xPackApiIntegrationTestsConfig.get('servers'), 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/advanced_settings/feature_controls.ts b/x-pack/test/api_integration/apis/management/advanced_settings/feature_controls.ts index 80ffc8fd36e497..77e23bd74cc220 100644 --- a/x-pack/test/api_integration/apis/management/advanced_settings/feature_controls.ts +++ b/x-pack/test/api_integration/apis/management/advanced_settings/feature_controls.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; import { FtrProviderContext } from '../../../ftr_provider_context'; +import { CSV_QUOTE_VALUES_SETTING } from '../../../../../../src/plugins/share/common/constants'; export default function featureControlsTests({ getService }: FtrProviderContext) { const supertest: SuperTest = getService('supertestWithoutAuth'); @@ -32,7 +33,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) .post(`${basePath}/api/kibana/settings`) .auth(username, password) .set('kbn-xsrf', 'foo') - .send({ changes: { 'csv:quoteValues': null } }) + .send({ changes: { [CSV_QUOTE_VALUES_SETTING]: null } }) .then((response: any) => ({ error: undefined, response })) .catch((error: any) => ({ error, response: undefined })); } diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/fixtures.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/fixtures.js index 6dc1982d70f698..12975c484ebee1 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/fixtures.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/fixtures.js @@ -4,21 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRandomString } from './lib'; import { INDEX_TEMPLATE_PATTERN_PREFIX } from './constants'; -export const getPolicyPayload = ({ name = getRandomString() } = {}) => ({ +export const getPolicyPayload = (name) => ({ name, phases: { hot: { + min_age: '1d', actions: { + set_priority: { + priority: 100, + }, + unfollow: {}, rollover: { max_age: '30d', max_size: '50gb', }, - set_priority: { - priority: 100, - }, }, }, warm: { @@ -26,6 +27,26 @@ export const getPolicyPayload = ({ name = getRandomString() } = {}) => ({ set_priority: { priority: 50, }, + unfollow: {}, + readonly: {}, + allocate: { + number_of_replicas: 5, + include: { + a: 'a', + }, + exclude: { + b: 'b', + }, + require: { + c: 'c', + }, + }, + shrink: { + number_of_shards: 1, + }, + forcemerge: { + max_num_segments: 1, + }, }, }, cold: { @@ -34,12 +55,34 @@ export const getPolicyPayload = ({ name = getRandomString() } = {}) => ({ set_priority: { priority: 0, }, + unfollow: {}, + allocate: { + number_of_replicas: 5, + include: { + a: 'a', + }, + exclude: { + b: 'b', + }, + require: { + c: 'c', + }, + }, + freeze: {}, + searchable_snapshot: { + snapshot_repository: 'backing_repo', + }, }, }, delete: { min_age: '10d', actions: { - delete: {}, + wait_for_snapshot: { + policy: 'policy', + }, + delete: { + delete_searchable_snapshot: true, + }, }, }, }, diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/indices.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/indices.js index 6a6b8d790d9e51..af9ff4bf1bd9ac 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/indices.js @@ -29,7 +29,7 @@ export default function ({ getService }) { describe('policies', () => { it('should add a lifecycle policy to the index', async () => { // Create a policy - const policy = getPolicyPayload(); + const policy = getPolicyPayload('indices-test-policy'); const { name: policyName } = policy; await createPolicy(policy); @@ -52,7 +52,7 @@ export default function ({ getService }) { it('should remove a lifecycle policy from an index', async () => { // Create a policy - const policy = getPolicyPayload(); + const policy = getPolicyPayload('remove-test-policy'); const { name: policyName } = policy; await createPolicy(policy); @@ -77,7 +77,7 @@ export default function ({ getService }) { describe('index management extension', () => { it('should have an endpoint to retry a policy for an index that is in the ERROR step', async () => { // Create a policy - const policy = getPolicyPayload(); + const policy = getPolicyPayload('extension-test-policy'); const { name: policyName } = policy; await createPolicy(policy); diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.helpers.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.helpers.js index a8863c5dc6c786..d2b00365cd3e04 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.helpers.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.helpers.js @@ -5,7 +5,6 @@ */ import { API_BASE_PATH, DEFAULT_POLICY_NAME } from './constants'; -import { getPolicyPayload } from './fixtures'; import { getPolicyNames } from './lib'; export const registerHelpers = ({ supertest }) => { @@ -14,7 +13,7 @@ export const registerHelpers = ({ supertest }) => { ? supertest.get(`${API_BASE_PATH}/policies?withIndices=true`) : supertest.get(`${API_BASE_PATH}/policies`); - const createPolicy = (policy = getPolicyPayload()) => { + const createPolicy = (policy) => { return supertest.post(`${API_BASE_PATH}/policies`).set('kbn-xsrf', 'xxx').send(policy); }; diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js index 9fd38a6b32a601..fad7fb848122d4 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js @@ -32,9 +32,7 @@ export default function ({ getService }) { after(() => Promise.all([cleanUpEsResources(), cleanUpPolicies()])); describe('list', () => { - // Disabled as the underline ES API has changed. Need to investigate - // Opened issue: https://github.com/elastic/kibana/issues/62778 - it.skip('should have a default policy to manage the Watcher history indices', async () => { + it('should have a default policy to manage the Watcher history indices', async () => { const { body } = await loadPolicies().expect(200); const policy = body.find((policy) => policy.name === DEFAULT_POLICY_NAME); @@ -50,7 +48,9 @@ export default function ({ getService }) { delete: { min_age: '7d', actions: { - delete: {}, + delete: { + delete_searchable_snapshot: true, + }, }, }, }, @@ -61,7 +61,7 @@ export default function ({ getService }) { it('should add the indices linked to the policies', async () => { // Create a policy - const policy = getPolicyPayload(); + const policy = getPolicyPayload('link-test-policy'); const { name: policyName } = policy; await createPolicy(policy); @@ -78,7 +78,7 @@ export default function ({ getService }) { describe('create', () => { it('should create a lifecycle policy', async () => { - const policy = getPolicyPayload(); + const policy = getPolicyPayload('create-test-policy'); const { name } = policy; // Load current policies @@ -96,7 +96,7 @@ export default function ({ getService }) { describe('delete', () => { it('should delete the policy created', async () => { - const policy = getPolicyPayload(); + const policy = getPolicyPayload('delete-test-policy'); const { name } = policy; // Create new policy diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js index 2287558d9ef32b..7fb9b35b8475e1 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js @@ -51,7 +51,7 @@ export default function ({ getService }) { describe('update', () => { it('should add a policy to a template', async () => { // Create policy - const policy = getPolicyPayload(); + const policy = getPolicyPayload('template-test-policy'); const { name: policyName } = policy; await createPolicy(policy); 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/api_integration/apis/security/license_downgrade.ts b/x-pack/test/api_integration/apis/security/license_downgrade.ts new file mode 100644 index 00000000000000..1811f99977b606 --- /dev/null +++ b/x-pack/test/api_integration/apis/security/license_downgrade.ts @@ -0,0 +1,56 @@ +/* + * 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 expect from '@kbn/expect/expect.js'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('Privileges registration', function () { + this.tags(['skipCloud']); + + it('privileges are re-registered on license downgrade', async () => { + // Verify currently registered privileges for TRIAL license. + // If you're adding a privilege to the following, that's great! + // If you're removing a privilege, this breaks backwards compatibility + // Roles are associated with these privileges, and we shouldn't be removing them in a minor version. + const expectedTrialLicenseDiscoverPrivileges = [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'url_create', + ]; + const trialPrivileges = await supertest + .get('/api/security/privileges') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + + expect(trialPrivileges.body.features.discover).to.eql(expectedTrialLicenseDiscoverPrivileges); + + // Revert license to basic. + await supertest + .post('/api/license/start_basic?acknowledge=true') + .set('kbn-xsrf', 'xxx') + .expect(200, { + basic_was_started: true, + acknowledged: true, + }); + + // Verify that privileges were re-registered. + const expectedBasicLicenseDiscoverPrivileges = ['all', 'read']; + const basicPrivileges = await supertest + .get('/api/security/privileges') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + + expect(basicPrivileges.body.features.discover).to.eql(expectedBasicLicenseDiscoverPrivileges); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/security/security_trial.ts b/x-pack/test/api_integration/apis/security/security_trial.ts new file mode 100644 index 00000000000000..c646fecc1909f6 --- /dev/null +++ b/x-pack/test/api_integration/apis/security/security_trial.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('security (trial license)', function () { + this.tags('ciGroup6'); + + // THIS TEST NEEDS TO BE LAST. IT IS DESTRUCTIVE! IT REMOVES TRIAL LICENSE!!! + loadTestFile(require.resolve('./license_downgrade')); + }); +} diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.ts similarity index 94% rename from x-pack/test/api_integration/config.js rename to x-pack/test/api_integration/config.ts index 2e6e25aaebff00..71da903d33b29b 100644 --- a/x-pack/test/api_integration/config.js +++ b/x-pack/test/api_integration/config.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { services } from './services'; -export async function getApiIntegrationConfig({ readConfigFile }) { +export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProviderContext) { const xPackFunctionalTestsConfig = await readConfigFile( require.resolve('../functional/config.js') ); diff --git a/x-pack/test/api_integration/config_security_basic.js b/x-pack/test/api_integration/config_security_basic.ts similarity index 67% rename from x-pack/test/api_integration/config_security_basic.js rename to x-pack/test/api_integration/config_security_basic.ts index 713045e8c4d3cd..84899405056867 100644 --- a/x-pack/test/api_integration/config_security_basic.js +++ b/x-pack/test/api_integration/config_security_basic.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable import/no-default-export */ + +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { default as createTestConfig } from './config'; -export default async function ({ readConfigFile }) { - //security APIs should function the same under a basic or trial license - return createTestConfig({ readConfigFile }).then((config) => { +export default async function (context: FtrConfigProviderContext) { + // security APIs should function the same under a basic or trial license + return createTestConfig(context).then((config) => { config.esTestCluster.license = 'basic'; config.esTestCluster.serverArgs = [ 'xpack.license.self_generated.type=basic', diff --git a/x-pack/test/api_integration/config_security_trial.ts b/x-pack/test/api_integration/config_security_trial.ts new file mode 100644 index 00000000000000..4c1e2913b987c1 --- /dev/null +++ b/x-pack/test/api_integration/config_security_trial.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable import/no-default-export */ + +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { default as createTestConfig } from './config'; + +export default async function (context: FtrConfigProviderContext) { + return createTestConfig(context).then((config) => { + config.testFiles = [require.resolve('./apis/security/security_trial')]; + return config; + }); +} diff --git a/x-pack/test/apm_api_integration/common/config.ts b/x-pack/test/apm_api_integration/common/config.ts index 9e011a98bbfcd4..83dc597829a3cd 100644 --- a/x-pack/test/apm_api_integration/common/config.ts +++ b/x-pack/test/apm_api_integration/common/config.ts @@ -17,7 +17,7 @@ export function createTestConfig(settings: Settings) { return async ({ readConfigFile }: FtrConfigProviderContext) => { const xPackAPITestsConfig = await readConfigFile( - require.resolve('../../api_integration/config.js') + require.resolve('../../api_integration/config.ts') ); return { diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 45b34b7d26940e..098d94f35d9ca2 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -39,7 +39,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) return async ({ readConfigFile }: FtrConfigProviderContext) => { const xPackApiIntegrationTestsConfig = await readConfigFile( - require.resolve('../../api_integration/config.js') + require.resolve('../../api_integration/config.ts') ); const servers = { diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index fa71a263b51a76..6a1add60b19f1e 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -42,7 +42,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) return async ({ readConfigFile }: FtrConfigProviderContext) => { const xPackApiIntegrationTestsConfig = await readConfigFile( - require.resolve('../../api_integration/config.js') + require.resolve('../../api_integration/config.ts') ); const servers = { ...xPackApiIntegrationTestsConfig.get('servers'), diff --git a/x-pack/test/encrypted_saved_objects_api_integration/config.ts b/x-pack/test/encrypted_saved_objects_api_integration/config.ts index f4b3091bec2343..fb643c2c5a901b 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/config.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/config.ts @@ -9,7 +9,7 @@ import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js')); + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); return { testFiles: [require.resolve('./tests')], diff --git a/x-pack/test/endpoint_api_integration_no_ingest/config.ts b/x-pack/test/endpoint_api_integration_no_ingest/config.ts index 00a65b9fbaec64..0cda1cfaf14338 100644 --- a/x-pack/test/endpoint_api_integration_no_ingest/config.ts +++ b/x-pack/test/endpoint_api_integration_no_ingest/config.ts @@ -7,7 +7,7 @@ import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js')); + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); return { ...xPackAPITestsConfig.getAll(), diff --git a/x-pack/test/epm_api_integration/config.ts b/x-pack/test/epm_api_integration/config.ts index b77d763313588e..6b08c7ec579559 100644 --- a/x-pack/test/epm_api_integration/config.ts +++ b/x-pack/test/epm_api_integration/config.ts @@ -7,7 +7,7 @@ import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js')); + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); return { testFiles: [require.resolve('./apis')], diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts index cbf1f3e1af2dfc..cda41c4e605919 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -140,7 +140,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/57377 describe.skip('no advanced_settings privileges', function () { - this.tags(['skipCoverage']); before(async () => { await security.role.create('no_advanced_settings_privileges_role', { elasticsearch: { diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts index f7991e62fdaa95..c8adb3ce67d555 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts @@ -60,7 +60,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('space with Advanced Settings disabled', function () { - this.tags('skipCoverage'); before(async () => { // we need to load the following in every situation as deleting // a space deletes all of the associated saved objects diff --git a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts index 626ca6add6a3ca..b4dfffcdeff574 100644 --- a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts +++ b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); @@ -95,7 +96,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } ]`; - await kibanaServer.uiSettings.update({ 'timepicker:quickRanges': SAMPLE_DATA_RANGE }); + await kibanaServer.uiSettings.update({ + [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: SAMPLE_DATA_RANGE, + }); // refresh page to make sure ui settings update is picked up await browser.refresh(); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/index.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/index.ts index 214a462447ef1c..1df48971ba8ccd 100644 --- a/x-pack/test/functional/apps/dev_tools/feature_controls/index.ts +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/index.ts @@ -7,7 +7,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('feature controls', function () { - this.tags(['skipFirefox', 'skipCoverage']); + this.tags(['skipFirefox']); loadTestFile(require.resolve('./dev_tools_security')); loadTestFile(require.resolve('./dev_tools_spaces')); }); diff --git a/x-pack/test/functional/apps/maps/embeddable/dashboard.js b/x-pack/test/functional/apps/maps/embeddable/dashboard.js index 25770ea55a5d55..3c935b385cb39f 100644 --- a/x-pack/test/functional/apps/maps/embeddable/dashboard.js +++ b/x-pack/test/functional/apps/maps/embeddable/dashboard.js @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['common', 'dashboard', 'maps']); @@ -19,7 +20,7 @@ export default function ({ getPageObjects, getService }) { before(async () => { await kibanaServer.uiSettings.replace({ defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', - 'courier:ignoreFilterIfFieldNotInIndex': true, + [UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX]: true, }); await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.loadSavedDashboard('map embeddable example'); @@ -27,7 +28,7 @@ export default function ({ getPageObjects, getService }) { after(async () => { await kibanaServer.uiSettings.replace({ - 'courier:ignoreFilterIfFieldNotInIndex': false, + [UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX]: false, }); }); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts index 4f56015227fdf1..c8baa13b564086 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts @@ -11,7 +11,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('classification creation', function () { + // flaky test, see https://github.com/elastic/kibana/issues/68356 + describe.skip('classification creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/bm_classification'); await ml.testResources.createIndexPatternIfNeeded('ft_bank_marketing', '@timestamp'); @@ -60,35 +61,19 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToDataFrameAnalytics(); }); - it('loads the job creation flyout', async () => { + it('loads the source selection modal', async () => { await ml.dataFrameAnalytics.startAnalyticsCreation(); }); + it('selects the source data and loads the job wizard page', async () => { + ml.jobSourceSelection.selectSourceForAnalyticsJob(testData.source); + }); + it('selects the job type', async () => { await ml.dataFrameAnalyticsCreation.assertJobTypeSelectExists(); await ml.dataFrameAnalyticsCreation.selectJobType(testData.jobType); }); - it('inputs the job id', async () => { - await ml.dataFrameAnalyticsCreation.assertJobIdInputExists(); - await ml.dataFrameAnalyticsCreation.setJobId(testData.jobId); - }); - - it('inputs the job description', async () => { - await ml.dataFrameAnalyticsCreation.assertJobDescriptionInputExists(); - await ml.dataFrameAnalyticsCreation.setJobDescription(testData.jobDescription); - }); - - it('selects the source index', async () => { - await ml.dataFrameAnalyticsCreation.assertSourceIndexInputExists(); - await ml.dataFrameAnalyticsCreation.selectSourceIndex(testData.source); - }); - - it('inputs the destination index', async () => { - await ml.dataFrameAnalyticsCreation.assertDestIndexInputExists(); - await ml.dataFrameAnalyticsCreation.setDestIndex(testData.destinationIndex); - }); - it('inputs the dependent variable', async () => { await ml.dataFrameAnalyticsCreation.assertDependentVariableInputExists(); await ml.dataFrameAnalyticsCreation.selectDependentVariable(testData.dependentVariable); @@ -99,11 +84,34 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.setTrainingPercent(testData.trainingPercent); }); + it('continues to the additional options step', async () => { + await ml.dataFrameAnalyticsCreation.continueToAdditionalOptionsStep(); + }); + it('inputs the model memory limit', async () => { await ml.dataFrameAnalyticsCreation.assertModelMemoryInputExists(); await ml.dataFrameAnalyticsCreation.setModelMemory(testData.modelMemory); }); + it('continues to the details step', async () => { + await ml.dataFrameAnalyticsCreation.continueToDetailsStep(); + }); + + it('inputs the job id', async () => { + await ml.dataFrameAnalyticsCreation.assertJobIdInputExists(); + await ml.dataFrameAnalyticsCreation.setJobId(testData.jobId); + }); + + it('inputs the job description', async () => { + await ml.dataFrameAnalyticsCreation.assertJobDescriptionInputExists(); + await ml.dataFrameAnalyticsCreation.setJobDescription(testData.jobDescription); + }); + + it('inputs the destination index', async () => { + await ml.dataFrameAnalyticsCreation.assertDestIndexInputExists(); + await ml.dataFrameAnalyticsCreation.setDestIndex(testData.destinationIndex); + }); + it('sets the create index pattern switch', async () => { await ml.dataFrameAnalyticsCreation.assertCreateIndexPatternSwitchExists(); await ml.dataFrameAnalyticsCreation.setCreateIndexPatternSwitchState( @@ -111,19 +119,14 @@ export default function ({ getService }: FtrProviderContext) { ); }); - it('creates the analytics job', async () => { - await ml.dataFrameAnalyticsCreation.assertCreateButtonExists(); - await ml.dataFrameAnalyticsCreation.createAnalyticsJob(testData.jobId); - }); - - it('starts the analytics job', async () => { - await ml.dataFrameAnalyticsCreation.assertStartButtonExists(); - await ml.dataFrameAnalyticsCreation.startAnalyticsJob(); + it('continues to the create step', async () => { + await ml.dataFrameAnalyticsCreation.continueToCreateStep(); }); - it('closes the create job flyout', async () => { - await ml.dataFrameAnalyticsCreation.assertCloseButtonExists(); - await ml.dataFrameAnalyticsCreation.closeCreateAnalyticsJobFlyout(); + it('creates and starts the analytics job', async () => { + await ml.dataFrameAnalyticsCreation.assertCreateButtonExists(); + await ml.dataFrameAnalyticsCreation.assertStartJobCheckboxCheckState(true); + await ml.dataFrameAnalyticsCreation.createAnalyticsJob(testData.jobId); }); it('finishes analytics processing', async () => { @@ -131,6 +134,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('displays the analytics table', async () => { + await ml.dataFrameAnalyticsCreation.navigateToJobManagementPage(); await ml.dataFrameAnalytics.assertAnalyticsTableExists(); }); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts index 7636b87033bf8e..b3e47db8267878 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts @@ -182,16 +182,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.createAnalyticsJob(cloneJobId); }); - it('should start the clone analytics job', async () => { - await ml.dataFrameAnalyticsCreation.assertStartButtonExists(); - await ml.dataFrameAnalyticsCreation.startAnalyticsJob(); - }); - - it('should close the create job flyout', async () => { - await ml.dataFrameAnalyticsCreation.assertCloseButtonExists(); - await ml.dataFrameAnalyticsCreation.closeCreateAnalyticsJobFlyout(); - }); - it('finishes analytics processing', async () => { await ml.dataFrameAnalytics.waitForAnalyticsCompletion(cloneJobId); }); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts index 0202c8431ce348..cff59fa42abb00 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts @@ -12,6 +12,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./outlier_detection_creation')); loadTestFile(require.resolve('./regression_creation')); loadTestFile(require.resolve('./classification_creation')); - loadTestFile(require.resolve('./cloning')); + // loadTestFile(require.resolve('./cloning')); }); } diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts index 7d10d6724d9e2c..b5c2b7528437cc 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts @@ -58,10 +58,14 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToDataFrameAnalytics(); }); - it('loads the job creation flyout', async () => { + it('loads the source selection modal', async () => { await ml.dataFrameAnalytics.startAnalyticsCreation(); }); + it('selects the source data and loads the job wizard page', async () => { + ml.jobSourceSelection.selectSourceForAnalyticsJob(testData.source); + }); + it('selects the job type', async () => { await ml.dataFrameAnalyticsCreation.assertJobTypeSelectExists(); await ml.dataFrameAnalyticsCreation.selectJobType(testData.jobType); @@ -75,6 +79,19 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.assertTrainingPercentInputMissing(); }); + it('continues to the additional options step', async () => { + await ml.dataFrameAnalyticsCreation.continueToAdditionalOptionsStep(); + }); + + it('inputs the model memory limit', async () => { + await ml.dataFrameAnalyticsCreation.assertModelMemoryInputExists(); + await ml.dataFrameAnalyticsCreation.setModelMemory(testData.modelMemory); + }); + + it('continues to the details step', async () => { + await ml.dataFrameAnalyticsCreation.continueToDetailsStep(); + }); + it('inputs the job id', async () => { await ml.dataFrameAnalyticsCreation.assertJobIdInputExists(); await ml.dataFrameAnalyticsCreation.setJobId(testData.jobId); @@ -85,21 +102,11 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.setJobDescription(testData.jobDescription); }); - it('selects the source index', async () => { - await ml.dataFrameAnalyticsCreation.assertSourceIndexInputExists(); - await ml.dataFrameAnalyticsCreation.selectSourceIndex(testData.source); - }); - it('inputs the destination index', async () => { await ml.dataFrameAnalyticsCreation.assertDestIndexInputExists(); await ml.dataFrameAnalyticsCreation.setDestIndex(testData.destinationIndex); }); - it('inputs the model memory limit', async () => { - await ml.dataFrameAnalyticsCreation.assertModelMemoryInputExists(); - await ml.dataFrameAnalyticsCreation.setModelMemory(testData.modelMemory); - }); - it('sets the create index pattern switch', async () => { await ml.dataFrameAnalyticsCreation.assertCreateIndexPatternSwitchExists(); await ml.dataFrameAnalyticsCreation.setCreateIndexPatternSwitchState( @@ -107,19 +114,14 @@ export default function ({ getService }: FtrProviderContext) { ); }); - it('creates the analytics job', async () => { - await ml.dataFrameAnalyticsCreation.assertCreateButtonExists(); - await ml.dataFrameAnalyticsCreation.createAnalyticsJob(testData.jobId); - }); - - it('starts the analytics job', async () => { - await ml.dataFrameAnalyticsCreation.assertStartButtonExists(); - await ml.dataFrameAnalyticsCreation.startAnalyticsJob(); + it('continues to the create step', async () => { + await ml.dataFrameAnalyticsCreation.continueToCreateStep(); }); - it('closes the create job flyout', async () => { - await ml.dataFrameAnalyticsCreation.assertCloseButtonExists(); - await ml.dataFrameAnalyticsCreation.closeCreateAnalyticsJobFlyout(); + it('creates and starts the analytics job', async () => { + await ml.dataFrameAnalyticsCreation.assertCreateButtonExists(); + await ml.dataFrameAnalyticsCreation.assertStartJobCheckboxCheckState(true); + await ml.dataFrameAnalyticsCreation.createAnalyticsJob(testData.jobId); }); it('finishes analytics processing', async () => { @@ -127,6 +129,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('displays the analytics table', async () => { + await ml.dataFrameAnalyticsCreation.navigateToJobManagementPage(); await ml.dataFrameAnalytics.assertAnalyticsTableExists(); }); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index 8322a4a1dd1392..c818619a183786 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -11,7 +11,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('regression creation', function () { + // flaky test, see https://github.com/elastic/kibana/issues/68352 + describe.skip('regression creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/egs_regression'); await ml.testResources.createIndexPatternIfNeeded('ft_egs_regression', '@timestamp'); @@ -60,35 +61,19 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToDataFrameAnalytics(); }); - it('loads the job creation flyout', async () => { + it('loads the source selection modal', async () => { await ml.dataFrameAnalytics.startAnalyticsCreation(); }); + it('selects the source data and loads the job wizard page', async () => { + ml.jobSourceSelection.selectSourceForAnalyticsJob(testData.source); + }); + it('selects the job type', async () => { await ml.dataFrameAnalyticsCreation.assertJobTypeSelectExists(); await ml.dataFrameAnalyticsCreation.selectJobType(testData.jobType); }); - it('inputs the job id', async () => { - await ml.dataFrameAnalyticsCreation.assertJobIdInputExists(); - await ml.dataFrameAnalyticsCreation.setJobId(testData.jobId); - }); - - it('inputs the job description', async () => { - await ml.dataFrameAnalyticsCreation.assertJobDescriptionInputExists(); - await ml.dataFrameAnalyticsCreation.setJobDescription(testData.jobDescription); - }); - - it('selects the source index', async () => { - await ml.dataFrameAnalyticsCreation.assertSourceIndexInputExists(); - await ml.dataFrameAnalyticsCreation.selectSourceIndex(testData.source); - }); - - it('inputs the destination index', async () => { - await ml.dataFrameAnalyticsCreation.assertDestIndexInputExists(); - await ml.dataFrameAnalyticsCreation.setDestIndex(testData.destinationIndex); - }); - it('inputs the dependent variable', async () => { await ml.dataFrameAnalyticsCreation.assertDependentVariableInputExists(); await ml.dataFrameAnalyticsCreation.selectDependentVariable(testData.dependentVariable); @@ -99,11 +84,34 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.setTrainingPercent(testData.trainingPercent); }); + it('continues to the additional options step', async () => { + await ml.dataFrameAnalyticsCreation.continueToAdditionalOptionsStep(); + }); + it('inputs the model memory limit', async () => { await ml.dataFrameAnalyticsCreation.assertModelMemoryInputExists(); await ml.dataFrameAnalyticsCreation.setModelMemory(testData.modelMemory); }); + it('continues to the details step', async () => { + await ml.dataFrameAnalyticsCreation.continueToDetailsStep(); + }); + + it('inputs the job id', async () => { + await ml.dataFrameAnalyticsCreation.assertJobIdInputExists(); + await ml.dataFrameAnalyticsCreation.setJobId(testData.jobId); + }); + + it('inputs the job description', async () => { + await ml.dataFrameAnalyticsCreation.assertJobDescriptionInputExists(); + await ml.dataFrameAnalyticsCreation.setJobDescription(testData.jobDescription); + }); + + it('inputs the destination index', async () => { + await ml.dataFrameAnalyticsCreation.assertDestIndexInputExists(); + await ml.dataFrameAnalyticsCreation.setDestIndex(testData.destinationIndex); + }); + it('sets the create index pattern switch', async () => { await ml.dataFrameAnalyticsCreation.assertCreateIndexPatternSwitchExists(); await ml.dataFrameAnalyticsCreation.setCreateIndexPatternSwitchState( @@ -111,19 +119,14 @@ export default function ({ getService }: FtrProviderContext) { ); }); - it('creates the analytics job', async () => { - await ml.dataFrameAnalyticsCreation.assertCreateButtonExists(); - await ml.dataFrameAnalyticsCreation.createAnalyticsJob(testData.jobId); - }); - - it('starts the analytics job', async () => { - await ml.dataFrameAnalyticsCreation.assertStartButtonExists(); - await ml.dataFrameAnalyticsCreation.startAnalyticsJob(); + it('continues to the create step', async () => { + await ml.dataFrameAnalyticsCreation.continueToCreateStep(); }); - it('closes the create job flyout', async () => { - await ml.dataFrameAnalyticsCreation.assertCloseButtonExists(); - await ml.dataFrameAnalyticsCreation.closeCreateAnalyticsJobFlyout(); + it('creates and starts the analytics job', async () => { + await ml.dataFrameAnalyticsCreation.assertCreateButtonExists(); + await ml.dataFrameAnalyticsCreation.assertStartJobCheckboxCheckState(true); + await ml.dataFrameAnalyticsCreation.createAnalyticsJob(testData.jobId); }); it('finishes analytics processing', async () => { @@ -131,6 +134,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('displays the analytics table', async () => { + await ml.dataFrameAnalyticsCreation.navigateToJobManagementPage(); await ml.dataFrameAnalytics.assertAnalyticsTableExists(); }); diff --git a/x-pack/test/functional/apps/uptime/certificates.ts b/x-pack/test/functional/apps/uptime/certificates.ts index 0ae56a3c5f256b..c7ba7816e0255f 100644 --- a/x-pack/test/functional/apps/uptime/certificates.ts +++ b/x-pack/test/functional/apps/uptime/certificates.ts @@ -14,15 +14,18 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const es = getService('es'); - // FLAKY: https://github.com/elastic/kibana/issues/66869 - describe.skip('certificates', function () { - beforeEach(async () => { + describe('certificates', function () { + before(async () => { + await makeCheck({ es, tls: true }); await uptime.goToRoot(true); + }); + + beforeEach(async () => { await makeCheck({ es, tls: true }); - await uptimeService.navigation.refreshApp(); }); it('can navigate to cert page', async () => { + await uptimeService.cert.isUptimeDataMissing(); await uptimeService.cert.hasViewCertButton(); await uptimeService.navigation.goToCertificates(); }); @@ -30,6 +33,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('page', () => { beforeEach(async () => { await uptimeService.navigation.goToCertificates(); + await uptimeService.navigation.refreshApp(); }); it('displays certificates', async () => { diff --git a/x-pack/test/functional/apps/visualize/index.ts b/x-pack/test/functional/apps/visualize/index.ts index 1c23b8cde86064..0f64ecf0022d0c 100644 --- a/x-pack/test/functional/apps/visualize/index.ts +++ b/x-pack/test/functional/apps/visualize/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function visualize({ loadTestFile }: FtrProviderContext) { describe('Visualize', function visualizeTestSuite() { - this.tags(['ciGroup4', 'skipFirefox', 'skipCoverage']); + this.tags(['ciGroup4', 'skipFirefox']); loadTestFile(require.resolve('./feature_controls/visualize_security')); loadTestFile(require.resolve('./feature_controls/visualize_spaces')); diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index fc2ce4bb16b99f..a48159cd7515f1 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -214,22 +214,60 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { ); }, - async getAnalyticsState(analyticsId: string): Promise { - log.debug(`Fetching analytics state for job ${analyticsId}`); + async getDFAJobStats(analyticsId: string): Promise { + log.debug(`Fetching data frame analytics job stats for job ${analyticsId}...`); const analyticsStats = await esSupertest .get(`/_ml/data_frame/analytics/${analyticsId}/_stats`) .expect(200) .then((res: any) => res.body); + return analyticsStats; + }, + + async getAnalyticsState(analyticsId: string): Promise { + log.debug(`Fetching analytics state for job ${analyticsId}`); + const analyticsStats = await this.getDFAJobStats(analyticsId); + expect(analyticsStats.data_frame_analytics).to.have.length( 1, `Expected dataframe analytics stats to have exactly one object (got '${analyticsStats.data_frame_analytics.length}')` ); + const state: DATA_FRAME_TASK_STATE = analyticsStats.data_frame_analytics[0].state; return state; }, + async getDFAJobTrainingRecordCount(analyticsId: string): Promise { + const analyticsStats = await this.getDFAJobStats(analyticsId); + + expect(analyticsStats.data_frame_analytics).to.have.length( + 1, + `Expected dataframe analytics stats to have exactly one object (got '${analyticsStats.data_frame_analytics.length}')` + ); + const trainingRecordCount: number = + analyticsStats.data_frame_analytics[0].data_counts.training_docs_count; + + return trainingRecordCount; + }, + + async waitForDFAJobTrainingRecordCountToBePositive(analyticsId: string) { + await retry.waitForWithTimeout( + `'${analyticsId}' to have training_docs_count > 0`, + 10 * 1000, + async () => { + const trainingRecordCount = await this.getDFAJobTrainingRecordCount(analyticsId); + if (trainingRecordCount > 0) { + return true; + } else { + throw new Error( + `expected data frame analytics job '${analyticsId}' to have training_docs_count > 0 (got ${trainingRecordCount})` + ); + } + } + ); + }, + async waitForAnalyticsState( analyticsId: string, expectedAnalyticsState: DATA_FRAME_TASK_STATE diff --git a/x-pack/test/functional/services/ml/data_frame_analytics.ts b/x-pack/test/functional/services/ml/data_frame_analytics.ts index bd7d76e34b447c..634b0d4d41fcab 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics.ts @@ -67,10 +67,11 @@ export function MachineLearningDataFrameAnalyticsProvider( } else { await testSubjects.click('mlAnalyticsButtonCreate'); } - await testSubjects.existOrFail('mlAnalyticsCreateJobFlyout'); + await testSubjects.existOrFail('analyticsCreateSourceIndexModal'); }, async waitForAnalyticsCompletion(analyticsId: string) { + await mlApi.waitForDFAJobTrainingRecordCountToBePositive(analyticsId); await mlApi.waitForAnalyticsState(analyticsId, DATA_FRAME_TASK_STATE.STOPPED); }, }; diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts index cff7e00eef6882..081eb8775fa5b6 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts @@ -42,12 +42,12 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( return { async assertJobTypeSelectExists() { - await testSubjects.existOrFail('mlAnalyticsCreateJobFlyoutJobTypeSelect'); + await testSubjects.existOrFail('mlAnalyticsCreateJobWizardJobTypeSelect'); }, async assertJobTypeSelection(expectedSelection: string) { const actualSelection = await testSubjects.getAttribute( - 'mlAnalyticsCreateJobFlyoutJobTypeSelect', + 'mlAnalyticsCreateJobWizardJobTypeSelect', 'value' ); expect(actualSelection).to.eql( @@ -57,12 +57,13 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( }, async selectJobType(jobType: string) { - await testSubjects.selectValue('mlAnalyticsCreateJobFlyoutJobTypeSelect', jobType); + await testSubjects.click('mlAnalyticsCreateJobWizardJobTypeSelect'); + await testSubjects.click(`mlAnalyticsCreation-${jobType}-option`); await this.assertJobTypeSelection(jobType); }, async assertAdvancedEditorSwitchExists() { - await testSubjects.existOrFail(`mlAnalyticsCreateJobFlyoutAdvancedEditorSwitch`, { + await testSubjects.existOrFail(`mlAnalyticsCreateJobWizardAdvancedEditorSwitch`, { allowHidden: true, }); }, @@ -70,7 +71,7 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( async assertAdvancedEditorSwitchCheckState(expectedCheckState: boolean) { const actualCheckState = (await testSubjects.getAttribute( - 'mlAnalyticsCreateJobFlyoutAdvancedEditorSwitch', + 'mlAnalyticsCreateJobWizardAdvancedEditorSwitch', 'aria-checked' )) === 'true'; expect(actualCheckState).to.eql( @@ -182,20 +183,22 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( }, async assertDependentVariableInputExists() { - await testSubjects.existOrFail( - 'mlAnalyticsCreateJobFlyoutDependentVariableSelect > comboBoxInput' - ); + await retry.tryForTime(8000, async () => { + await testSubjects.existOrFail( + 'mlAnalyticsCreateJobWizardDependentVariableSelect > comboBoxInput' + ); + }); }, async assertDependentVariableInputMissing() { await testSubjects.missingOrFail( - 'mlAnalyticsCreateJobFlyoutDependentVariableSelect > comboBoxInput' + 'mlAnalyticsCreateJobWizardDependentVariableSelect > comboBoxInput' ); }, async assertDependentVariableSelection(expectedSelection: string[]) { const actualSelection = await comboBox.getComboBoxSelectedOptions( - 'mlAnalyticsCreateJobFlyoutDependentVariableSelect > comboBoxInput' + 'mlAnalyticsCreateJobWizardDependentVariableSelect > comboBoxInput' ); expect(actualSelection).to.eql( expectedSelection, @@ -205,23 +208,23 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( async selectDependentVariable(dependentVariable: string) { await comboBox.set( - 'mlAnalyticsCreateJobFlyoutDependentVariableSelect > comboBoxInput', + 'mlAnalyticsCreateJobWizardDependentVariableSelect > comboBoxInput', dependentVariable ); await this.assertDependentVariableSelection([dependentVariable]); }, async assertTrainingPercentInputExists() { - await testSubjects.existOrFail('mlAnalyticsCreateJobFlyoutTrainingPercentSlider'); + await testSubjects.existOrFail('mlAnalyticsCreateJobWizardTrainingPercentSlider'); }, async assertTrainingPercentInputMissing() { - await testSubjects.missingOrFail('mlAnalyticsCreateJobFlyoutTrainingPercentSlider'); + await testSubjects.missingOrFail('mlAnalyticsCreateJobWizardTrainingPercentSlider'); }, async assertTrainingPercentValue(expectedValue: string) { const actualTrainingPercent = await testSubjects.getAttribute( - 'mlAnalyticsCreateJobFlyoutTrainingPercentSlider', + 'mlAnalyticsCreateJobWizardTrainingPercentSlider', 'value' ); expect(actualTrainingPercent).to.eql( @@ -231,7 +234,7 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( }, async setTrainingPercent(trainingPercent: string) { - const slider = await testSubjects.find('mlAnalyticsCreateJobFlyoutTrainingPercentSlider'); + const slider = await testSubjects.find('mlAnalyticsCreateJobWizardTrainingPercentSlider'); let currentValue = await slider.getAttribute('value'); let currentDiff = +currentValue - +trainingPercent; @@ -271,13 +274,28 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( await this.assertTrainingPercentValue(trainingPercent); }, + async continueToAdditionalOptionsStep() { + await testSubjects.click('mlAnalyticsCreateJobWizardContinueButton'); + await testSubjects.existOrFail('mlAnalyticsCreateJobWizardAdvancedStep'); + }, + + async continueToDetailsStep() { + await testSubjects.click('mlAnalyticsCreateJobWizardContinueButton'); + await testSubjects.existOrFail('mlAnalyticsCreateJobWizardDetailsStep'); + }, + + async continueToCreateStep() { + await testSubjects.click('mlAnalyticsCreateJobWizardContinueButton'); + await testSubjects.existOrFail('mlAnalyticsCreateJobWizardCreateStep'); + }, + async assertModelMemoryInputExists() { - await testSubjects.existOrFail('mlAnalyticsCreateJobFlyoutModelMemoryInput'); + await testSubjects.existOrFail('mlAnalyticsCreateJobWizardModelMemoryInput'); }, async assertModelMemoryValue(expectedValue: string) { const actualModelMemory = await testSubjects.getAttribute( - 'mlAnalyticsCreateJobFlyoutModelMemoryInput', + 'mlAnalyticsCreateJobWizardModelMemoryInput', 'value' ); expect(actualModelMemory).to.eql( @@ -289,7 +307,7 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( async setModelMemory(modelMemory: string) { await retry.tryForTime(15 * 1000, async () => { await mlCommon.setValueWithChecks( - 'mlAnalyticsCreateJobFlyoutModelMemoryInput', + 'mlAnalyticsCreateJobWizardModelMemoryInput', modelMemory, { clearWithKeyboard: true, @@ -300,14 +318,14 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( }, async assertCreateIndexPatternSwitchExists() { - await testSubjects.existOrFail(`mlAnalyticsCreateJobFlyoutCreateIndexPatternSwitch`, { + await testSubjects.existOrFail(`mlAnalyticsCreateJobWizardCreateIndexPatternSwitch`, { allowHidden: true, }); }, async getCreateIndexPatternSwitchCheckState(): Promise { const state = await testSubjects.getAttribute( - 'mlAnalyticsCreateJobFlyoutCreateIndexPatternSwitch', + 'mlAnalyticsCreateJobWizardCreateIndexPatternSwitch', 'aria-checked' ); return state === 'true'; @@ -323,58 +341,46 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( async setCreateIndexPatternSwitchState(checkState: boolean) { if ((await this.getCreateIndexPatternSwitchCheckState()) !== checkState) { - await testSubjects.click('mlAnalyticsCreateJobFlyoutCreateIndexPatternSwitch'); + await testSubjects.click('mlAnalyticsCreateJobWizardCreateIndexPatternSwitch'); } await this.assertCreateIndexPatternSwitchCheckState(checkState); }, - async assertCreateButtonExists() { - await testSubjects.existOrFail('mlAnalyticsCreateJobFlyoutCreateButton'); + async assertStartJobCheckboxExists() { + await testSubjects.existOrFail('mlAnalyticsCreateJobWizardStartJobCheckbox'); + }, + + async assertStartJobCheckboxCheckState(expectedCheckState: boolean) { + const actualCheckState = + (await testSubjects.getAttribute( + 'mlAnalyticsCreateJobWizardStartJobCheckbox', + 'checked' + )) === 'true'; + expect(actualCheckState).to.eql( + expectedCheckState, + `Start job check state should be ${expectedCheckState} (got ${actualCheckState})` + ); }, - async assertCreateButtonMissing() { - await testSubjects.missingOrFail('mlAnalyticsCreateJobFlyoutCreateButton'); + async assertCreateButtonExists() { + await testSubjects.existOrFail('mlAnalyticsCreateJobWizardCreateButton'); }, async isCreateButtonDisabled() { - const isEnabled = await testSubjects.isEnabled('mlAnalyticsCreateJobFlyoutCreateButton'); + const isEnabled = await testSubjects.isEnabled('mlAnalyticsCreateJobWizardCreateButton'); return !isEnabled; }, async createAnalyticsJob(analyticsId: string) { - await testSubjects.click('mlAnalyticsCreateJobFlyoutCreateButton'); + await testSubjects.click('mlAnalyticsCreateJobWizardCreateButton'); await retry.tryForTime(5000, async () => { - await this.assertCreateButtonMissing(); - await this.assertStartButtonExists(); + await this.assertBackToManagementCardExists(); }); await mlApi.waitForDataFrameAnalyticsJobToExist(analyticsId); }, - async assertStartButtonExists() { - await testSubjects.existOrFail('mlAnalyticsCreateJobFlyoutStartButton'); - }, - - async assertStartButtonMissing() { - await testSubjects.missingOrFail('mlAnalyticsCreateJobFlyoutStartButton'); - }, - - async startAnalyticsJob() { - await testSubjects.click('mlAnalyticsCreateJobFlyoutStartButton'); - await retry.tryForTime(5000, async () => { - await this.assertStartButtonMissing(); - await this.assertCloseButtonExists(); - }); - }, - - async assertCloseButtonExists() { - await testSubjects.existOrFail('mlAnalyticsCreateJobFlyoutCloseButton'); - }, - - async closeCreateAnalyticsJobFlyout() { - await retry.tryForTime(10 * 1000, async () => { - await testSubjects.click('mlAnalyticsCreateJobFlyoutCloseButton'); - await testSubjects.missingOrFail('mlAnalyticsCreateJobFlyout'); - }); + async assertBackToManagementCardExists() { + await testSubjects.existOrFail('analyticsWizardCardManagement'); }, async getHeaderText() { @@ -395,5 +401,19 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( await this.assertExcludedFieldsSelection(job.analyzed_fields.excludes); await this.assertModelMemoryValue(job.model_memory_limit); }, + + async assertCreationCalloutMessagesExist() { + await testSubjects.existOrFail('analyticsWizardCreationCallout_0'); + await testSubjects.existOrFail('analyticsWizardCreationCallout_1'); + await testSubjects.existOrFail('analyticsWizardCreationCallout_2'); + }, + + async navigateToJobManagementPage() { + await retry.tryForTime(5000, async () => { + await this.assertCreationCalloutMessagesExist(); + }); + await testSubjects.click('analyticsWizardCardManagement'); + await testSubjects.existOrFail('mlPageDataFrameAnalytics'); + }, }; } diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts index d5f4ee63f615bd..60507f5ab33311 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts @@ -105,6 +105,7 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F } public async assertAnalyticsRowFields(analyticsId: string, expectedRow: object) { + await this.refreshAnalyticsTable(); const rows = await this.parseAnalyticsTable(); const analyticsRow = rows.filter((row) => row.id === analyticsId)[0]; expect(analyticsRow).to.eql( diff --git a/x-pack/test/functional/services/ml/job_source_selection.ts b/x-pack/test/functional/services/ml/job_source_selection.ts index a1857d882be3d3..8da7318b642ad8 100644 --- a/x-pack/test/functional/services/ml/job_source_selection.ts +++ b/x-pack/test/functional/services/ml/job_source_selection.ts @@ -34,6 +34,10 @@ export function MachineLearningJobSourceSelectionProvider({ getService }: FtrPro await this.selectSource(sourceName, 'mlPageJobTypeSelection'); }, + async selectSourceForAnalyticsJob(sourceName: string) { + await this.selectSource(sourceName, 'mlAnalyticsCreationContainer'); + }, + async selectSourceForIndexBasedDataVisualizer(sourceName: string) { await this.selectSource(sourceName, 'mlPageIndexDataVisualizer'); }, diff --git a/x-pack/test/functional/services/uptime/certificates.ts b/x-pack/test/functional/services/uptime/certificates.ts index fb7cb6191b0ae8..2ceab1ca89e54a 100644 --- a/x-pack/test/functional/services/uptime/certificates.ts +++ b/x-pack/test/functional/services/uptime/certificates.ts @@ -17,31 +17,49 @@ export function UptimeCertProvider({ getService }: FtrProviderContext) { await input.type(text); }; + const refreshApp = async () => { + await testSubjects.click('superDatePickerApplyTimeButton', 10000); + }; + return { + async isUptimeDataMissing() { + return retry.tryForTime(60 * 1000, async () => { + if (await testSubjects.exists('data-missing', { timeout: 0 })) { + await refreshApp(); + } + await testSubjects.missingOrFail('data-missing'); + }); + }, async hasViewCertButton() { return retry.tryForTime(15000, async () => { await testSubjects.existOrFail('uptimeCertificatesLink'); }); }, async certificateExists(cert: { certId: string; monitorId: string }) { - return retry.tryForTime(15000, async () => { + return retry.tryForTime(60 * 1000, async () => { + if (!(await testSubjects.exists(cert.certId))) { + await refreshApp(); + } await testSubjects.existOrFail(cert.certId); await testSubjects.existOrFail('monitor-page-link-' + cert.monitorId); }); }, async hasCertificates(expectedTotal?: number) { - return retry.tryForTime(15000, async () => { + return retry.tryForTime(60 * 1000, async () => { const totalCerts = await testSubjects.getVisibleText('uptimeCertTotal'); if (expectedTotal) { - expect(Number(totalCerts) === expectedTotal).to.eql(true); + expect(Number(totalCerts)).to.eql(expectedTotal); } else { + if (Number(totalCerts) < 1) { + await refreshApp(); + } expect(Number(totalCerts) > 0).to.eql(true); } }); }, async searchIsWorking(monId: string) { const self = this; - return retry.tryForTime(15000, async () => { + return retry.tryForTime(60 * 1000, async () => { await changeSearchField(monId); await self.hasCertificates(1); }); diff --git a/x-pack/test/functional/services/uptime/navigation.ts b/x-pack/test/functional/services/uptime/navigation.ts index d372bd53c081b9..7c5a4632f86278 100644 --- a/x-pack/test/functional/services/uptime/navigation.ts +++ b/x-pack/test/functional/services/uptime/navigation.ts @@ -26,7 +26,7 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv }; const refreshApp = async () => { - await testSubjects.click('superDatePickerApplyTimeButton'); + await testSubjects.click('superDatePickerApplyTimeButton', 10000); }; return { @@ -65,10 +65,15 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv }, goToCertificates: async () => { - return retry.try(async () => { - await testSubjects.click('uptimeCertificatesLink'); - await testSubjects.existOrFail('uptimeCertificatesPage'); - }); + if (!(await testSubjects.exists('uptimeCertificatesPage', { timeout: 0 }))) { + return retry.try(async () => { + if (await testSubjects.exists('uptimeCertificatesLink', { timeout: 0 })) { + await testSubjects.click('uptimeCertificatesLink', 10000); + } + await testSubjects.existOrFail('uptimeCertificatesPage'); + }); + } + return true; }, async loadDataAndGoToMonitorPage(dateStart: string, dateEnd: string, monitorId: string) { diff --git a/x-pack/test/kerberos_api_integration/config.ts b/x-pack/test/kerberos_api_integration/config.ts index 1e8e62e0484115..7b65d79e18e7dc 100644 --- a/x-pack/test/kerberos_api_integration/config.ts +++ b/x-pack/test/kerberos_api_integration/config.ts @@ -9,7 +9,7 @@ import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js')); + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); const kerberosKeytabPath = resolve( __dirname, diff --git a/x-pack/test/login_selector_api_integration/config.ts b/x-pack/test/login_selector_api_integration/config.ts index 33fa5df7cd1e08..ba7aadb121e825 100644 --- a/x-pack/test/login_selector_api_integration/config.ts +++ b/x-pack/test/login_selector_api_integration/config.ts @@ -12,7 +12,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { const kibanaAPITestsConfig = await readConfigFile( require.resolve('../../../test/api_integration/config.js') ); - const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js')); + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); const kibanaPort = xPackAPITestsConfig.get('servers.kibana.port'); const kerberosKeytabPath = resolve(__dirname, '../kerberos_api_integration/fixtures/krb5.keytab'); diff --git a/x-pack/test/observability_api_integration/common/config.ts b/x-pack/test/observability_api_integration/common/config.ts index 9e011a98bbfcd4..83dc597829a3cd 100644 --- a/x-pack/test/observability_api_integration/common/config.ts +++ b/x-pack/test/observability_api_integration/common/config.ts @@ -17,7 +17,7 @@ export function createTestConfig(settings: Settings) { return async ({ readConfigFile }: FtrConfigProviderContext) => { const xPackAPITestsConfig = await readConfigFile( - require.resolve('../../api_integration/config.js') + require.resolve('../../api_integration/config.ts') ); return { diff --git a/x-pack/test/oidc_api_integration/config.ts b/x-pack/test/oidc_api_integration/config.ts index a78d6c3febc2df..7a0d786e201301 100644 --- a/x-pack/test/oidc_api_integration/config.ts +++ b/x-pack/test/oidc_api_integration/config.ts @@ -9,7 +9,7 @@ import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js')); + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); const plugin = resolve(__dirname, './fixtures/oidc_provider'); const kibanaPort = xPackAPITestsConfig.get('servers.kibana.port'); const jwksPath = resolve(__dirname, './fixtures/jwks.json'); diff --git a/x-pack/test/page_load_metrics/config.ts b/x-pack/test/page_load_metrics/config.ts new file mode 100644 index 00000000000000..641099ff8e9346 --- /dev/null +++ b/x-pack/test/page_load_metrics/config.ts @@ -0,0 +1,42 @@ +/* + * 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 { resolve } from 'path'; + +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { PuppeteerTestRunner } from './runner'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const kibanaCommonTestsConfig = await readConfigFile( + require.resolve('../../../test/common/config.js') + ); + const xpackFunctionalTestsConfig = await readConfigFile( + require.resolve('../functional/config.js') + ); + + return { + ...kibanaCommonTestsConfig.getAll(), + + testRunner: PuppeteerTestRunner, + + esArchiver: { + directory: resolve(__dirname, 'es_archives'), + }, + + screenshots: { + directory: resolve(__dirname, 'screenshots'), + }, + + esTestCluster: { + ...xpackFunctionalTestsConfig.get('esTestCluster'), + serverArgs: [...xpackFunctionalTestsConfig.get('esTestCluster.serverArgs')], + }, + + kbnTestServer: { + ...xpackFunctionalTestsConfig.get('kbnTestServer'), + }, + }; +} diff --git a/x-pack/test/page_load_metrics/es_archives/default/data.json.gz b/x-pack/test/page_load_metrics/es_archives/default/data.json.gz new file mode 100644 index 00000000000000..5a5290ddf64478 Binary files /dev/null and b/x-pack/test/page_load_metrics/es_archives/default/data.json.gz differ diff --git a/x-pack/test/page_load_metrics/es_archives/default/mappings.json b/x-pack/test/page_load_metrics/es_archives/default/mappings.json new file mode 100644 index 00000000000000..c36f9576c4df12 --- /dev/null +++ b/x-pack/test/page_load_metrics/es_archives/default/mappings.json @@ -0,0 +1,2402 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "ae24d22d5986d04124cc6568f771066f", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "21c3ea0763beb1ecb0162529706b88c5", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "23d7aa4a720d4938ccde3983f87bd58d", + "maps-telemetry": "bfd39d88aadadb4be597ea984d433dbe", + "metrics-explorer-view": "428e319af3e822c80a84cf87123ca35c", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "296a89039fc4260292be36b1b005d8f2", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", + "url": "b675c3be8d76ecf029294d51dc7ec65d", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "properties": { + "agents": { + "properties": { + "dotnet": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "go": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "java": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "js-base": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "nodejs": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "python": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "ruby": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "rum-js": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + } + } + }, + "cardinality": { + "properties": { + "transaction": { + "properties": { + "name": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + }, + "user_agent": { + "properties": { + "original": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "counts": { + "properties": { + "agent_configuration": { + "properties": { + "all": { + "type": "long" + } + } + }, + "error": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "max_error_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "max_transaction_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "services": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "sourcemap": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "span": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "traces": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + } + } + }, + "has_any_services": { + "type": "boolean" + }, + "indices": { + "properties": { + "all": { + "properties": { + "total": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + } + } + }, + "store": { + "properties": { + "size_in_bytes": { + "type": "long" + } + } + } + } + } + } + }, + "shards": { + "properties": { + "total": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "ml": { + "properties": { + "all_jobs_count": { + "type": "long" + } + } + } + } + }, + "retainment": { + "properties": { + "error": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "span": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services_per_agent": { + "properties": { + "dotnet": { + "null_value": 0, + "type": "long" + }, + "go": { + "null_value": 0, + "type": "long" + }, + "java": { + "null_value": 0, + "type": "long" + }, + "js-base": { + "null_value": 0, + "type": "long" + }, + "nodejs": { + "null_value": 0, + "type": "long" + }, + "python": { + "null_value": 0, + "type": "long" + }, + "ruby": { + "null_value": 0, + "type": "long" + }, + "rum-js": { + "null_value": 0, + "type": "long" + } + } + }, + "tasks": { + "properties": { + "agent_configuration": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "agents": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "cardinality": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "groupings": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "indices_stats": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "processor_events": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "versions": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + } + } + }, + "version": { + "properties": { + "apm_server": { + "properties": { + "major": { + "type": "long" + }, + "minor": { + "type": "long" + }, + "patch": { + "type": "long" + } + } + } + } + } + } + }, + "application_usage_totals": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + } + } + }, + "application_usage_transactional": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + }, + "timestamp": { + "type": "date" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "defaultIndex": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "inventory-view": { + "properties": { + "accountId": { + "type": "keyword" + }, + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "legend": { + "properties": { + "palette": { + "type": "keyword" + }, + "reverseColors": { + "type": "boolean" + }, + "steps": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "region": { + "type": "keyword" + }, + "sort": { + "properties": { + "by": { + "type": "keyword" + }, + "direction": { + "type": "keyword" + } + } + }, + "time": { + "type": "long" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "bounds": { + "type": "geo_shape" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "properties": { + "attributesPerMap": { + "properties": { + "dataSourcesCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + }, + "emsVectorLayersCount": { + "dynamic": "true", + "type": "object" + }, + "layerTypesCount": { + "dynamic": "true", + "type": "object" + }, + "layersCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + } + } + }, + "indexPatternsWithGeoFieldCount": { + "type": "long" + }, + "indexPatternsWithGeoPointFieldCount": { + "type": "long" + }, + "indexPatternsWithGeoShapeFieldCount": { + "type": "long" + }, + "mapsTotalCount": { + "type": "long" + }, + "settings": { + "properties": { + "showMapVisualizationTypes": { + "type": "boolean" + } + } + }, + "timeCaptured": { + "type": "date" + } + } + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "forceInterval": { + "type": "boolean" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "type": "keyword" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "integer" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "type": "keyword" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "type": "keyword" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "properties": { + "certAgeThreshold": { + "type": "long" + }, + "certExpirationThreshold": { + "type": "long" + }, + "heartbeatIndices": { + "type": "keyword" + } + } + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "test", + "mappings": { + "properties": { + "foo": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/page_load_metrics/runner.ts b/x-pack/test/page_load_metrics/runner.ts new file mode 100644 index 00000000000000..05f293730f843a --- /dev/null +++ b/x-pack/test/page_load_metrics/runner.ts @@ -0,0 +1,33 @@ +/* + * 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 { CiStatsReporter } from '@kbn/dev-utils'; +import { capturePageLoadMetrics } from '@kbn/test'; +// @ts-ignore not TS yet +import getUrl from '../../../src/test_utils/get_url'; + +import { FtrProviderContext } from './../functional/ftr_provider_context'; + +export async function PuppeteerTestRunner({ getService }: FtrProviderContext) { + const log = getService('log'); + const config = getService('config'); + const esArchiver = getService('esArchiver'); + + await esArchiver.load('default'); + const metrics = await capturePageLoadMetrics(log, { + headless: true, + appConfig: { + url: getUrl.baseUrl(config.get('servers.kibana')), + username: config.get('servers.kibana.username'), + password: config.get('servers.kibana.password'), + }, + screenshotsDir: config.get('screenshots.directory'), + }); + const reporter = CiStatsReporter.fromEnv(log); + + log.debug('Report page load asset size'); + await reporter.metrics(metrics); +} diff --git a/x-pack/test/pki_api_integration/config.ts b/x-pack/test/pki_api_integration/config.ts index e67ab0c6197f67..5ce3111530dd99 100644 --- a/x-pack/test/pki_api_integration/config.ts +++ b/x-pack/test/pki_api_integration/config.ts @@ -10,7 +10,7 @@ import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js')); + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); const servers = { ...xPackAPITestsConfig.get('servers'), diff --git a/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/feature_usage.ts b/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/feature_usage.ts index 5c8fac9586e3ac..e16d55f8fad2c0 100644 --- a/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/feature_usage.ts +++ b/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/feature_usage.ts @@ -27,7 +27,14 @@ export default function ({ getService }: FtrProviderContext) { const response = await supertest.get('/api/licensing/feature_usage').expect(200); - expect(response.body).to.eql({ + const testFeaturesResponse = { + ...response.body, + features: response.body.features.filter((feature: { name: string }) => + feature.name.startsWith('Test feature ') + ), + }; + + expect(testFeaturesResponse).to.eql({ features: [ { last_used: null, diff --git a/x-pack/test/saml_api_integration/config.ts b/x-pack/test/saml_api_integration/config.ts index d2aef5561dd11c..1bc85a803a7c21 100644 --- a/x-pack/test/saml_api_integration/config.ts +++ b/x-pack/test/saml_api_integration/config.ts @@ -11,7 +11,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { const kibanaAPITestsConfig = await readConfigFile( require.resolve('../../../test/api_integration/config.js') ); - const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js')); + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); const kibanaPort = xPackAPITestsConfig.get('servers.kibana.port'); const idpPath = resolve(__dirname, '../../test/saml_api_integration/fixtures/idp_metadata.xml'); diff --git a/x-pack/test/saved_object_api_integration/common/config.ts b/x-pack/test/saved_object_api_integration/common/config.ts index fc04a9774e023e..378f27082d9e00 100644 --- a/x-pack/test/saved_object_api_integration/common/config.ts +++ b/x-pack/test/saved_object_api_integration/common/config.ts @@ -24,7 +24,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) functional: await readConfigFile(require.resolve('../../../../test/functional/config.js')), }, xpack: { - api: await readConfigFile(require.resolve('../../api_integration/config.js')), + api: await readConfigFile(require.resolve('../../api_integration/config.ts')), }, }; diff --git a/x-pack/test/spaces_api_integration/common/config.ts b/x-pack/test/spaces_api_integration/common/config.ts index 27dc67b92b4e24..89a49c7d3d4fa5 100644 --- a/x-pack/test/spaces_api_integration/common/config.ts +++ b/x-pack/test/spaces_api_integration/common/config.ts @@ -25,7 +25,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) functional: await readConfigFile(require.resolve('../../../../test/functional/config.js')), }, xpack: { - api: await readConfigFile(require.resolve('../../api_integration/config.js')), + api: await readConfigFile(require.resolve('../../api_integration/config.ts')), }, }; diff --git a/x-pack/test/token_api_integration/config.js b/x-pack/test/token_api_integration/config.js index efb2813b489bb5..31ec2f1bc6bfb0 100644 --- a/x-pack/test/token_api_integration/config.js +++ b/x-pack/test/token_api_integration/config.js @@ -5,7 +5,7 @@ */ export default async function ({ readConfigFile }) { - const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js')); + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); return { testFiles: [require.resolve('./auth')], 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 }; diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 9bba90291b8847..e0bf3b8f169462 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -27,9 +27,6 @@ "plugins/xpack_main/*": [ "x-pack/legacy/plugins/xpack_main/public/*" ], - "plugins/security/*": [ - "x-pack/legacy/plugins/security/public/*" - ], "plugins/spaces/*": [ "x-pack/legacy/plugins/spaces/public/*" ], diff --git a/x-pack/typings/hapi.d.ts b/x-pack/typings/hapi.d.ts index 6af723101fc223..253b639a52ff21 100644 --- a/x-pack/typings/hapi.d.ts +++ b/x-pack/typings/hapi.d.ts @@ -7,7 +7,6 @@ import 'hapi'; import { XPackMainPlugin } from '../legacy/plugins/xpack_main/server/xpack_main'; -import { SecurityPlugin } from '../legacy/plugins/security'; import { ActionsPlugin, ActionsClient } from '../plugins/actions/server'; import { AlertingPlugin, AlertsClient } from '../plugins/alerts/server'; import { TaskManager } from '../plugins/task_manager/server'; @@ -19,7 +18,6 @@ declare module 'hapi' { } interface PluginProperties { xpack_main: XPackMainPlugin; - security?: SecurityPlugin; actions?: ActionsPlugin; alerts?: AlertingPlugin; task_manager?: TaskManager; diff --git a/yarn.lock b/yarn.lock index 892fa1b5aa5671..100b4018112c74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4630,6 +4630,13 @@ dependencies: "@types/node" "*" +"@types/puppeteer@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-3.0.0.tgz#24cdcc131e319477608d893f0017e08befd70423" + integrity sha512-59+fkfHHXHzX5rgoXIMnZyzum7ZLx/Wc3fhsOduFThpTpKbzzdBHMZsrkKGLunimB4Ds/tI5lXTRLALK8Mmnhg== + dependencies: + "@types/node" "*" + "@types/q@^1.5.1": version "1.5.2" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" @@ -7695,6 +7702,15 @@ bl@^3.0.0: dependencies: readable-stream "^3.0.1" +bl@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.2.tgz#52b71e9088515d0606d9dd9cc7aa48dc1f98e73a" + integrity sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + blob@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" @@ -8208,6 +8224,14 @@ buffer@^5.1.0, buffer@^5.2.0: base64-js "^1.0.2" ieee754 "^1.1.4" +buffer@^5.2.1, buffer@^5.5.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" + integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -16676,7 +16700,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -20841,6 +20865,11 @@ mixin-object@^2.0.1: for-in "^0.1.3" is-extendable "^0.1.1" +mkdirp-classic@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + mkdirp@0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -23967,6 +23996,22 @@ puppeteer@^2.0.0: rimraf "^2.6.1" ws "^6.1.0" +puppeteer@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-3.3.0.tgz#95839af9fdc0aa4de7e5ee073a4c0adeb9e2d3d7" + integrity sha512-23zNqRltZ1PPoK28uRefWJ/zKb5Jhnzbbwbpcna2o5+QMn17F0khq5s1bdH3vPlyj+J36pubccR8wiNA/VE0Vw== + dependencies: + debug "^4.1.0" + extract-zip "^2.0.0" + https-proxy-agent "^4.0.0" + mime "^2.0.3" + progress "^2.0.1" + proxy-from-env "^1.0.0" + rimraf "^3.0.2" + tar-fs "^2.0.0" + unbzip2-stream "^1.3.3" + ws "^7.2.3" + q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -26302,6 +26347,13 @@ rimraf@^2.5.4, rimraf@^2.7.1: dependencies: glob "^7.1.3" +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + rimraf@~2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.0.3.tgz#f50a2965e7144e9afd998982f15df706730f56a9" @@ -28654,6 +28706,16 @@ tar-fs@^1.16.3: pump "^1.0.0" tar-stream "^1.1.2" +tar-fs@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5" + integrity sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.0.0" + tar-stream@^1.1.2, tar-stream@^1.5.2: version "1.5.5" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.5.5.tgz#5cad84779f45c83b1f2508d96b09d88c7218af55" @@ -28664,6 +28726,17 @@ tar-stream@^1.1.2, tar-stream@^1.5.2: readable-stream "^2.0.0" xtend "^4.0.0" +tar-stream@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.2.tgz#6d5ef1a7e5783a95ff70b69b97455a5968dc1325" + integrity sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q== + dependencies: + bl "^4.0.1" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + tar-stream@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.0.tgz#d1aaa3661f05b38b5acc9b7020efdca5179a2cc3" @@ -28962,7 +29035,7 @@ through2@~2.0.3: readable-stream "~2.3.6" xtend "~4.0.1" -through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@~2.3.4, through@~2.3.6, through@~2.3.8: +through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.8, through@~2.3.4, through@~2.3.6, through@~2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -30151,6 +30224,14 @@ unbzip2-stream@^1.0.9: buffer "^3.0.1" through "^2.3.6" +unbzip2-stream@^1.3.3: + version "1.4.2" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.2.tgz#84eb9e783b186d8fb397515fbb656f312f1a7dbf" + integrity sha512-pZMVAofMrrHX6Ik39hCk470kulCbmZ2SWfQLPmTWqfJV/oUm0gn1CblvHdUu4+54Je6Jq34x8kY6XjTy6dMkOg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" + unc-path-regex@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" @@ -32015,6 +32096,11 @@ ws@^7.0.0: dependencies: async-limiter "^1.0.0" +ws@^7.2.3: + version "7.3.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd" + integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w== + ws@~3.3.1: version "3.3.3" resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"