]: NP[K] };
+
+type ExtendedObjectType = ObjectType<
+ ExtendedProps
+>;
+
+type ExtendedObjectTypeOptions
= ObjectTypeOptions<
+ ExtendedProps
+>;
+
interface UnknownOptions {
/**
* Options for dealing with unknown keys:
@@ -61,10 +81,13 @@ export type ObjectTypeOptions
= TypeOptions extends Type> {
- private props: Record;
+ private props: P;
+ private options: ObjectTypeOptions;
+ private propSchemas: Record;
- constructor(props: P, { unknowns = 'forbid', ...typeOptions }: ObjectTypeOptions = {}) {
+ constructor(props: P, options: ObjectTypeOptions
= {}) {
const schemaKeys = {} as Record;
+ const { unknowns = 'forbid', ...typeOptions } = options;
for (const [key, value] of Object.entries(props)) {
schemaKeys[key] = value.getSchema();
}
@@ -77,7 +100,93 @@ export class ObjectType extends Type>
.options({ stripUnknown: { objects: unknowns === 'ignore' } });
super(schema, typeOptions);
- this.props = schemaKeys;
+ this.props = props;
+ this.propSchemas = schemaKeys;
+ this.options = options;
+ }
+
+ /**
+ * Return a new `ObjectType` instance extended with given `newProps` properties.
+ * Original properties can be deleted from the copy by passing a `null` or `undefined` value for the key.
+ *
+ * @example
+ * How to add a new key to an object schema
+ * ```ts
+ * const origin = schema.object({
+ * initial: schema.string(),
+ * });
+ *
+ * const extended = origin.extends({
+ * added: schema.number(),
+ * });
+ * ```
+ *
+ * How to remove an existing key from an object schema
+ * ```ts
+ * const origin = schema.object({
+ * initial: schema.string(),
+ * toRemove: schema.number(),
+ * });
+ *
+ * const extended = origin.extends({
+ * toRemove: undefined,
+ * });
+ * ```
+ *
+ * How to override the schema's options
+ * ```ts
+ * const origin = schema.object({
+ * initial: schema.string(),
+ * }, { defaultValue: { initial: 'foo' }});
+ *
+ * const extended = origin.extends({
+ * added: schema.number(),
+ * }, { defaultValue: { initial: 'foo', added: 'bar' }});
+ *
+ * @remarks
+ * `extends` only support extending first-level properties. It's currently not possible to perform deep/nested extensions.
+ *
+ * ```ts
+ * const origin = schema.object({
+ * foo: schema.string(),
+ * nested: schema.object({
+ * a: schema.string(),
+ * b: schema.string(),
+ * }),
+ * });
+ *
+ * const extended = origin.extends({
+ * nested: schema.object({
+ * c: schema.string(),
+ * }),
+ * });
+ *
+ * // TypeOf is `{ foo: string; nested: { c: string } }`
+ * ```
+ */
+ public extends(
+ newProps: NP,
+ newOptions?: ExtendedObjectTypeOptions
+ ): ExtendedObjectType
{
+ const extendedProps = Object.entries({
+ ...this.props,
+ ...newProps,
+ }).reduce((memo, [key, value]) => {
+ if (value !== null && value !== undefined) {
+ return {
+ ...memo,
+ [key]: value,
+ };
+ }
+ return memo;
+ }, {} as ExtendedProps
);
+
+ const extendedOptions = {
+ ...this.options,
+ ...newOptions,
+ } as ExtendedObjectTypeOptions
;
+
+ return new ObjectType(extendedProps, extendedOptions);
}
protected handleError(type: string, { reason, value }: Record) {
@@ -95,10 +204,10 @@ export class ObjectType extends Type>
}
validateKey(key: string, value: any) {
- if (!this.props[key]) {
+ if (!this.propSchemas[key]) {
throw new Error(`${key} is not a valid part of this schema`);
}
- const { value: validatedValue, error } = this.props[key].validate(value);
+ const { value: validatedValue, error } = this.propSchemas[key].validate(value);
if (error) {
throw new ValidationError(error as any, key);
}
diff --git a/src/dev/build/build_distributables.js b/src/dev/build/build_distributables.js
index 3a8709893565..66f0c0355c2d 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 000000000000..f620f12b17d8
--- /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 000000000000..e970ac5ec044
--- /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 ee9dc159de47..ddc6d000bca1 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 8105fa8a7d5d..bafb5a2fe115 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/storybook/aliases.ts b/src/dev/storybook/aliases.ts
index 4ed8f8e7db19..2f785896da8d 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 0d1b69778263..b7af6a73e1bc 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 08a347fbf729..879fab206b99 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 229bfb1978a4..9c2ab0819776 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 = {
@@ -524,6 +542,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 +552,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 9d02ad67b393..ee92eda064aa 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 a7492e538b3a..7c25c6aa3166 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 8dd0a766da97..a59d1e8c546d 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 7e25d80c9d61..5d4cc851cf45 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 9b7cec2f182b..9a2610a82b97 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 19d9ad34e729..dcd07fe394c7 100644
--- a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx
+++ b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx
@@ -30,7 +30,7 @@ import React from 'react';
import { Subscription } from 'rxjs';
import ReactGridLayout, { Layout } from 'react-grid-layout';
import { GridData } from '../../../../common';
-import { ViewMode, EmbeddableChildPanel } from '../../../embeddable_plugin';
+import { ViewMode, EmbeddableChildPanel, EmbeddableStart } from '../../../embeddable_plugin';
import { DASHBOARD_GRID_COLUMN_COUNT, DASHBOARD_GRID_HEIGHT } from '../dashboard_constants';
import { DashboardPanelState } from '../types';
import { withKibana } from '../../../../../kibana_react/public';
@@ -115,6 +115,7 @@ const ResponsiveSizedGrid = sizeMe(config)(ResponsiveGrid);
export interface DashboardGridProps extends ReactIntl.InjectedIntlProps {
kibana: DashboardReactContextValue;
+ PanelComponent: EmbeddableStart['EmbeddablePanel'];
container: DashboardContainer;
}
@@ -271,14 +272,7 @@ class DashboardGridUi extends React.Component {
);
diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx
index 1b257880b940..25e451dc7f79 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 ae239bc27fdb..429837583b64 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 66a96e3e6e12..8ec72dc1f9a7 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 d146d81973d0..5fa3c67dea40 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 0a82cf03bdb4..ff8fc5b11b26 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 8dad9fc206e7..e0c26170c290 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 fe36d5b12e87..31c5ea41bf5a 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 a483b5a1e4f9..1d844bca3f89 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 8b26564814af..754234bdeb78 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 ef3b0a1503a9..ecf9c7d19108 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 9e50d47bb262..f00261e00971 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 c04a371066de..9325485bce75 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 ef7a0e8c3a5a..a6a45a26f06b 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 22c7e90c0613..3ddc8d0b68a5 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 0c946fc6e185..554003932375 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 43404c32cb3d..3d54009d0fdc 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 66e8d5a6e739..06b5cbdfdfdf 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 ebb4e9d583bc..dcdb528ac8b7 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 3c69a498e74c..878142906f54 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 d58a0eb45c04..60a49a4bd50f 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 a71eb7580cf0..b7827d2c8de0 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 06e4c1c8be6d..4e394445b75a 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 50dc35ea955e..772715353725 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 413163ed059a..df2fbc8e5a8f 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 d5c97d0c95c5..8a5596f669cb 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 3f3f13bb955c..4052c0b39015 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 d04df4f8aac6..c1fad17f488d 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 9d976784329c..30fcdd9d83a3 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 8b8156b4519d..05a74b3e6205 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 3f1c2b9b3b73..5e40712067bb 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 4809d76a46f5..f9b62fdd4fc6 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 f0c43bd2e74c..60bdc9ed6473 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 c619c9b17d9a..436b52274462 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 e99e13ba33d1..61d3568350b6 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 304c1c4d63f5..fed2c52bc491 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 ce98f6ab2a7b..dc61e1940663 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 38f4ce73713c..b926739112e0 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 a54a25acc591..43dba150bf8d 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 546365b89d9b..94138f60b52b 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 c44e1faeb8e7..053fca7d5773 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 f579adbc0c7e..5f2d4c00cd6b 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 433cb652ee5c..f65bf97e391e 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 7723254f3aa5..18ed632e0a8e 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 72a29e377ac5..831d23864d22 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 446320b09757..81e352fea51b 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 9f3437161541..157716b38f52 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 df7a7b9cf4d0..8c9d0df2ed89 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 5af62be29520..de978c7968ae 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/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js
index caba094bd098..88885b3eb211 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 60dfb69e85e7..82bfcc8bc42f 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 99a5547ed076..5a319d30b251 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/embeddable/public/lib/containers/embeddable_child_panel.test.tsx b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx
index b046376a304a..e29e941e898f 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 70628665e6e8..be8ff2c95fe0 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 31e14a0af59d..913c3a0b3082 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 6453046b86e2..5fefa1fc9072 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 f5487c381cfc..9da0b7602c4f 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 ebb76c743393..ec92f334267f 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 4cd01abaf799..490f0c00c7c4 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 a9cb83504d95..311efae49f73 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 6bea4fe46a49..d64ff94d7180 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 3b3f86e579f1..2545bbcb5114 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 2f7f07a0e454..fa1327b3fcd0 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 4d2cec158f63..774b23af11ac 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 edb96f119385..b6205a8731df 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 a1b7289efee2..c97f19f59d34 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 69be069272f7..0fdf3d9b13e3 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
}
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 f67dcf42adff..bd7626a49333 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 1bc979399b1b..a624ff72ead6 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 65b28fb83327..bc0e368fbdab 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 972f937ad109..84da28718e32 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 71e82770bfa0..308579126eeb 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 0c53ddd3f0ba..a96890d4d150 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 93a4eaba4ad9..9ada39e35958 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 1e28965a3579..c113db76332b 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 110deb6a9bc1..7d8b60a7e459 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 043bd52d87aa..b899814f2fe1 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 42b8681f142e..b427e5f12cad 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 000000000000..9129f060c5ee
--- /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 81794c31527a..45c750de05ae 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 c4aa4c262edb..c4267c9a36f7 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 e475684ed593..0bbf862216ed 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 70c3bc2c1ed0..05644eddc5fc 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 ef64eccfea31..3546fa405649 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 15055022af8a..0761b8862e8e 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 cea92b1db93a..1a970e505b7c 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 228cfa1e9e49..e8e8d9203411 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/dashboard/create_and_add_embeddables.js b/test/functional/apps/dashboard/create_and_add_embeddables.js
index ba715f3472b9..f5c2496a9a5a 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 6bc34a8b998a..c931e6763f48 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 b356d01cdb63..27c149b9e0e0 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 bd427577cd78..42b82486dc13 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 a052aad9450f..ae78761fef0d 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/x-pack/.gitignore b/x-pack/.gitignore
index 92597a101c03..305032926579 100644
--- a/x-pack/.gitignore
+++ b/x-pack/.gitignore
@@ -5,6 +5,8 @@
/test/functional/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 e453abb3a0f9..d17e5d1a74a3 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 744d97ca02c7..03d2e3287c0f 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/examples/ui_actions_enhanced_examples/kibana.json b/x-pack/examples/ui_actions_enhanced_examples/kibana.json
index e220cdd5cd29..7f3ed80ed62f 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"],
"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 847035403da0..bfe853241ae1 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/drilldown.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/drilldown.tsx
index fef01c9640f0..91e05d16b436 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 = (
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 20267a8b7292..4810fb2d6ad8 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 0d4f274caf57..3f28854351f5 100644
--- a/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts
+++ b/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts
@@ -9,7 +9,7 @@ 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';
@@ -17,19 +17,19 @@ import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/
export interface SetupDependencies {
data: DataPublicPluginSetup;
- advancedUiActions: AdvancedUiActionsSetup;
+ uiActionsEnhanced: AdvancedUiActionsSetup;
}
export interface StartDependencies {
data: DataPublicPluginStart;
- advancedUiActions: AdvancedUiActionsStart;
+ 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 301caad683dd..3414f09deed4 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 bb1f68e1c03b..80599f38d982 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 589f34ac7460..1bf9bbb22b35 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 41371fcbc4c6..addeef34f63b 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/readme.md b/x-pack/plugins/apm/readme.md
index ceed5e6c3971..cb694712d7c9 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 c7a17197ca77..892f8f0ddd10 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 7256657903aa..14409ae166a8 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 2dd116d5ada0..ad368a912cd8 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 ef5bfb70d4b3..25278adcf452 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 4025d4deaf99..5a3e3904f4f2 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 93912b7b0517..11bee4608857 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 f416ca97f711..37211ea53717 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 c258a4148f84..413f5a7afe35 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 555acf1fca5f..309e6cbf53a3 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 ec3a78e97eae..9a4ecb2d4bfb 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 cccacf701a9a..e831f87baa11 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 f5926cd6961c..4325e3309b89 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 c94d19d28e6d..6ce7dccd3a3e 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 7ff84a75dd52..26a69132cffb 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 c493e8ce8678..3a511c7b5a17 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 678c054aa322..1614f94b488f 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 a186feec3392..5fde4fc79e43 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 0f7f0cb22760..32cbec795d09 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 3c9d2d2a86fb..45cf7365ebd9 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 c9cb0b0eb1cb..d585fa0692e8 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 add8b748afee..be048bf92060 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 84c1a04a71d1..87f886817517 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 38168377b02b..1813851d728d 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 0108e04df9c9..32176241c102 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 780a1d5d8987..5663671de7bd 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 f8b3a9dfb92d..5c5d98d75295 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 e93674ba650a..fdc42585a80c 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 d26acb4459a7..e6413ac03aea 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 924605be332b..4f5c316f2fc1 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/graph/server/routes/search.ts b/x-pack/plugins/graph/server/routes/search.ts
index ffca273d66c7..645e6b520013 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 bfe1bbb04338..28bc8671f29e 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 789de0f528b1..03538fad9aa8 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 2eb635e19be4..7bf3f96e2b2e 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 e5bce31ee6de..da461609f0b8 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 18e5edb5c225..8e7755a65af3 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 d195ce46c2f5..a7ac2ebf9bb0 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 5260dc64d0c9..98bd3077670a 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 c9a279e90d0e..8f6a8dddeb19 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 cf00e0f6d14e..11c25ffbb590 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 e0db9cd58ee2..6250ef0dc247 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 95545b6c66f5..50b35fc76721 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 6e935a526330..88067d479f7e 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 966e2e8e6483..526b9fede2a6 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 788e96ee895e..7696b3832c51 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 459eda7552c8..3792e322ae40 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 33f7fbe45182..16eb544c56a0 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 33a83d1e9335..249881f668d9 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 221d1b009ced..056101061a82 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 eee35dc1ab46..5a7db8ef50ab 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 c37088982f20..f113aa44d058 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 a87412ef9295..06babb0db3bd 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 7b266034bc33..387887239aaf 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 0cdfaae70f15..52e26e6d3e89 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 7129e536287c..08102ae93cc0 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 000000000000..1c95cca3fead
--- /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/security_solution/public/timelines/components/timeline/and_or_badge/__examples__/index.stories.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts
similarity index 50%
rename from x-pack/plugins/security_solution/public/timelines/components/timeline/and_or_badge/__examples__/index.stories.tsx
rename to x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts
index f34e9ee21453..dcaba319bb21 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/and_or_badge/__examples__/index.stories.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts
@@ -3,10 +3,6 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { storiesOf } from '@storybook/react';
-import React from 'react';
-import { AndOrBadge } from '..';
-storiesOf('components/AndOrBadge', module)
- .add('and', () => )
- .add('or', () => );
+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 000000000000..78e33d7940bd
--- /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 000000000000..519120b559e7
--- /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 000000000000..ec2956973d4f
--- /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 000000000000..a8499df45ce2
--- /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 000000000000..92fedd5d68f0
--- /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 ed403276af56..9f51f114176f 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 db0833ea0323..fc3d5125e306 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 b69e441feb17..8bdd230f8995 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 27341685f3dc..f567b9835d53 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 9ad26d0af802..d3e539989bc9 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 181707b3661b..3961942b83ea 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 fe118b118108..a999c58f5bb4 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 1409fa8af2ce..26e74847e3e0 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 3dc31482b494..b5cc00ad6d8c 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 b18a8d88d3a4..12ec005258a6 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 81d7aa1b4978..5b2a0d8722e4 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 491a686f8117..6ab28e902112 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 055c32d5cd5e..e2e93bfb365d 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 f4c7332a88e1..db5cfb1416de 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 ef6486eac0fd..afee0c049818 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 7ad1d943a989..1471efbd21e1 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 7c0c53579563..79c276a1e58f 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 233a34a67d1e..a282a742d614 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 8b3903f2ee3b..2c98a568d16d 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 f0c2466b25b0..50c42544b8bd 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 5e83a976bd7a..635dce93f002 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 61f2f95fe20a..0bcb2464f8d7 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 f1a2edd2d554..33f4f46681a2 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 ffa59a6fb6bc..1349d33983fc 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 e665e8b8dd32..defc142d4976 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 f9a577e001c6..3000c9321b3b 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 23cf9e7ff818..cd25cb572951 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 60b4266edadb..70d7f16b0f7f 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 76e65afc595c..ba577660d865 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 4f6114241367..84f431cf2b3b 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 f13e435cd1d5..17a50c6c5f68 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 75b6b5d66f1d..45c7507160e9 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 d87761d3dabe..8fdb71de2dfe 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 af282d53273d..c86b716b2f49 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 4eeef560dd6d..7b104ea372ae 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 89a0018f9040..5508c021d331 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 7633e07e8f3d..0b4e6d27b96e 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 400902c152c9..58343e26153c 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 000000000000..f957dcab2e87
--- /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 000000000000..a9c8b6d4040a
--- /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 000000000000..8b137ac72361
--- /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 000000000000..144a06210600
--- /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 000000000000..6a19e55b533c
--- /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 000000000000..dfe7969d8a6d
--- /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 000000000000..e437d27372a3
--- /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 000000000000..4da656156287
--- /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 000000000000..ad540285e49f
--- /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 000000000000..220910535aaf
--- /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 000000000000..6603af9aa302
--- /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 000000000000..9446dfd4ed52
--- /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 000000000000..ba67a6f080d4
--- /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 000000000000..f31c9cd28f65
--- /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 000000000000..fe13cc1d6edf
--- /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 000000000000..856358538b26
--- /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 000000000000..6e95a0a24657
--- /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 000000000000..2dda5f5d819b
--- /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 000000000000..01c8e4bff934
--- /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 000000000000..a40813ed2fc3
--- /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 000000000000..a4d86b48006e
--- /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 000000000000..67f8472e7ad1
--- /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 000000000000..6cadd87d97e2
--- /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 000000000000..efd7c0634bed
--- /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 000000000000..5199fa1b6e4c
--- /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 000000000000..e8f25584201e
--- /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 000000000000..7e2d651439ae
--- /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 000000000000..def862b85916
--- /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 f95e6a93058b..8c158c1ca14a 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 72514c91ff58..295a3988e1b5 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 4a99c042b108..bb012a219085 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 48eb95948bb1..17b905cab135 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 000000000000..14ff9de7ded4
--- /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 10565fb4d7a9..8e9b09ef41fa 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 952bd48468b0..595a1cf73e18 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 66e4103f5bb4..c42e03b584a5 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 e5e56c084f7b..8ec415bc2abb 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 4ff7deab34f2..387ce89ee412 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 1ec767d014a2..c4cbe149f88b 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 d68c0342ac85..97b4043c9fd6 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 000000000000..68af9a2a49ca
--- /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 552c15a408b6..9b6bcc25c8c7 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 efcec8cf2b95..717ed3ba64c3 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 9c5d663a6e4a..91661ea3fd3f 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 2b23eca1ab5c..1915a4ce6516 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 baecf7df9064..250c7255f5b9 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 e48ce9f9a1d9..74a7b432de26 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 0b2469c10357..e6b4e4ccf858 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 73133da5a006..24383028e558 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 63cd4440ecf8..24863559212f 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 856e59702963..f7a4d03a2645 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 dfabfa98f8cb..b1234a6ddf0b 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 6f227d00974c..ddcf94079ade 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 de5ddb2503b2..4b17cc669efe 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 0434da3d11fe..83aa23de6766 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 a9e4366f4eda..3f997a703bef 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 41567a04fe03..0bb7785259c0 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 6784052ef433..344cde9c7825 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 98110a83103a..6821c163d817 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 afb8b6ec5dbe..43387d913e6f 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 77f4455d813c..15888733ec42 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 98342a8494e3..ac5c2a72b966 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 1c1e0ed781f1..c7323509c00d 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 779b852195b0..ec48c727a573 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 aead8cb07897..1036997ca821 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 000000000000..978c985cfe82
--- /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 000000000000..989784a1436d
--- /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 72937c15756a..183ad9169a12 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 325205345476..000000000000
--- 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 cf970a561b93..d5c1323354f8 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 b023c12d35b7..06f064a379fe 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 f3b2881e79ec..5a15290a7f1a 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 fff4345c7240..e21203e60b88 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 22e7830d20e2..8e54794494a9 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 5cd2eac20094..d73adde66a49 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 000000000000..46796fa73ef2
--- /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 000000000000..1bc1e664981b
--- /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 000000000000..6ed42145abd7
--- /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 000000000000..a3e1f35ee382
--- /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 72a946d6c515..c2d99433b034 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 3e30ff9447f3..e01c608e5f30 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 bdda0be9b15a..c8f47aaae7b5 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 d7710bf669ce..bec60fa149bc 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 5db83375afa9..d83cf92bcaa0 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 b0c74b98ee19..1a93d6701e25 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 e43072b95c90..5721a2699d15 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 29fbe3af21b9..6acfd06a0309 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 b5cd8f2dec0a..482794804685 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 3fcb00d87958..6c8c5e3f5180 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 57d41b655490..b17a5aa28ac6 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 45b5cf2526e1..816f9b77115e 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 e39ee38d71da..b82d1c0a36ab 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 1f474630fd6e..b8f81f6d7e5f 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 ad71059984a8..778c6bd92bc7 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 9632ddfeadc0..0c58f5620964 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(() => (
(
+ ({ eui: euiLightVars, darkMode: true })}>
+
+
+ ))
+ .add('or', () => (
+ ({ eui: euiLightVars, darkMode: true })}>
+
+
+ ))
+ .add('antennas', () => (
+ ({ eui: euiLightVars, darkMode: true })}>
+
+
+
+
+
+ {sampleText}
+
+
+
+ ));
diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx
new file mode 100644
index 000000000000..ed918a59a514
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx
@@ -0,0 +1,48 @@
+/*
+ * 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 { ThemeProvider } from 'styled-components';
+import { mount } from 'enzyme';
+import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
+
+import { AndOrBadge } from './';
+
+describe('AndOrBadge', () => {
+ test('it renders top and bottom antenna bars when "includeAntennas" is true', () => {
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('AND');
+ expect(wrapper.find('EuiFlexItem[data-test-subj="andOrBadgeBarTop"]')).toHaveLength(1);
+ expect(wrapper.find('EuiFlexItem[data-test-subj="andOrBadgeBarBottom"]')).toHaveLength(1);
+ });
+
+ test('it renders "and" when "type" is "and"', () => {
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('AND');
+ expect(wrapper.find('EuiFlexItem[data-test-subj="and-or-badge-bar"]')).toHaveLength(0);
+ });
+
+ test('it renders "or" when "type" is "or"', () => {
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('OR');
+ expect(wrapper.find('EuiFlexItem[data-test-subj="and-or-badge-bar"]')).toHaveLength(0);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.tsx
new file mode 100644
index 000000000000..ba3f880d9757
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.tsx
@@ -0,0 +1,108 @@
+/*
+ * 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 { EuiFlexGroup, EuiBadge, EuiFlexItem } from '@elastic/eui';
+import React from 'react';
+import styled, { css } from 'styled-components';
+
+import * as i18n from './translations';
+
+const AndOrBadgeAntenna = styled(EuiFlexItem)`
+ ${({ theme }) => css`
+ background: ${theme.eui.euiColorLightShade};
+ position: relative;
+ width: 2px;
+ &:after {
+ background: ${theme.eui.euiColorLightShade};
+ content: '';
+ height: 8px;
+ right: -4px;
+ position: absolute;
+ width: 9px;
+ clip-path: circle();
+ }
+ &.topAndOrBadgeAntenna {
+ &:after {
+ top: -1px;
+ }
+ }
+ &.bottomAndOrBadgeAntenna {
+ &:after {
+ bottom: -1px;
+ }
+ }
+ &.euiFlexItem {
+ margin: 0 12px 0 0;
+ }
+ `}
+`;
+
+const EuiFlexItemWrapper = styled(EuiFlexItem)`
+ &.euiFlexItem {
+ margin: 0 12px 0 0;
+ }
+`;
+
+const RoundedBadge = (styled(EuiBadge)`
+ align-items: center;
+ border-radius: 100%;
+ display: inline-flex;
+ font-size: 9px;
+ height: 34px;
+ justify-content: center;
+ margin: 0 5px 0 5px;
+ padding: 7px 6px 4px 6px;
+ user-select: none;
+ width: 34px;
+ .euiBadge__content {
+ position: relative;
+ top: -1px;
+ }
+ .euiBadge__text {
+ text-overflow: clip;
+ }
+` as unknown) as typeof EuiBadge;
+
+RoundedBadge.displayName = 'RoundedBadge';
+
+export type AndOr = 'and' | 'or';
+
+/** Displays AND / OR in a round badge */
+// Ref: https://github.com/elastic/eui/issues/1655
+export const AndOrBadge = React.memo<{ type: AndOr; includeAntennas?: boolean }>(
+ ({ type, includeAntennas = false }) => {
+ const getBadge = () => (
+
+ {type === 'and' ? i18n.AND : i18n.OR}
+
+ );
+
+ const getBadgeWithAntennas = () => (
+
+
+ {getBadge()}
+
+
+ );
+
+ return includeAntennas ? getBadgeWithAntennas() : getBadge();
+ }
+);
+
+AndOrBadge.displayName = 'AndOrBadge';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/and_or_badge/translations.ts b/x-pack/plugins/security_solution/public/common/components/and_or_badge/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/timelines/components/timeline/and_or_badge/translations.ts
rename to x-pack/plugins/security_solution/public/common/components/and_or_badge/translations.ts
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 759274e3a4ff..6fe15310fc88 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/exceptions/__examples__/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/__examples__/index.stories.tsx
new file mode 100644
index 000000000000..b6620ed103bc
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/__examples__/index.stories.tsx
@@ -0,0 +1,118 @@
+/*
+ * 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 { storiesOf } from '@storybook/react';
+import React from 'react';
+import { ThemeProvider } from 'styled-components';
+import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
+
+import { ExceptionItem } from '../viewer';
+import { Operator } from '../types';
+import { getExceptionItemMock } from '../mocks';
+
+storiesOf('components/exceptions', module)
+ .add('ExceptionItem/with os', () => {
+ const payload = getExceptionItemMock();
+ payload.description = '';
+ payload.comments = [];
+ payload.entries = [
+ {
+ field: 'actingProcess.file.signer',
+ type: 'match',
+ operator: Operator.INCLUSION,
+ value: 'Elastic, N.V.',
+ },
+ ];
+
+ return (
+ ({ eui: euiLightVars, darkMode: false })}>
+ {}}
+ handleEdit={() => {}}
+ />
+
+ );
+ })
+ .add('ExceptionItem/with description', () => {
+ const payload = getExceptionItemMock();
+ payload._tags = [];
+ payload.comments = [];
+ payload.entries = [
+ {
+ field: 'actingProcess.file.signer',
+ type: 'match',
+ operator: Operator.INCLUSION,
+ value: 'Elastic, N.V.',
+ },
+ ];
+
+ return (
+ ({ eui: euiLightVars, darkMode: false })}>
+ {}}
+ handleEdit={() => {}}
+ />
+
+ );
+ })
+ .add('ExceptionItem/with comments', () => {
+ const payload = getExceptionItemMock();
+ payload._tags = [];
+ payload.description = '';
+ payload.entries = [
+ {
+ field: 'actingProcess.file.signer',
+ type: 'match',
+ operator: Operator.INCLUSION,
+ value: 'Elastic, N.V.',
+ },
+ ];
+
+ return (
+ ({ eui: euiLightVars, darkMode: false })}>
+ {}}
+ handleEdit={() => {}}
+ />
+
+ );
+ })
+ .add('ExceptionItem/with nested entries', () => {
+ const payload = getExceptionItemMock();
+ payload._tags = [];
+ payload.description = '';
+ payload.comments = [];
+
+ return (
+ ({ eui: euiLightVars, darkMode: false })}>
+ {}}
+ handleEdit={() => {}}
+ />
+
+ );
+ })
+ .add('ExceptionItem/with everything', () => {
+ const payload = getExceptionItemMock();
+
+ return (
+ ({ eui: euiLightVars, darkMode: false })}>
+ {}}
+ handleEdit={() => {}}
+ />
+
+ );
+ });
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx
new file mode 100644
index 000000000000..223eabb0ea4e
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx
@@ -0,0 +1,467 @@
+/*
+ * 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 { mount } from 'enzyme';
+import moment from 'moment-timezone';
+
+import {
+ getOperatorType,
+ getExceptionOperatorSelect,
+ determineIfIsNested,
+ getFormattedEntries,
+ formatEntry,
+ getOperatingSystems,
+ getTagsInclude,
+ getDescriptionListContent,
+ getFormattedComments,
+} from './helpers';
+import {
+ OperatorType,
+ Operator,
+ NestedExceptionEntry,
+ FormattedEntry,
+ DescriptionListItem,
+} from './types';
+import {
+ isOperator,
+ isNotOperator,
+ isOneOfOperator,
+ isNotOneOfOperator,
+ isInListOperator,
+ isNotInListOperator,
+ existsOperator,
+ doesNotExistOperator,
+} from './operators';
+import { getExceptionItemEntryMock, getExceptionItemMock } from './mocks';
+
+describe('Exception helpers', () => {
+ beforeEach(() => {
+ moment.tz.setDefault('UTC');
+ });
+
+ afterEach(() => {
+ moment.tz.setDefault('Browser');
+ });
+
+ describe('#getOperatorType', () => {
+ test('returns operator type "match" if entry.type is "match"', () => {
+ const payload = getExceptionItemEntryMock();
+ payload.type = 'match';
+ const operatorType = getOperatorType(payload);
+
+ expect(operatorType).toEqual(OperatorType.PHRASE);
+ });
+
+ test('returns operator type "match" if entry.type is "nested"', () => {
+ const payload = getExceptionItemEntryMock();
+ payload.type = 'nested';
+ const operatorType = getOperatorType(payload);
+
+ expect(operatorType).toEqual(OperatorType.PHRASE);
+ });
+
+ test('returns operator type "match_any" if entry.type is "match_any"', () => {
+ const payload = getExceptionItemEntryMock();
+ payload.type = 'match_any';
+ const operatorType = getOperatorType(payload);
+
+ expect(operatorType).toEqual(OperatorType.PHRASES);
+ });
+
+ test('returns operator type "list" if entry.type is "list"', () => {
+ const payload = getExceptionItemEntryMock();
+ payload.type = 'list';
+ const operatorType = getOperatorType(payload);
+
+ expect(operatorType).toEqual(OperatorType.LIST);
+ });
+
+ test('returns operator type "exists" if entry.type is "exists"', () => {
+ const payload = getExceptionItemEntryMock();
+ payload.type = 'exists';
+ const operatorType = getOperatorType(payload);
+
+ expect(operatorType).toEqual(OperatorType.EXISTS);
+ });
+ });
+
+ describe('#getExceptionOperatorSelect', () => {
+ test('it returns "isOperator" when "operator" is "included" and operator type is "match"', () => {
+ const payload = getExceptionItemEntryMock();
+ const result = getExceptionOperatorSelect(payload);
+
+ expect(result).toEqual(isOperator);
+ });
+
+ test('it returns "isNotOperator" when "operator" is "excluded" and operator type is "match"', () => {
+ const payload = getExceptionItemEntryMock();
+ payload.operator = Operator.EXCLUSION;
+ const result = getExceptionOperatorSelect(payload);
+
+ expect(result).toEqual(isNotOperator);
+ });
+
+ test('it returns "isOneOfOperator" when "operator" is "included" and operator type is "match_any"', () => {
+ const payload = getExceptionItemEntryMock();
+ payload.type = 'match_any';
+ payload.operator = Operator.INCLUSION;
+ const result = getExceptionOperatorSelect(payload);
+
+ expect(result).toEqual(isOneOfOperator);
+ });
+
+ test('it returns "isNotOneOfOperator" when "operator" is "excluded" and operator type is "match_any"', () => {
+ const payload = getExceptionItemEntryMock();
+ payload.type = 'match_any';
+ payload.operator = Operator.EXCLUSION;
+ const result = getExceptionOperatorSelect(payload);
+
+ expect(result).toEqual(isNotOneOfOperator);
+ });
+
+ test('it returns "existsOperator" when "operator" is "included" and no operator type is provided', () => {
+ const payload = getExceptionItemEntryMock();
+ payload.type = 'exists';
+ payload.operator = Operator.INCLUSION;
+ const result = getExceptionOperatorSelect(payload);
+
+ expect(result).toEqual(existsOperator);
+ });
+
+ test('it returns "doesNotExistsOperator" when "operator" is "excluded" and no operator type is provided', () => {
+ const payload = getExceptionItemEntryMock();
+ payload.type = 'exists';
+ payload.operator = Operator.EXCLUSION;
+ const result = getExceptionOperatorSelect(payload);
+
+ expect(result).toEqual(doesNotExistOperator);
+ });
+
+ test('it returns "isInList" when "operator" is "included" and operator type is "list"', () => {
+ const payload = getExceptionItemEntryMock();
+ payload.type = 'list';
+ payload.operator = Operator.INCLUSION;
+ const result = getExceptionOperatorSelect(payload);
+
+ expect(result).toEqual(isInListOperator);
+ });
+
+ test('it returns "isNotInList" when "operator" is "excluded" and operator type is "list"', () => {
+ const payload = getExceptionItemEntryMock();
+ payload.type = 'list';
+ payload.operator = Operator.EXCLUSION;
+ const result = getExceptionOperatorSelect(payload);
+
+ expect(result).toEqual(isNotInListOperator);
+ });
+ });
+
+ describe('#determineIfIsNested', () => {
+ test('it returns true if type NestedExceptionEntry', () => {
+ const payload: NestedExceptionEntry = {
+ field: 'actingProcess.file.signer',
+ type: 'nested',
+ entries: [],
+ };
+ const result = determineIfIsNested(payload);
+
+ expect(result).toBeTruthy();
+ });
+
+ test('it returns false if NOT type NestedExceptionEntry', () => {
+ const payload = getExceptionItemEntryMock();
+ const result = determineIfIsNested(payload);
+
+ expect(result).toBeFalsy();
+ });
+ });
+
+ describe('#getFormattedEntries', () => {
+ test('it returns empty array if no entries passed', () => {
+ const result = getFormattedEntries([]);
+
+ expect(result).toEqual([]);
+ });
+
+ test('it formats nested entries as expected', () => {
+ const payload = [
+ {
+ field: 'file.signature',
+ type: 'nested',
+ entries: [
+ {
+ field: 'signer',
+ type: 'match',
+ operator: Operator.INCLUSION,
+ value: 'Evil',
+ },
+ {
+ field: 'trusted',
+ type: 'match',
+ operator: Operator.INCLUSION,
+ value: 'true',
+ },
+ ],
+ },
+ ];
+ const result = getFormattedEntries(payload);
+ const expected: FormattedEntry[] = [
+ {
+ fieldName: 'file.signature',
+ operator: null,
+ value: null,
+ isNested: false,
+ },
+ {
+ fieldName: 'file.signature.signer',
+ isNested: true,
+ operator: 'is',
+ value: 'Evil',
+ },
+ {
+ fieldName: 'file.signature.trusted',
+ isNested: true,
+ operator: 'is',
+ value: 'true',
+ },
+ ];
+ expect(result).toEqual(expected);
+ });
+
+ test('it formats non-nested entries as expected', () => {
+ const payload = [
+ {
+ field: 'actingProcess.file.signer',
+ type: 'match',
+ operator: Operator.INCLUSION,
+ value: 'Elastic, N.V.',
+ },
+ {
+ field: 'actingProcess.file.signer',
+ type: 'match',
+ operator: Operator.EXCLUSION,
+ value: 'Global Signer',
+ },
+ ];
+ const result = getFormattedEntries(payload);
+ const expected: FormattedEntry[] = [
+ {
+ fieldName: 'actingProcess.file.signer',
+ isNested: false,
+ operator: 'is',
+ value: 'Elastic, N.V.',
+ },
+ {
+ fieldName: 'actingProcess.file.signer',
+ isNested: false,
+ operator: 'is not',
+ value: 'Global Signer',
+ },
+ ];
+ expect(result).toEqual(expected);
+ });
+
+ test('it formats a mix of nested and non-nested entries as expected', () => {
+ const payload = getExceptionItemMock();
+ const result = getFormattedEntries(payload.entries);
+ const expected: FormattedEntry[] = [
+ {
+ fieldName: 'actingProcess.file.signer',
+ isNested: false,
+ operator: 'is',
+ value: 'Elastic, N.V.',
+ },
+ {
+ fieldName: 'host.name',
+ isNested: false,
+ operator: 'is not',
+ value: 'Global Signer',
+ },
+ {
+ fieldName: 'file.signature',
+ isNested: false,
+ operator: null,
+ value: null,
+ },
+ {
+ fieldName: 'file.signature.signer',
+ isNested: true,
+ operator: 'is',
+ value: 'Evil',
+ },
+ {
+ fieldName: 'file.signature.trusted',
+ isNested: true,
+ operator: 'is',
+ value: 'true',
+ },
+ ];
+ expect(result).toEqual(expected);
+ });
+ });
+
+ describe('#formatEntry', () => {
+ test('it formats an entry', () => {
+ const payload = getExceptionItemEntryMock();
+ const formattedEntry = formatEntry({ isNested: false, item: payload });
+ const expected: FormattedEntry = {
+ fieldName: 'actingProcess.file.signer',
+ isNested: false,
+ operator: 'is',
+ value: 'Elastic, N.V.',
+ };
+
+ expect(formattedEntry).toEqual(expected);
+ });
+
+ test('it formats a nested entry', () => {
+ const payload = getExceptionItemEntryMock();
+ const formattedEntry = formatEntry({ isNested: true, parent: 'parent', item: payload });
+ const expected: FormattedEntry = {
+ fieldName: 'parent.actingProcess.file.signer',
+ isNested: true,
+ operator: 'is',
+ value: 'Elastic, N.V.',
+ };
+
+ expect(formattedEntry).toEqual(expected);
+ });
+ });
+
+ describe('#getOperatingSystems', () => {
+ test('it returns null if no operating system tag specified', () => {
+ const result = getOperatingSystems(['some tag', 'some other tag']);
+
+ expect(result).toEqual('');
+ });
+
+ test('it returns null if operating system tag malformed', () => {
+ const result = getOperatingSystems(['some tag', 'jibberos:mac,windows', 'some other tag']);
+
+ expect(result).toEqual('');
+ });
+
+ test('it returns formatted operating systems if space included in os tag', () => {
+ const result = getOperatingSystems(['some tag', 'os: mac', 'some other tag']);
+
+ expect(result).toEqual('Mac');
+ });
+
+ test('it returns formatted operating systems if multiple os tags specified', () => {
+ const result = getOperatingSystems(['some tag', 'os: mac', 'some other tag', 'os:windows']);
+
+ expect(result).toEqual('Mac, Windows');
+ });
+ });
+
+ describe('#getTagsInclude', () => {
+ test('it returns a tuple of "false" and "null" if no matches found', () => {
+ const result = getTagsInclude({ tags: ['some', 'tags', 'here'], regex: /(no match)/ });
+
+ expect(result).toEqual([false, null]);
+ });
+
+ test('it returns a tuple of "true" and matching string if matches found', () => {
+ const result = getTagsInclude({ tags: ['some', 'tags', 'here'], regex: /(some)/ });
+
+ expect(result).toEqual([true, 'some']);
+ });
+ });
+
+ describe('#getDescriptionListContent', () => {
+ test('it returns formatted description list with os if one is specified', () => {
+ const payload = getExceptionItemMock();
+ payload.description = '';
+ const result = getDescriptionListContent(payload);
+ const expected: DescriptionListItem[] = [
+ {
+ description: 'Windows',
+ title: 'OS',
+ },
+ {
+ description: 'April 23rd 2020 @ 00:19:13',
+ title: 'Date created',
+ },
+ {
+ description: 'user_name',
+ title: 'Created by',
+ },
+ ];
+
+ expect(result).toEqual(expected);
+ });
+
+ test('it returns formatted description list with a description if one specified', () => {
+ const payload = getExceptionItemMock();
+ payload._tags = [];
+ payload.description = 'Im a description';
+ const result = getDescriptionListContent(payload);
+ const expected: DescriptionListItem[] = [
+ {
+ description: 'April 23rd 2020 @ 00:19:13',
+ title: 'Date created',
+ },
+ {
+ description: 'user_name',
+ title: 'Created by',
+ },
+ {
+ description: 'Im a description',
+ title: 'Comment',
+ },
+ ];
+
+ expect(result).toEqual(expected);
+ });
+
+ test('it returns just user and date created if no other fields specified', () => {
+ const payload = getExceptionItemMock();
+ payload._tags = [];
+ payload.description = '';
+ const result = getDescriptionListContent(payload);
+ const expected: DescriptionListItem[] = [
+ {
+ description: 'April 23rd 2020 @ 00:19:13',
+ title: 'Date created',
+ },
+ {
+ description: 'user_name',
+ title: 'Created by',
+ },
+ ];
+
+ expect(result).toEqual(expected);
+ });
+ });
+
+ describe('#getFormattedComments', () => {
+ test('it returns formatted comment object with username and timestamp', () => {
+ const payload = getExceptionItemMock().comments;
+ const result = getFormattedComments(payload);
+
+ expect(result[0].username).toEqual('user_name');
+ expect(result[0].timestamp).toEqual('on Apr 23rd 2020 @ 00:19:13');
+ });
+
+ test('it returns formatted timeline icon with comment users initial', () => {
+ const payload = getExceptionItemMock().comments;
+ const result = getFormattedComments(payload);
+
+ const wrapper = mount(result[0].timelineIcon as React.ReactElement);
+
+ expect(wrapper.text()).toEqual('U');
+ });
+
+ test('it returns comment text', () => {
+ const payload = getExceptionItemMock().comments;
+ const result = getFormattedComments(payload);
+
+ const wrapper = mount(result[0].children as React.ReactElement);
+
+ expect(wrapper.text()).toEqual('Comment goes here');
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx
new file mode 100644
index 000000000000..bd22de636bf6
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx
@@ -0,0 +1,192 @@
+/*
+ * 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 { EuiText, EuiCommentProps, EuiAvatar } from '@elastic/eui';
+import { capitalize } from 'lodash';
+import moment from 'moment';
+
+import * as i18n from './translations';
+import {
+ FormattedEntry,
+ OperatorType,
+ OperatorOption,
+ ExceptionEntry,
+ NestedExceptionEntry,
+ DescriptionListItem,
+ Comment,
+ ExceptionListItemSchema,
+} from './types';
+import { EXCEPTION_OPERATORS, isOperator } from './operators';
+
+/**
+ * Returns the operator type, may not need this if using io-ts types
+ *
+ * @param entry a single ExceptionItem entry
+ */
+export const getOperatorType = (entry: ExceptionEntry): OperatorType => {
+ switch (entry.type) {
+ case 'nested':
+ case 'match':
+ return OperatorType.PHRASE;
+ case 'match_any':
+ return OperatorType.PHRASES;
+ case 'list':
+ return OperatorType.LIST;
+ default:
+ return OperatorType.EXISTS;
+ }
+};
+
+/**
+ * Determines operator selection (is/is not/is one of, etc.)
+ * Default operator is "is"
+ *
+ * @param entry a single ExceptionItem entry
+ */
+export const getExceptionOperatorSelect = (entry: ExceptionEntry): OperatorOption => {
+ const operatorType = getOperatorType(entry);
+ const foundOperator = EXCEPTION_OPERATORS.find((operatorOption) => {
+ return entry.operator === operatorOption.operator && operatorType === operatorOption.type;
+ });
+
+ return foundOperator ?? isOperator;
+};
+
+export const determineIfIsNested = (
+ tbd: ExceptionEntry | NestedExceptionEntry
+): tbd is NestedExceptionEntry => {
+ if (tbd.type === 'nested') {
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Formats ExceptionItem entries into simple field, operator, value
+ * for use in rendering items in table
+ *
+ * @param entries an ExceptionItem's entries
+ */
+export const getFormattedEntries = (
+ entries: Array
+): FormattedEntry[] => {
+ const formattedEntries = entries.map((entry) => {
+ if (determineIfIsNested(entry)) {
+ const parent = { fieldName: entry.field, operator: null, value: null, isNested: false };
+ return entry.entries.reduce(
+ (acc, nestedEntry) => {
+ const formattedEntry = formatEntry({
+ isNested: true,
+ parent: entry.field,
+ item: nestedEntry,
+ });
+ return [...acc, { ...formattedEntry }];
+ },
+ [parent]
+ );
+ } else {
+ return formatEntry({ isNested: false, item: entry });
+ }
+ });
+
+ return formattedEntries.flat();
+};
+
+/**
+ * Helper method for `getFormattedEntries`
+ */
+export const formatEntry = ({
+ isNested,
+ parent,
+ item,
+}: {
+ isNested: boolean;
+ parent?: string;
+ item: ExceptionEntry;
+}): FormattedEntry => {
+ const operator = getExceptionOperatorSelect(item);
+ const operatorType = getOperatorType(item);
+ const value = operatorType === OperatorType.EXISTS ? null : item.value;
+
+ return {
+ fieldName: isNested ? `${parent}.${item.field}` : item.field,
+ operator: operator.message,
+ value,
+ isNested,
+ };
+};
+
+export const getOperatingSystems = (tags: string[]): string => {
+ const osMatches = tags
+ .filter((tag) => tag.startsWith('os:'))
+ .map((os) => capitalize(os.substring(3).trim()))
+ .join(', ');
+
+ return osMatches;
+};
+
+export const getTagsInclude = ({
+ tags,
+ regex,
+}: {
+ tags: string[];
+ regex: RegExp;
+}): [boolean, string | null] => {
+ const matches: string[] | null = tags.join(';').match(regex);
+ const match = matches != null ? matches[1] : null;
+ return [matches != null, match];
+};
+
+/**
+ * Formats ExceptionItem information for description list component
+ *
+ * @param exceptionItem an ExceptionItem
+ */
+export const getDescriptionListContent = (
+ exceptionItem: ExceptionListItemSchema
+): DescriptionListItem[] => {
+ const details = [
+ {
+ title: i18n.OPERATING_SYSTEM,
+ value: getOperatingSystems(exceptionItem._tags),
+ },
+ {
+ title: i18n.DATE_CREATED,
+ value: moment(exceptionItem.created_at).format('MMMM Do YYYY @ HH:mm:ss'),
+ },
+ {
+ title: i18n.CREATED_BY,
+ value: exceptionItem.created_by,
+ },
+ {
+ title: i18n.COMMENT,
+ value: exceptionItem.description,
+ },
+ ];
+
+ return details.reduce((acc, { value, title }) => {
+ if (value != null && value.trim() !== '') {
+ return [...acc, { title, description: value }];
+ } else {
+ return acc;
+ }
+ }, []);
+};
+
+/**
+ * Formats ExceptionItem.comments into EuiCommentList format
+ *
+ * @param comments ExceptionItem.comments
+ */
+export const getFormattedComments = (comments: Comment[]): EuiCommentProps[] =>
+ comments.map((comment) => ({
+ username: comment.user,
+ timestamp: moment(comment.timestamp).format('on MMM Do YYYY @ HH:mm:ss'),
+ event: i18n.COMMENT_EVENT,
+ timelineIcon: ,
+ children: {comment.comment},
+ }));
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/mocks.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/mocks.ts
new file mode 100644
index 000000000000..15aec3533b32
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/mocks.ts
@@ -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 {
+ Operator,
+ ExceptionListItemSchema,
+ ExceptionEntry,
+ NestedExceptionEntry,
+ FormattedEntry,
+} from './types';
+
+export const getExceptionItemEntryMock = (): ExceptionEntry => ({
+ field: 'actingProcess.file.signer',
+ type: 'match',
+ operator: Operator.INCLUSION,
+ value: 'Elastic, N.V.',
+});
+
+export const getNestedExceptionItemEntryMock = (): NestedExceptionEntry => ({
+ field: 'actingProcess.file.signer',
+ type: 'nested',
+ entries: [{ ...getExceptionItemEntryMock() }],
+});
+
+export const getFormattedEntryMock = (isNested = false): FormattedEntry => ({
+ fieldName: 'host.name',
+ operator: 'is',
+ value: 'some name',
+ isNested,
+});
+
+export const getExceptionItemMock = (): ExceptionListItemSchema => ({
+ id: 'uuid_here',
+ item_id: 'item-id',
+ created_at: '2020-04-23T00:19:13.289Z',
+ created_by: 'user_name',
+ list_id: 'test-exception',
+ tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f',
+ updated_at: '2020-04-23T00:19:13.289Z',
+ updated_by: 'user_name',
+ namespace_type: 'single',
+ name: '',
+ description: 'This is a description',
+ comments: [
+ {
+ user: 'user_name',
+ timestamp: '2020-04-23T00:19:13.289Z',
+ comment: 'Comment goes here',
+ },
+ ],
+ _tags: ['os:windows'],
+ tags: [],
+ type: 'simple',
+ entries: [
+ {
+ field: 'actingProcess.file.signer',
+ type: 'match',
+ operator: Operator.INCLUSION,
+ value: 'Elastic, N.V.',
+ },
+ {
+ field: 'host.name',
+ type: 'match',
+ operator: Operator.EXCLUSION,
+ value: 'Global Signer',
+ },
+ {
+ field: 'file.signature',
+ type: 'nested',
+ entries: [
+ {
+ field: 'signer',
+ type: 'match',
+ operator: Operator.INCLUSION,
+ value: 'Evil',
+ },
+ {
+ field: 'trusted',
+ type: 'match',
+ operator: Operator.INCLUSION,
+ value: 'true',
+ },
+ ],
+ },
+ ],
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/operators.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/operators.ts
new file mode 100644
index 000000000000..19c726893e68
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/operators.ts
@@ -0,0 +1,91 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { OperatorOption, OperatorType, Operator } from './types';
+
+export const isOperator: OperatorOption = {
+ message: i18n.translate('xpack.securitySolution.exceptions.isOperatorLabel', {
+ defaultMessage: 'is',
+ }),
+ value: 'is',
+ type: OperatorType.PHRASE,
+ operator: Operator.INCLUSION,
+};
+
+export const isNotOperator: OperatorOption = {
+ message: i18n.translate('xpack.securitySolution.exceptions.isNotOperatorLabel', {
+ defaultMessage: 'is not',
+ }),
+ value: 'is_not',
+ type: OperatorType.PHRASE,
+ operator: Operator.EXCLUSION,
+};
+
+export const isOneOfOperator: OperatorOption = {
+ message: i18n.translate('xpack.securitySolution.exceptions.isOneOfOperatorLabel', {
+ defaultMessage: 'is one of',
+ }),
+ value: 'is_one_of',
+ type: OperatorType.PHRASES,
+ operator: Operator.INCLUSION,
+};
+
+export const isNotOneOfOperator: OperatorOption = {
+ message: i18n.translate('xpack.securitySolution.exceptions.isNotOneOfOperatorLabel', {
+ defaultMessage: 'is not one of',
+ }),
+ value: 'is_not_one_of',
+ type: OperatorType.PHRASES,
+ operator: Operator.EXCLUSION,
+};
+
+export const existsOperator: OperatorOption = {
+ message: i18n.translate('xpack.securitySolution.exceptions.existsOperatorLabel', {
+ defaultMessage: 'exists',
+ }),
+ value: 'exists',
+ type: OperatorType.EXISTS,
+ operator: Operator.INCLUSION,
+};
+
+export const doesNotExistOperator: OperatorOption = {
+ message: i18n.translate('xpack.securitySolution.exceptions.doesNotExistOperatorLabel', {
+ defaultMessage: 'does not exist',
+ }),
+ value: 'does_not_exist',
+ type: OperatorType.EXISTS,
+ operator: Operator.EXCLUSION,
+};
+
+export const isInListOperator: OperatorOption = {
+ message: i18n.translate('xpack.securitySolution.exceptions.isInListOperatorLabel', {
+ defaultMessage: 'is in list',
+ }),
+ value: 'is_in_list',
+ type: OperatorType.LIST,
+ operator: Operator.INCLUSION,
+};
+
+export const isNotInListOperator: OperatorOption = {
+ message: i18n.translate('xpack.securitySolution.exceptions.isNotInListOperatorLabel', {
+ defaultMessage: 'is not in list',
+ }),
+ value: 'is_not_in_list',
+ type: OperatorType.LIST,
+ operator: Operator.EXCLUSION,
+};
+
+export const EXCEPTION_OPERATORS: OperatorOption[] = [
+ isOperator,
+ isNotOperator,
+ isOneOfOperator,
+ isNotOneOfOperator,
+ existsOperator,
+ doesNotExistOperator,
+ isInListOperator,
+ isNotInListOperator,
+];
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts
new file mode 100644
index 000000000000..704849430daf
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts
@@ -0,0 +1,49 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+export const EDIT = i18n.translate('xpack.securitySolution.exceptions.editButtonLabel', {
+ defaultMessage: 'Edit',
+});
+
+export const REMOVE = i18n.translate('xpack.securitySolution.exceptions.removeButtonLabel', {
+ defaultMessage: 'Remove',
+});
+
+export const COMMENTS_SHOW = (comments: number) =>
+ i18n.translate('xpack.securitySolution.exceptions.showCommentsLabel', {
+ values: { comments },
+ defaultMessage: 'Show ({comments}) {comments, plural, =1 {Comment} other {Comments}}',
+ });
+
+export const COMMENTS_HIDE = (comments: number) =>
+ i18n.translate('xpack.securitySolution.exceptions.hideCommentsLabel', {
+ values: { comments },
+ defaultMessage: 'Hide ({comments}) {comments, plural, =1 {Comment} other {Comments}}',
+ });
+
+export const DATE_CREATED = i18n.translate('xpack.securitySolution.exceptions.dateCreatedLabel', {
+ defaultMessage: 'Date created',
+});
+
+export const CREATED_BY = i18n.translate('xpack.securitySolution.exceptions.createdByLabel', {
+ defaultMessage: 'Created by',
+});
+
+export const COMMENT = i18n.translate('xpack.securitySolution.exceptions.commentLabel', {
+ defaultMessage: 'Comment',
+});
+
+export const COMMENT_EVENT = i18n.translate('xpack.securitySolution.exceptions.commentEventLabel', {
+ defaultMessage: 'added a comment',
+});
+
+export const OPERATING_SYSTEM = i18n.translate(
+ 'xpack.securitySolution.exceptions.operatingSystemLabel',
+ {
+ defaultMessage: 'OS',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts
new file mode 100644
index 000000000000..e8393610e459
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts
@@ -0,0 +1,78 @@
+/*
+ * 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 { ReactNode } from 'react';
+
+export interface OperatorOption {
+ message: string;
+ value: string;
+ operator: Operator;
+ type: OperatorType;
+}
+
+export enum Operator {
+ INCLUSION = 'included',
+ EXCLUSION = 'excluded',
+}
+
+export enum OperatorType {
+ NESTED = 'nested',
+ PHRASE = 'match',
+ PHRASES = 'match_any',
+ EXISTS = 'exists',
+ LIST = 'list',
+}
+
+export interface FormattedEntry {
+ fieldName: string;
+ operator: string | null;
+ value: string | null;
+ isNested: boolean;
+}
+
+export interface NestedExceptionEntry {
+ field: string;
+ type: string;
+ entries: ExceptionEntry[];
+}
+
+export interface ExceptionEntry {
+ field: string;
+ type: string;
+ operator: Operator;
+ value: string;
+}
+
+export interface DescriptionListItem {
+ title: NonNullable;
+ description: NonNullable;
+}
+
+export interface Comment {
+ user: string;
+ timestamp: string;
+ comment: string;
+}
+
+// TODO: Delete once types are updated
+export interface ExceptionListItemSchema {
+ _tags: string[];
+ comments: Comment[];
+ created_at: string;
+ created_by: string;
+ description?: string;
+ entries: Array;
+ id: string;
+ item_id: string;
+ list_id: string;
+ meta?: unknown;
+ name: string;
+ namespace_type: 'single' | 'agnostic';
+ tags: string[];
+ tie_breaker_id: string;
+ type: string;
+ updated_at: string;
+ updated_by: string;
+}
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_details.test.tsx
new file mode 100644
index 000000000000..536d005c57b6
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_details.test.tsx
@@ -0,0 +1,226 @@
+/*
+ * 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 { ThemeProvider } from 'styled-components';
+import { mount } from 'enzyme';
+import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
+import moment from 'moment-timezone';
+
+import { ExceptionDetails } from './exception_details';
+import { getExceptionItemMock } from '../mocks';
+
+describe('ExceptionDetails', () => {
+ beforeEach(() => {
+ moment.tz.setDefault('UTC');
+ });
+
+ afterEach(() => {
+ moment.tz.setDefault('Browser');
+ });
+
+ test('it renders no comments button if no comments exist', () => {
+ const exceptionItem = getExceptionItemMock();
+ exceptionItem.comments = [];
+
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]')).toHaveLength(0);
+ });
+
+ test('it renders comments button if comments exist', () => {
+ const exceptionItem = getExceptionItemMock();
+
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ expect(
+ wrapper.find('.euiButtonEmpty[data-test-subj="exceptionsViewerItemCommentsBtn"]')
+ ).toHaveLength(1);
+ });
+
+ test('it renders correct number of comments', () => {
+ const exceptionItem = getExceptionItemMock();
+
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]').at(0).text()).toEqual(
+ 'Show (1) Comment'
+ );
+ });
+
+ test('it renders comments plural if more than one', () => {
+ const exceptionItem = getExceptionItemMock();
+ exceptionItem.comments = [
+ {
+ user: 'user_1',
+ timestamp: '2020-04-23T00:19:13.289Z',
+ comment: 'Comment goes here',
+ },
+ {
+ user: 'user_2',
+ timestamp: '2020-04-23T00:19:13.289Z',
+ comment: 'Comment goes here',
+ },
+ ];
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]').at(0).text()).toEqual(
+ 'Show (2) Comments'
+ );
+ });
+
+ test('it renders comments show text if "showComments" is false', () => {
+ const exceptionItem = getExceptionItemMock();
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]').at(0).text()).toEqual(
+ 'Show (1) Comment'
+ );
+ });
+
+ test('it renders comments hide text if "showComments" is true', () => {
+ const exceptionItem = getExceptionItemMock();
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]').at(0).text()).toEqual(
+ 'Hide (1) Comment'
+ );
+ });
+
+ test('it invokes "onCommentsClick" when comments button clicked', () => {
+ const mockOnCommentsClick = jest.fn();
+ const exceptionItem = getExceptionItemMock();
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+ const commentsBtn = wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]').at(0);
+ commentsBtn.simulate('click');
+
+ expect(mockOnCommentsClick).toHaveBeenCalledTimes(1);
+ });
+
+ test('it renders the operating system if one is specified in the exception item', () => {
+ const exceptionItem = getExceptionItemMock();
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ expect(wrapper.find('EuiDescriptionListTitle').at(0).text()).toEqual('OS');
+ expect(wrapper.find('EuiDescriptionListDescription').at(0).text()).toEqual('Windows');
+ });
+
+ test('it renders the exception item creator', () => {
+ const exceptionItem = getExceptionItemMock();
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ expect(wrapper.find('EuiDescriptionListTitle').at(1).text()).toEqual('Date created');
+ expect(wrapper.find('EuiDescriptionListDescription').at(1).text()).toEqual(
+ 'April 23rd 2020 @ 00:19:13'
+ );
+ });
+
+ test('it renders the exception item creation timestamp', () => {
+ const exceptionItem = getExceptionItemMock();
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ expect(wrapper.find('EuiDescriptionListTitle').at(2).text()).toEqual('Created by');
+ expect(wrapper.find('EuiDescriptionListDescription').at(2).text()).toEqual('user_name');
+ });
+
+ test('it renders the description if one is included on the exception item', () => {
+ const exceptionItem = getExceptionItemMock();
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ expect(wrapper.find('EuiDescriptionListTitle').at(3).text()).toEqual('Comment');
+ expect(wrapper.find('EuiDescriptionListDescription').at(3).text()).toEqual(
+ 'This is a description'
+ );
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_details.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_details.tsx
new file mode 100644
index 000000000000..8745e80a2154
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_details.tsx
@@ -0,0 +1,85 @@
+/*
+ * 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 { EuiFlexItem, EuiFlexGroup, EuiDescriptionList, EuiButtonEmpty } from '@elastic/eui';
+import React, { useMemo } from 'react';
+import styled, { css } from 'styled-components';
+import { transparentize } from 'polished';
+
+import { ExceptionListItemSchema } from '../types';
+import { getDescriptionListContent } from '../helpers';
+import * as i18n from '../translations';
+
+const StyledExceptionDetails = styled(EuiFlexItem)`
+ ${({ theme }) => css`
+ background-color: ${transparentize(0.95, theme.eui.euiColorPrimary)};
+ padding: ${theme.eui.euiSize};
+
+ .euiDescriptionList__title.listTitle--width {
+ width: 40%;
+ }
+
+ .euiDescriptionList__description.listDescription--width {
+ width: 60%;
+ }
+ `}
+`;
+
+const ExceptionDetailsComponent = ({
+ showComments,
+ onCommentsClick,
+ exceptionItem,
+}: {
+ showComments: boolean;
+ exceptionItem: ExceptionListItemSchema;
+ onCommentsClick: () => void;
+}): JSX.Element => {
+ const descriptionList = useMemo(() => getDescriptionListContent(exceptionItem), [exceptionItem]);
+
+ const commentsSection = useMemo((): JSX.Element => {
+ const { comments } = exceptionItem;
+ if (comments.length > 0) {
+ return (
+
+ {!showComments
+ ? i18n.COMMENTS_SHOW(comments.length)
+ : i18n.COMMENTS_HIDE(comments.length)}
+
+ );
+ } else {
+ return <>>;
+ }
+ }, [showComments, onCommentsClick, exceptionItem]);
+
+ return (
+
+
+
+
+
+ {commentsSection}
+
+
+ );
+};
+
+ExceptionDetailsComponent.displayName = 'ExceptionDetailsComponent';
+
+export const ExceptionDetails = React.memo(ExceptionDetailsComponent);
+
+ExceptionDetails.displayName = 'ExceptionDetails';
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_entries.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_entries.test.tsx
new file mode 100644
index 000000000000..e0c62f51d032
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_entries.test.tsx
@@ -0,0 +1,150 @@
+/*
+ * 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 { ThemeProvider } from 'styled-components';
+import { mount } from 'enzyme';
+import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
+
+import { ExceptionEntries } from './exception_entries';
+import { getFormattedEntryMock } from '../mocks';
+import { getEmptyValue } from '../../empty_value';
+
+describe('ExceptionEntries', () => {
+ test('it does NOT render the and badge if only one exception item entry exists', () => {
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionsViewerAndBadge"]')).toHaveLength(0);
+ });
+
+ test('it renders the and badge if more than one exception item exists', () => {
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionsViewerAndBadge"]')).toHaveLength(1);
+ });
+
+ test('it invokes "handlEdit" when edit button clicked', () => {
+ const mockHandleEdit = jest.fn();
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+ const editBtn = wrapper.find('[data-test-subj="exceptionsViewerEditBtn"] button').at(0);
+ editBtn.simulate('click');
+
+ expect(mockHandleEdit).toHaveBeenCalledTimes(1);
+ });
+
+ test('it invokes "handleDelete" when delete button clicked', () => {
+ const mockHandleDelete = jest.fn();
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+ const deleteBtn = wrapper.find('[data-test-subj="exceptionsViewerDeleteBtn"] button').at(0);
+ deleteBtn.simulate('click');
+
+ expect(mockHandleDelete).toHaveBeenCalledTimes(1);
+ });
+
+ test('it renders nested entry', () => {
+ const parentEntry = getFormattedEntryMock();
+ parentEntry.operator = null;
+ parentEntry.value = null;
+
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ const parentField = wrapper
+ .find('[data-test-subj="exceptionFieldNameCell"] .euiTableCellContent')
+ .at(0);
+ const parentOperator = wrapper
+ .find('[data-test-subj="exceptionFieldOperatorCell"] .euiTableCellContent')
+ .at(0);
+ const parentValue = wrapper
+ .find('[data-test-subj="exceptionFieldValueCell"] .euiTableCellContent')
+ .at(0);
+
+ const nestedField = wrapper
+ .find('[data-test-subj="exceptionFieldNameCell"] .euiTableCellContent')
+ .at(1);
+ const nestedOperator = wrapper
+ .find('[data-test-subj="exceptionFieldOperatorCell"] .euiTableCellContent')
+ .at(1);
+ const nestedValue = wrapper
+ .find('[data-test-subj="exceptionFieldValueCell"] .euiTableCellContent')
+ .at(1);
+
+ expect(parentField.text()).toEqual('host.name');
+ expect(parentOperator.text()).toEqual(getEmptyValue());
+ expect(parentValue.text()).toEqual(getEmptyValue());
+
+ expect(nestedField.exists('.euiToolTipAnchor')).toBeTruthy();
+ expect(nestedField.text()).toEqual('host.name');
+ expect(nestedOperator.text()).toEqual('is');
+ expect(nestedValue.text()).toEqual('some name');
+ });
+
+ test('it renders non-nested entries', () => {
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ const field = wrapper
+ .find('[data-test-subj="exceptionFieldNameCell"] .euiTableCellContent')
+ .at(0);
+ const operator = wrapper
+ .find('[data-test-subj="exceptionFieldOperatorCell"] .euiTableCellContent')
+ .at(0);
+ const value = wrapper
+ .find('[data-test-subj="exceptionFieldValueCell"] .euiTableCellContent')
+ .at(0);
+
+ expect(field.exists('.euiToolTipAnchor')).toBeFalsy();
+ expect(field.text()).toEqual('host.name');
+ expect(operator.text()).toEqual('is');
+ expect(value.text()).toEqual('some name');
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_entries.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_entries.tsx
new file mode 100644
index 000000000000..d0236adc27c6
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_entries.tsx
@@ -0,0 +1,169 @@
+/*
+ * 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 {
+ EuiBasicTable,
+ EuiIconTip,
+ EuiFlexItem,
+ EuiFlexGroup,
+ EuiButton,
+ EuiTableFieldDataColumnType,
+} from '@elastic/eui';
+import React, { useMemo } from 'react';
+import styled, { css } from 'styled-components';
+import { transparentize } from 'polished';
+
+import { AndOrBadge } from '../../and_or_badge';
+import { getEmptyValue } from '../../empty_value';
+import * as i18n from '../translations';
+import { FormattedEntry } from '../types';
+
+const EntriesDetails = styled(EuiFlexItem)`
+ padding: ${({ theme }) => theme.eui.euiSize};
+`;
+
+const StyledEditButton = styled(EuiButton)`
+ ${({ theme }) => css`
+ background-color: ${transparentize(0.9, theme.eui.euiColorPrimary)};
+ border: none;
+ font-weight: ${theme.eui.euiFontWeightSemiBold};
+ `}
+`;
+
+const StyledRemoveButton = styled(EuiButton)`
+ ${({ theme }) => css`
+ background-color: ${transparentize(0.9, theme.eui.euiColorDanger)};
+ border: none;
+ font-weight: ${theme.eui.euiFontWeightSemiBold};
+ `}
+`;
+
+const AndOrBadgeContainer = styled(EuiFlexItem)`
+ padding-top: ${({ theme }) => theme.eui.euiSizeXL};
+`;
+
+interface ExceptionEntriesComponentProps {
+ entries: FormattedEntry[];
+ handleDelete: () => void;
+ handleEdit: () => void;
+}
+
+const ExceptionEntriesComponent = ({
+ entries,
+ handleDelete,
+ handleEdit,
+}: ExceptionEntriesComponentProps): JSX.Element => {
+ const columns = useMemo(
+ (): Array> => [
+ {
+ field: 'fieldName',
+ name: 'Field',
+ sortable: false,
+ truncateText: true,
+ 'data-test-subj': 'exceptionFieldNameCell',
+ width: '30%',
+ render: (value: string | null, data: FormattedEntry) => {
+ if (value != null && data.isNested) {
+ return (
+ <>
+
+ {value}
+ >
+ );
+ } else {
+ return value ?? getEmptyValue();
+ }
+ },
+ },
+ {
+ field: 'operator',
+ name: 'Operator',
+ sortable: false,
+ truncateText: true,
+ 'data-test-subj': 'exceptionFieldOperatorCell',
+ width: '20%',
+ render: (value: string | null) => value ?? getEmptyValue(),
+ },
+ {
+ field: 'value',
+ name: 'Value',
+ sortable: false,
+ truncateText: true,
+ 'data-test-subj': 'exceptionFieldValueCell',
+ width: '60%',
+ render: (values: string | string[] | null) => {
+ if (Array.isArray(values)) {
+ return (
+
+ {values.map((value) => {
+ return {value};
+ })}
+
+ );
+ } else {
+ return values ?? getEmptyValue();
+ }
+ },
+ },
+ ],
+ [entries]
+ );
+
+ return (
+
+
+
+
+ {entries.length > 1 && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ {i18n.EDIT}
+
+
+
+
+ {i18n.REMOVE}
+
+
+
+
+
+
+ );
+};
+
+ExceptionEntriesComponent.displayName = 'ExceptionEntriesComponent';
+
+export const ExceptionEntries = React.memo(ExceptionEntriesComponent);
+
+ExceptionEntries.displayName = 'ExceptionEntries';
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx
new file mode 100644
index 000000000000..7d3b7195def8
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx
@@ -0,0 +1,116 @@
+/*
+ * 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 { ThemeProvider } from 'styled-components';
+import { mount } from 'enzyme';
+import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
+
+import { ExceptionItem } from './';
+import { getExceptionItemMock } from '../mocks';
+
+describe('ExceptionItem', () => {
+ it('it renders ExceptionDetails and ExceptionEntries', () => {
+ const exceptionItem = getExceptionItemMock();
+
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ expect(wrapper.find('ExceptionDetails')).toHaveLength(1);
+ expect(wrapper.find('ExceptionEntries')).toHaveLength(1);
+ });
+
+ it('it invokes "handleEdit" when edit button clicked', () => {
+ const mockHandleEdit = jest.fn();
+ const exceptionItem = getExceptionItemMock();
+
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ const editBtn = wrapper.find('[data-test-subj="exceptionsViewerEditBtn"] button').at(0);
+ editBtn.simulate('click');
+
+ expect(mockHandleEdit).toHaveBeenCalledTimes(1);
+ });
+
+ it('it invokes "handleDelete" when delete button clicked', () => {
+ const mockHandleDelete = jest.fn();
+ const exceptionItem = getExceptionItemMock();
+
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ const editBtn = wrapper.find('[data-test-subj="exceptionsViewerDeleteBtn"] button').at(0);
+ editBtn.simulate('click');
+
+ expect(mockHandleDelete).toHaveBeenCalledTimes(1);
+ });
+
+ it('it renders comment accordion closed to begin with', () => {
+ const mockHandleDelete = jest.fn();
+ const exceptionItem = getExceptionItemMock();
+
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ expect(wrapper.find('.euiAccordion-isOpen')).toHaveLength(0);
+ });
+
+ it('it renders comment accordion open when showComments is true', () => {
+ const mockHandleDelete = jest.fn();
+ const exceptionItem = getExceptionItemMock();
+
+ const wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+
+ const commentsBtn = wrapper
+ .find('.euiButtonEmpty[data-test-subj="exceptionsViewerItemCommentsBtn"]')
+ .at(0);
+ commentsBtn.simulate('click');
+
+ expect(wrapper.find('.euiAccordion-isOpen')).toHaveLength(1);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx
new file mode 100644
index 000000000000..f4cdce62f56b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx
@@ -0,0 +1,99 @@
+/*
+ * 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 {
+ EuiPanel,
+ EuiFlexGroup,
+ EuiCommentProps,
+ EuiCommentList,
+ EuiAccordion,
+ EuiFlexItem,
+} from '@elastic/eui';
+import React, { useEffect, useState, useMemo, useCallback } from 'react';
+import styled from 'styled-components';
+
+import { ExceptionDetails } from './exception_details';
+import { ExceptionEntries } from './exception_entries';
+import { getFormattedEntries, getFormattedComments } from '../helpers';
+import { FormattedEntry, ExceptionListItemSchema } from '../types';
+
+const MyFlexItem = styled(EuiFlexItem)`
+ &.comments--show {
+ padding: ${({ theme }) => theme.eui.euiSize};
+ border-top: ${({ theme }) => `${theme.eui.euiBorderThin}`}
+
+`;
+
+interface ExceptionItemProps {
+ exceptionItem: ExceptionListItemSchema;
+ commentsAccordionId: string;
+ handleDelete: ({ id }: { id: string }) => void;
+ handleEdit: (item: ExceptionListItemSchema) => void;
+}
+
+const ExceptionItemComponent = ({
+ exceptionItem,
+ commentsAccordionId,
+ handleDelete,
+ handleEdit,
+}: ExceptionItemProps): JSX.Element => {
+ const [entryItems, setEntryItems] = useState([]);
+ const [showComments, setShowComments] = useState(false);
+
+ useEffect((): void => {
+ const formattedEntries = getFormattedEntries(exceptionItem.entries);
+ setEntryItems(formattedEntries);
+ }, [exceptionItem.entries]);
+
+ const onDelete = useCallback((): void => {
+ handleDelete({ id: exceptionItem.id });
+ }, [handleDelete, exceptionItem]);
+
+ const onEdit = useCallback((): void => {
+ handleEdit(exceptionItem);
+ }, [handleEdit, exceptionItem]);
+
+ const onCommentsClick = useCallback((): void => {
+ setShowComments(!showComments);
+ }, [setShowComments, showComments]);
+
+ const formattedComments = useMemo((): EuiCommentProps[] => {
+ return getFormattedComments(exceptionItem.comments);
+ }, [exceptionItem]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+ExceptionItemComponent.displayName = 'ExceptionItemComponent';
+
+export const ExceptionItem = React.memo(ExceptionItemComponent);
+
+ExceptionItem.displayName = 'ExceptionItem';
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 ac4a3533853f..936bd26adff7 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 06a69d0612ee..c9085b595381 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 918ec70bd740..d8f8742cb436 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 3c0189625ee2..d930136b3c0c 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 30dffa8dbf6b..4af39ade70d2 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 1ff5cb8e734e..c71a9ada75ee 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 58e4e2f363e9..453191ebafce 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 ba85fbef860d..6aa9c6c05936 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 b9942979beb1..2b92451e3011 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 bd1c5f96f8cd..000000000000
--- 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 3ab0cb1f748d..a01e249561e5 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 c6570da5cb5a..5b140a53a363 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 000000000000..ff7f522b9bc5
--- /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 9b38d7ce5a23..62a2d9e3205c 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 17feacb0a767..b8eaa39c7775 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 0959a3438aad..a6cd2ca3afac 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 dd9ab19a702e..85667c9f9fc3 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 a2c410b5dbd6..05af1ee062de 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 c0d5e6931db2..23682544ec42 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 05b265b49ea5..5e7cbc0ef58d 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 421903cb6e1a..4881342c0657 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 a0d4e6280912..b05cdfb3be84 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 c6ecffe0fd51..8172165a77e7 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 78fd679f818b..ddba6d7344ce 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 efad4e3a468d..645a4896770e 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 5e0e3e7e163e..7d84bb52238a 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 1fafb969e6e6..125723e9bcea 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