From 446c5237d59b7e324b39b87412880205f3b43db9 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 25 Aug 2020 13:47:04 +0200 Subject: [PATCH 001/216] [Visualize] fix performance degradation after lodash@4 upgrade --- src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js b/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js index 6490dfe252b292..dda9d85ec43c5b 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js @@ -41,7 +41,7 @@ export class VisConfig { const visType = visTypes[visConfigArgs.type]; const typeDefaults = visType(visConfigArgs, this.data); - this._values = _.defaultsDeep({}, typeDefaults, DEFAULT_VIS_CONFIG); + this._values = _.defaultsDeep({ ...typeDefaults }, DEFAULT_VIS_CONFIG); this._values.el = el; } From 7fa23a4ec1f917164fef3fc84f64a5d2f23aa287 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Tue, 25 Aug 2020 08:20:17 -0500 Subject: [PATCH 002/216] IndexPattern class - no longer use `getConfig` or `uiSettingsValues` (#75717) * remove getConfig and uiSettingsValues from IndexPattern class --- ...-data-public.indexpattern._constructor_.md | 4 +- ...plugin-plugins-data-public.indexpattern.md | 2 +- .../data/common/field_formats/errors.ts | 27 ++++++++ .../field_formats/field_formats_registry.ts | 3 +- .../data/common/field_formats/index.ts | 2 + .../index_patterns/_fields_fetcher.ts | 2 +- .../index_patterns/index_pattern.test.ts | 18 ++--- .../index_patterns/index_pattern.ts | 67 +++++++------------ .../index_patterns/index_patterns.ts | 10 ++- .../data/common/index_patterns/types.ts | 2 +- src/plugins/data/public/plugin.ts | 2 +- src/plugins/data/public/public.api.md | 2 +- 12 files changed, 75 insertions(+), 66 deletions(-) create mode 100644 src/plugins/data/common/field_formats/errors.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md index 0268846772f2c4..2e078e3404fe60 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `IndexPattern` class Signature: ```typescript -constructor(id: string | undefined, { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, uiSettingsValues, }: IndexPatternDeps); +constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, shortDotsEnable, metaFields, }: IndexPatternDeps); ``` ## Parameters @@ -17,5 +17,5 @@ constructor(id: string | undefined, { getConfig, savedObjectsClient, apiClient, | Parameter | Type | Description | | --- | --- | --- | | id | string | undefined | | -| { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, uiSettingsValues, } | IndexPatternDeps | | +| { savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, shortDotsEnable, metaFields, } | IndexPatternDeps | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index d340aaeeef25e0..649f8ef077e3f6 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -14,7 +14,7 @@ export declare class IndexPattern implements IIndexPattern | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(id, { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, uiSettingsValues, })](./kibana-plugin-plugins-data-public.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | +| [(constructor)(id, { savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, shortDotsEnable, metaFields, })](./kibana-plugin-plugins-data-public.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | ## Properties diff --git a/src/plugins/data/common/field_formats/errors.ts b/src/plugins/data/common/field_formats/errors.ts new file mode 100644 index 00000000000000..d72eef080923dd --- /dev/null +++ b/src/plugins/data/common/field_formats/errors.ts @@ -0,0 +1,27 @@ +/* + * 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 class FieldFormatNotFoundError extends Error { + public readonly formatId: string; + constructor(message: string, formatId: string) { + super(message); + this.name = 'FieldFormatNotFoundError'; + this.formatId = formatId; + } +} 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 32f9f37b9ba53c..4b46adf399363c 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.ts @@ -34,6 +34,7 @@ import { FieldFormat } from './field_format'; import { SerializedFieldFormat } from '../../../expressions/common/types'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../kbn_field_types/types'; import { UI_SETTINGS } from '../constants'; +import { FieldFormatNotFoundError } from '../field_formats'; export class FieldFormatsRegistry { protected fieldFormats: Map = new Map(); @@ -161,7 +162,7 @@ export class FieldFormatsRegistry { const ConcreteFieldFormat = this.getType(formatId); if (!ConcreteFieldFormat) { - throw new Error(`Field Format '${formatId}' not found!`); + throw new FieldFormatNotFoundError(`Field Format '${formatId}' not found!`, formatId); } return new ConcreteFieldFormat(params, this.getConfig); diff --git a/src/plugins/data/common/field_formats/index.ts b/src/plugins/data/common/field_formats/index.ts index d622af2f663a14..c1b1619abd2479 100644 --- a/src/plugins/data/common/field_formats/index.ts +++ b/src/plugins/data/common/field_formats/index.ts @@ -55,3 +55,5 @@ export { IFieldFormat, FieldFormatsStartCommon, } from './types'; + +export * from './errors'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts b/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts index baeb1587d57a9e..4eba0576ff2352 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts @@ -24,7 +24,7 @@ import { GetFieldsOptions, IIndexPatternsApiClient } from '../types'; export const createFieldsFetcher = ( indexPattern: IndexPattern, apiClient: IIndexPatternsApiClient, - metaFields: string + metaFields: string[] = [] ) => { const fieldFetcher = { fetch: (options: GetFieldsOptions) => { diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index e4f297b29c3728..09b79cae4aac2a 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -32,7 +32,7 @@ import { fieldFormatsMock } from '../../field_formats/mocks'; class MockFieldFormatter {} -fieldFormatsMock.getType = jest.fn().mockImplementation(() => MockFieldFormatter); +fieldFormatsMock.getInstance = jest.fn().mockImplementation(() => new MockFieldFormatter()) as any; jest.mock('../../field_mapping', () => { const originalModule = jest.requireActual('../../field_mapping'); @@ -89,10 +89,6 @@ const patternCache = { clearAll: jest.fn(), }; -const config = { - get: jest.fn(), -}; - const apiClient = { _getUrl: jest.fn(), getFieldsForTimePattern: jest.fn(), @@ -102,14 +98,14 @@ const apiClient = { // helper function to create index patterns function create(id: string, payload?: any): Promise { const indexPattern = new IndexPattern(id, { - getConfig: (cfg: any) => config.get(cfg), savedObjectsClient: savedObjectsClient as any, apiClient, patternCache, fieldFormats: fieldFormatsMock, onNotification: () => {}, onError: () => {}, - uiSettingsValues: { shortDotsEnable: false, metaFields: [] }, + shortDotsEnable: false, + metaFields: [], }); setDocsourcePayload(id, payload); @@ -391,14 +387,14 @@ describe('IndexPattern', () => { }); // Create a normal index pattern const pattern = new IndexPattern('foo', { - getConfig: (cfg: any) => config.get(cfg), savedObjectsClient: savedObjectsClient as any, apiClient, patternCache, fieldFormats: fieldFormatsMock, onNotification: () => {}, onError: () => {}, - uiSettingsValues: { shortDotsEnable: false, metaFields: [] }, + shortDotsEnable: false, + metaFields: [], }); await pattern.init(); @@ -406,14 +402,14 @@ describe('IndexPattern', () => { // Create the same one - we're going to handle concurrency const samePattern = new IndexPattern('foo', { - getConfig: (cfg: any) => config.get(cfg), savedObjectsClient: savedObjectsClient as any, apiClient, patternCache, fieldFormats: fieldFormatsMock, onNotification: () => {}, onError: () => {}, - uiSettingsValues: { shortDotsEnable: false, metaFields: [] }, + shortDotsEnable: false, + metaFields: [], }); await samePattern.init(); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index 4e484dce7826f0..e81ef1d6b2482e 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -22,20 +22,19 @@ import { i18n } from '@kbn/i18n'; import { SavedObjectsClientCommon } from '../..'; import { DuplicateField, SavedObjectNotFound } from '../../../../kibana_utils/common'; -import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern } from '../../../common'; +import { + ES_FIELD_TYPES, + KBN_FIELD_TYPES, + IIndexPattern, + FieldFormatNotFoundError, +} from '../../../common'; import { findByTitle } from '../utils'; import { IndexPatternMissingIndices } from '../lib'; import { IndexPatternField, IIndexPatternFieldList, FieldList } from '../fields'; import { createFieldsFetcher } from './_fields_fetcher'; import { formatHitProvider } from './format_hit'; import { flattenHitWrapper } from './flatten_hit'; -import { - OnNotification, - OnError, - UiSettingsCommon, - IIndexPatternsApiClient, - IndexPatternAttributes, -} from '../types'; +import { OnNotification, OnError, IIndexPatternsApiClient, IndexPatternAttributes } from '../types'; import { FieldFormatsStartCommon, FieldFormat } from '../../field_formats'; import { PatternCache } from './_pattern_cache'; import { expandShorthand, FieldMappingSpec, MappingObject } from '../../field_mapping'; @@ -44,21 +43,16 @@ import { SerializedFieldFormat } from '../../../../expressions/common'; const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; const savedObjectType = 'index-pattern'; -interface IUiSettingsValues { - [key: string]: any; - shortDotsEnable: any; - metaFields: any; -} interface IndexPatternDeps { - getConfig: UiSettingsCommon['get']; savedObjectsClient: SavedObjectsClientCommon; apiClient: IIndexPatternsApiClient; patternCache: PatternCache; fieldFormats: FieldFormatsStartCommon; onNotification: OnNotification; onError: OnError; - uiSettingsValues: IUiSettingsValues; + shortDotsEnable: boolean; + metaFields: string[]; } export class IndexPattern implements IIndexPattern { @@ -78,7 +72,6 @@ export class IndexPattern implements IIndexPattern { private version: string | undefined; private savedObjectsClient: SavedObjectsClientCommon; private patternCache: PatternCache; - private getConfig: UiSettingsCommon['get']; private sourceFilters?: SourceFilter[]; private originalBody: { [key: string]: any } = {}; public fieldsFetcher: any; // probably want to factor out any direct usage and change to private @@ -87,7 +80,6 @@ export class IndexPattern implements IIndexPattern { private onNotification: OnNotification; private onError: OnError; private apiClient: IIndexPatternsApiClient; - private uiSettingsValues: IUiSettingsValues; private mapping: MappingObject = expandShorthand({ title: ES_FIELD_TYPES.TEXT, @@ -114,35 +106,31 @@ export class IndexPattern implements IIndexPattern { constructor( id: string | undefined, { - getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, - uiSettingsValues, + shortDotsEnable = false, + metaFields = [], }: IndexPatternDeps ) { this.id = id; this.savedObjectsClient = savedObjectsClient; this.patternCache = patternCache; - // instead of storing config we rather store the getter only as np uiSettingsClient has circular references - // which cause problems when being consumed from angular - this.getConfig = getConfig; this.fieldFormats = fieldFormats; this.onNotification = onNotification; this.onError = onError; - this.uiSettingsValues = uiSettingsValues; - this.shortDotsEnable = uiSettingsValues.shortDotsEnable; - this.metaFields = uiSettingsValues.metaFields; + this.shortDotsEnable = shortDotsEnable; + this.metaFields = metaFields; this.fields = new FieldList(this, [], this.shortDotsEnable, this.onUnknownType); this.apiClient = apiClient; - this.fieldsFetcher = createFieldsFetcher(this, apiClient, uiSettingsValues.metaFields); - this.flattenHit = flattenHitWrapper(this, uiSettingsValues.metaFields); + this.fieldsFetcher = createFieldsFetcher(this, apiClient, metaFields); + this.flattenHit = flattenHitWrapper(this, metaFields); this.formatHit = formatHitProvider( this, fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.STRING) @@ -157,15 +145,15 @@ export class IndexPattern implements IIndexPattern { } private deserializeFieldFormatMap(mapping: any) { - const FieldFormatter = this.fieldFormats.getType(mapping.id); - - return ( - FieldFormatter && - new FieldFormatter( - mapping.params, - (key: string) => this.uiSettingsValues[key]?.userValue || this.uiSettingsValues[key]?.value - ) - ); + try { + return this.fieldFormats.getInstance(mapping.id, mapping.params); + } catch (err) { + if (err instanceof FieldFormatNotFoundError) { + return undefined; + } else { + throw err; + } + } } private isFieldRefreshRequired(specs?: FieldSpec[]): boolean { @@ -513,17 +501,14 @@ export class IndexPattern implements IIndexPattern { saveAttempts++ < MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS ) { const samePattern = new IndexPattern(this.id, { - getConfig: this.getConfig, savedObjectsClient: this.savedObjectsClient, apiClient: this.apiClient, patternCache: this.patternCache, fieldFormats: this.fieldFormats, onNotification: this.onNotification, onError: this.onError, - uiSettingsValues: { - shortDotsEnable: this.shortDotsEnable, - metaFields: this.metaFields, - }, + shortDotsEnable: this.shortDotsEnable, + metaFields: this.metaFields, }); return samePattern.init().then(() => { diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 8874ce5f04b7c2..0ad9ae8f2014f8 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -185,17 +185,16 @@ export class IndexPatternsService { async specToIndexPattern(spec: IndexPatternSpec) { const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE); const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); - const uiSettingsValues = await this.config.getAll(); const indexPattern = new IndexPattern(spec.id, { - getConfig: (cfg: any) => this.config.get(cfg), savedObjectsClient: this.savedObjectsClient, apiClient: this.apiClient, patternCache: indexPatternCache, fieldFormats: this.fieldFormats, onNotification: this.onNotification, onError: this.onError, - uiSettingsValues: { ...uiSettingsValues, shortDotsEnable, metaFields }, + shortDotsEnable, + metaFields, }); indexPattern.initFromSpec(spec); @@ -205,17 +204,16 @@ export class IndexPatternsService { async make(id?: string): Promise { const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE); const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); - const uiSettingsValues = await this.config.getAll(); const indexPattern = new IndexPattern(id, { - getConfig: (cfg: any) => this.config.get(cfg), savedObjectsClient: this.savedObjectsClient, apiClient: this.apiClient, patternCache: indexPatternCache, fieldFormats: this.fieldFormats, onNotification: this.onNotification, onError: this.onError, - uiSettingsValues: { ...uiSettingsValues, shortDotsEnable, metaFields }, + shortDotsEnable, + metaFields, }); return indexPattern.init(); diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index a771113acd2310..7a230c20f6cd02 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -96,7 +96,7 @@ export interface GetFieldsOptions { type?: string; params?: any; lookBack?: boolean; - metaFields?: string; + metaFields?: string[]; } export interface IIndexPatternsApiClient { diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index ee0b0714febc0b..3bc19a578a4170 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -177,7 +177,7 @@ export class DataPublicPlugin onNotification: (toastInputFields) => { notifications.toasts.add(toastInputFields); }, - onError: notifications.toasts.addError, + onError: notifications.toasts.addError.bind(notifications.toasts), onRedirectNoIndexPattern: onRedirectNoIndexPattern( application.capabilities, application.navigateToApp, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index f8a108a5a4c58f..261f16229460ac 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -944,7 +944,7 @@ export type IMetricAggType = MetricAggType; // @public (undocumented) export class IndexPattern implements IIndexPattern { // Warning: (ae-forgotten-export) The symbol "IndexPatternDeps" needs to be exported by the entry point index.d.ts - constructor(id: string | undefined, { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, uiSettingsValues, }: IndexPatternDeps); + constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, shortDotsEnable, metaFields, }: IndexPatternDeps); // (undocumented) [key: string]: any; // (undocumented) From 90f0a294afa921baead26cb37d4d91ca338d2f23 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Tue, 25 Aug 2020 09:29:55 -0400 Subject: [PATCH 003/216] [Actions] change routing key refereence in Pager Duty action message to include integration key (#75516) resolves https://github.com/elastic/kibana/issues/68209 Since routing key figures fairly prominently throughout PagerDuty APIs, and ours, it seems like it make sense to include it in the single validation message we have for it, as well as using the term we use for it in the product: "integration key". See the referenced issue for more background. --- .../builtin_action_types/pagerduty/pagerduty.test.tsx | 2 +- .../components/builtin_action_types/pagerduty/pagerduty.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx index ba7eb598c120d2..0674e5b35c61fb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx @@ -69,7 +69,7 @@ describe('pagerduty connector validation', () => { expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ errors: { - routingKey: ['A routing key is required.'], + routingKey: ['An integration key / routing key is required.'], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx index 5e29fca3971806..90d8da346c71d4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx @@ -38,7 +38,7 @@ export function getActionType(): ActionTypeModel { i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredRoutingKeyText', { - defaultMessage: 'A routing key is required.', + defaultMessage: 'An integration key / routing key is required.', } ) ); From 59c4cd4a6983db6954f28e89b5a8d1267c1d632a Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Tue, 25 Aug 2020 06:33:04 -0700 Subject: [PATCH 004/216] Reduced the number of targets for a proxy server, only actions executions should be affected (#75839) * Reduced the number of targets for a proxy server, only actions executions should be affected * fixed typecheck --- .../common/lib/get_proxy_server.ts | 30 +++++++------------ .../actions/builtin_action_types/jira.ts | 22 +++++++++++++- .../actions/builtin_action_types/pagerduty.ts | 22 +++++++++++++- .../actions/builtin_action_types/resilient.ts | 20 +++++++++++++ .../builtin_action_types/servicenow.ts | 21 ++++++++++++- .../actions/builtin_action_types/slack.ts | 16 ++++++++++ .../actions/builtin_action_types/webhook.ts | 17 +++++++++++ .../tests/actions/index.ts | 19 ------------ 8 files changed, 126 insertions(+), 41 deletions(-) diff --git a/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts b/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts index 7528b00f926d02..2b4908c156e51e 100644 --- a/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts +++ b/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts @@ -4,32 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import http from 'http'; import httpProxy from 'http-proxy'; -import { ToolingLog } from '@kbn/dev-utils'; export const getHttpProxyServer = async ( - defaultKibanaTargetUrl: string, + targetUrl: string, kbnTestServerConfig: any, - log: ToolingLog -): Promise => { - const proxy = httpProxy.createProxyServer({ secure: false, selfHandleResponse: false }); - - const proxyPort = getProxyPort(kbnTestServerConfig); - const proxyServer = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => { - const targetUrl = new URL(req.url ?? defaultKibanaTargetUrl); + onProxyResHandler: (proxyRes?: unknown, req?: unknown, res?: unknown) => void +): Promise => { + const proxyServer = httpProxy.createProxyServer({ + target: targetUrl, + secure: false, + selfHandleResponse: false, + }); - if (targetUrl.hostname !== 'some.non.existent.com') { - proxy.web(req, res, { - target: `${targetUrl.protocol}//${targetUrl.hostname}:${targetUrl.port}`, - }); - } else { - res.writeHead(500, { 'Content-Type': 'text/plain' }); - res.write('error on call some.non.existent.com'); - res.end(); - } + proxyServer.on('proxyRes', (proxyRes: unknown, req: unknown, res: unknown) => { + onProxyResHandler(proxyRes, req, res); }); + const proxyPort = getProxyPort(kbnTestServerConfig); proxyServer.listen(proxyPort); return proxyServer; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 78a1df0b9c1c78..3ffd58b945ddbb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; +import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -35,6 +37,7 @@ const mapping = [ export default function jiraTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const configService = getService('config'); const mockJira = { config: { @@ -80,7 +83,6 @@ export default function jiraTest({ getService }: FtrProviderContext) { getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA) ); }); - describe('Jira - Action Creation', () => { it('should return 200 when creating a jira action successfully', async () => { const { body: createdAction } = await supertest @@ -292,6 +294,9 @@ export default function jiraTest({ getService }: FtrProviderContext) { describe('Jira - Executor', () => { let simulatedActionId: string; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; + before(async () => { const { body } = await supertest .post('/api/actions/action') @@ -307,6 +312,14 @@ export default function jiraTest({ getService }: FtrProviderContext) { secrets: mockJira.secrets, }); simulatedActionId = body.id; + + proxyServer = await getHttpProxyServer( + kibanaServer.resolveUrl('/'), + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); }); describe('Validation', () => { @@ -529,6 +542,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); expect(body).to.eql({ status: 'ok', actionId: simulatedActionId, @@ -541,6 +555,12 @@ export default function jiraTest({ getService }: FtrProviderContext) { }); }); }); + + after(() => { + if (proxyServer) { + proxyServer.close(); + } + }); }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts index 76b3e8e39791a9..0c4d9096aa31ab 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; +import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -17,16 +19,27 @@ import { export default function pagerdutyTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const configService = getService('config'); describe('pagerduty action', () => { let simulatedActionId = ''; let pagerdutySimulatorURL: string = ''; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; // need to wait for kibanaServer to settle ... - before(() => { + before(async () => { pagerdutySimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.PAGERDUTY) ); + + proxyServer = await getHttpProxyServer( + kibanaServer.resolveUrl('/'), + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); }); it('should return successfully when passed valid create parameters', async () => { @@ -145,6 +158,7 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); expect(result).to.eql({ status: 'ok', actionId: simulatedActionId, @@ -203,5 +217,11 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { expect(result.message).to.match(/error posting pagerduty event: http status 502/); expect(result.retry).to.equal(true); }); + + after(() => { + if (proxyServer) { + proxyServer.close(); + } + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts index 8adaf9f1219319..9cbc2373ef9433 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; +import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -35,6 +37,7 @@ const mapping = [ export default function resilientTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const configService = getService('config'); const mockResilient = { config: { @@ -292,6 +295,8 @@ export default function resilientTest({ getService }: FtrProviderContext) { describe('IBM Resilient - Executor', () => { let simulatedActionId: string; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; before(async () => { const { body } = await supertest .post('/api/actions/action') @@ -307,6 +312,14 @@ export default function resilientTest({ getService }: FtrProviderContext) { secrets: mockResilient.secrets, }); simulatedActionId = body.id; + + proxyServer = await getHttpProxyServer( + kibanaServer.resolveUrl('/'), + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); }); describe('Validation', () => { @@ -529,6 +542,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); expect(body).to.eql({ status: 'ok', actionId: simulatedActionId, @@ -541,6 +555,12 @@ export default function resilientTest({ getService }: FtrProviderContext) { }); }); }); + + after(() => { + if (proxyServer) { + proxyServer.close(); + } + }); }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 2dad6f2c425e55..3f8341df3d2950 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; +import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -35,6 +37,7 @@ const mapping = [ export default function servicenowTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const configService = getService('config'); const mockServiceNow = { config: { @@ -264,6 +267,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { describe('ServiceNow - Executor', () => { let simulatedActionId: string; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; before(async () => { const { body } = await supertest .post('/api/actions/action') @@ -279,6 +284,14 @@ export default function servicenowTest({ getService }: FtrProviderContext) { secrets: mockServiceNow.secrets, }); simulatedActionId = body.id; + + proxyServer = await getHttpProxyServer( + kibanaServer.resolveUrl('/'), + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); }); describe('Validation', () => { @@ -448,7 +461,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }, }) .expect(200); - + expect(proxyHaveBeenCalled).to.equal(true); expect(result).to.eql({ status: 'ok', actionId: simulatedActionId, @@ -461,6 +474,12 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }); }); }); + + after(() => { + if (proxyServer) { + proxyServer.close(); + } + }); }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts index 1712c31187b029..83ad17757f3a6f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -4,9 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; import http from 'http'; import getPort from 'get-port'; +import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; @@ -14,11 +16,14 @@ import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simu // eslint-disable-next-line import/no-default-export export default function slackTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const configService = getService('config'); describe('slack action', () => { let simulatedActionId = ''; let slackSimulatorURL: string = ''; let slackServer: http.Server; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; // need to wait for kibanaServer to settle ... before(async () => { @@ -28,6 +33,13 @@ export default function slackTest({ getService }: FtrProviderContext) { slackServer.listen(availablePort); } slackSimulatorURL = `http://localhost:${availablePort}`; + proxyServer = await getHttpProxyServer( + slackSimulatorURL, + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); }); it('should return 200 when creating a slack action successfully', async () => { @@ -157,6 +169,7 @@ export default function slackTest({ getService }: FtrProviderContext) { }) .expect(200); expect(result.status).to.eql('ok'); + expect(proxyHaveBeenCalled).to.equal(true); }); it('should handle an empty message error', async () => { @@ -224,6 +237,9 @@ export default function slackTest({ getService }: FtrProviderContext) { after(() => { slackServer.close(); + if (proxyServer) { + proxyServer.close(); + } }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index abebb2650ad08a..d82d116396cd65 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import httpProxy from 'http-proxy'; import http from 'http'; import expect from '@kbn/expect'; import { URL, format as formatUrl } from 'url'; import getPort from 'get-port'; +import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, @@ -31,6 +33,7 @@ function parsePort(url: Record): Record { @@ -80,6 +85,14 @@ export default function webhookTest({ getService }: FtrProviderContext) { kibanaURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK) ); + + proxyServer = await getHttpProxyServer( + webhookSimulatorURL, + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); }); it('should return 200 when creating a webhook action successfully', async () => { @@ -178,6 +191,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); expect(result.status).to.eql('ok'); }); @@ -241,6 +255,9 @@ export default function webhookTest({ getService }: FtrProviderContext) { after(() => { webhookServer.close(); + if (proxyServer) { + proxyServer.close(); + } }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts index 54484ba34636fb..7b2e5f14fc4b65 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts @@ -4,24 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import http from 'http'; -import { getHttpProxyServer } from '../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function actionsTests({ loadTestFile, getService }: FtrProviderContext) { - const configService = getService('config'); - const kibanaServer = getService('kibanaServer'); - const log = getService('log'); describe('Actions', () => { - let proxyServer: http.Server | undefined; - before(async () => { - proxyServer = await getHttpProxyServer( - kibanaServer.resolveUrl('/'), - configService.get('kbnTestServer.serverArgs'), - log - ); - }); loadTestFile(require.resolve('./builtin_action_types/email')); loadTestFile(require.resolve('./builtin_action_types/es_index')); loadTestFile(require.resolve('./builtin_action_types/es_index_preconfigured')); @@ -39,11 +26,5 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./list_action_types')); loadTestFile(require.resolve('./update')); - - after(() => { - if (proxyServer) { - proxyServer.close(); - } - }); }); } From 1e8c05f87ad5901abeeffe40a730153c3e1658c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 25 Aug 2020 15:15:36 +0100 Subject: [PATCH 005/216] [APM] UI filters: Change transaction type selector from dropdown to radio buttons (#75625) * changing transaction type filter to radio group * fixing unit test * changing transaction type filter to radio group * adding onclick to the badge component * adding onclick to the badge component * adding i18n to aria Co-authored-by: Elastic Machine --- .../TransactionOverview.test.tsx | 46 ++++++++----------- .../app/TransactionOverview/index.tsx | 2 +- .../LocalUIFilters/Filter/FilterBadgeList.tsx | 26 +++++++---- .../shared/LocalUIFilters/Filter/index.tsx | 2 +- .../TransactionTypeFilter/index.tsx | 15 +++--- 5 files changed, 47 insertions(+), 44 deletions(-) rename x-pack/plugins/apm/public/components/app/TransactionOverview/{__jest__ => }/TransactionOverview.test.tsx (70%) diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx similarity index 70% rename from x-pack/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx rename to x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx index 9c514e429c3747..28030dd509835e 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx @@ -4,25 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { + fireEvent, + getByText, queryByLabelText, render, - getByText, - getByDisplayValue, - queryByDisplayValue, - fireEvent, } from '@testing-library/react'; import { omit } from 'lodash'; -import { history } from '../../../../utils/history'; -import { TransactionOverview } from '..'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import * as useServiceTransactionTypesHook from '../../../../hooks/useServiceTransactionTypes'; -import * as useFetcherHook from '../../../../hooks/useFetcher'; -import { fromQuery } from '../../../shared/Links/url_helpers'; +import React from 'react'; import { Router } from 'react-router-dom'; -import { UrlParamsProvider } from '../../../../context/UrlParamsContext'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import { TransactionOverview } from './'; +import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; +import { UrlParamsProvider } from '../../../context/UrlParamsContext'; +import { IUrlParams } from '../../../context/UrlParamsContext/types'; +import * as useFetcherHook from '../../../hooks/useFetcher'; +import * as useServiceTransactionTypesHook from '../../../hooks/useServiceTransactionTypes'; +import { history } from '../../../utils/history'; +import { fromQuery } from '../../shared/Links/url_helpers'; jest.spyOn(history, 'push'); jest.spyOn(history, 'replace'); @@ -85,7 +83,7 @@ describe('TransactionOverview', () => { const FILTER_BY_TYPE_LABEL = 'Transaction type'; describe('when transactionType is selected and multiple transaction types are given', () => { - it('should render dropdown with transaction types', () => { + it('renders a radio group with transaction types', () => { const { container } = setup({ serviceTransactionTypes: ['firstType', 'secondType'], urlParams: { @@ -94,9 +92,8 @@ describe('TransactionOverview', () => { }, }); - // secondType is selected in the dropdown - expect(queryByDisplayValue(container, 'secondType')).not.toBeNull(); - expect(queryByDisplayValue(container, 'firstType')).toBeNull(); + expect(getByText(container, 'firstType')).toBeInTheDocument(); + expect(getByText(container, 'secondType')).toBeInTheDocument(); expect(getByText(container, 'firstType')).not.toBeNull(); }); @@ -110,22 +107,19 @@ describe('TransactionOverview', () => { }, }); - expect(queryByDisplayValue(container, 'firstType')).toBeNull(); + expect(history.location.search).toEqual('?transactionType=secondType'); + expect(getByText(container, 'firstType')).toBeInTheDocument(); + expect(getByText(container, 'secondType')).toBeInTheDocument(); - fireEvent.change(getByDisplayValue(container, 'secondType'), { - target: { value: 'firstType' }, - }); + fireEvent.click(getByText(container, 'firstType')); expect(history.push).toHaveBeenCalled(); - - getByDisplayValue(container, 'firstType'); - - expect(queryByDisplayValue(container, 'firstType')).not.toBeNull(); + expect(history.location.search).toEqual('?transactionType=firstType'); }); }); describe('when a transaction type is selected, and there are no other transaction types', () => { - it('should not render a dropdown with transaction types', () => { + it('does not render a radio group with transaction types', () => { const { container } = setup({ serviceTransactionTypes: ['firstType'], urlParams: { diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index d9bd3e59d281f1..f6eb131a8a733b 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -121,7 +121,7 @@ export function TransactionOverview() { - + diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx index 2090a92bf0de4c..ed8d865d2d2889 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx @@ -5,8 +5,9 @@ */ import React from 'react'; -import { EuiFlexGrid, EuiFlexItem, EuiBadge, EuiIcon } from '@elastic/eui'; +import { EuiFlexGrid, EuiFlexItem, EuiBadge } from '@elastic/eui'; import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; import { unit, px, truncate } from '../../../../style/variables'; const BadgeText = styled.div` @@ -20,22 +21,31 @@ interface Props { onRemove: (val: string) => void; } +const removeFilterLabel = i18n.translate( + 'xpack.apm.uifilter.badge.removeFilter', + { defaultMessage: 'Remove filter' } +); + function FilterBadgeList({ onRemove, value }: Props) { return ( {value.map((val) => ( - + {val} + ))} diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx index c13439a3c5928e..48ebc2add00539 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx @@ -164,7 +164,7 @@ function Filter({ name, title, options, onChange, value, showCount }: Props) { }} value={value} /> - + ) : null} diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx index afd2d023d16bad..54a08e9d45af50 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx @@ -9,7 +9,7 @@ import { EuiTitle, EuiHorizontalRule, EuiSpacer, - EuiSelect, + EuiRadioGroup, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useUrlParams } from '../../../../hooks/useUrlParams'; @@ -26,8 +26,8 @@ function TransactionTypeFilter({ transactionTypes }: Props) { } = useUrlParams(); const options = transactionTypes.map((type) => ({ - text: type, - value: type, + id: type, + label: type, })); return ( @@ -42,16 +42,15 @@ function TransactionTypeFilter({ transactionTypes }: Props) { - { + idSelected={transactionType} + onChange={(selectedTransactionType) => { const newLocation = { ...history.location, search: fromQuery({ ...toQuery(history.location.search), - transactionType: event.target.value, + transactionType: selectedTransactionType, }), }; history.push(newLocation); From c3b6745e3db705a9e14f4b951d967e9880bdd116 Mon Sep 17 00:00:00 2001 From: James Rodewig <40268737+jrodewig@users.noreply.github.com> Date: Tue, 25 Aug 2020 10:29:57 -0400 Subject: [PATCH 006/216] Correct punctuation for ingest processors help text (#75695) --- .../processors/common_fields/ignore_missing_field.tsx | 2 +- .../manage_processor_form/processors/date_index_name.tsx | 2 +- .../components/manage_processor_form/processors/enrich.tsx | 6 +++--- .../components/manage_processor_form/processors/fail.tsx | 2 +- .../components/manage_processor_form/processors/foreach.tsx | 4 ++-- .../components/manage_processor_form/processors/geoip.tsx | 4 ++-- .../components/manage_processor_form/processors/grok.tsx | 4 ++-- .../components/manage_processor_form/processors/gsub.tsx | 6 +++--- .../manage_processor_form/processors/html_strip.tsx | 2 +- .../manage_processor_form/processors/inference.tsx | 2 +- .../components/manage_processor_form/processors/join.tsx | 4 ++-- .../components/manage_processor_form/processors/json.tsx | 2 +- 12 files changed, 20 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx index 35dd462d88425e..63ebb47dfc5734 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx @@ -32,7 +32,7 @@ export const fieldsConfig: FieldsConfig = { helpText: ( {'field'}, }} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx index 2a278a251c30f0..8cbc064c1c90ce 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx @@ -133,7 +133,7 @@ const fieldsConfig: FieldsConfig = { helpText: ( {'ENGLISH'} }} /> ), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx index 31eac38222afbc..5986374b338cfe 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx @@ -157,7 +157,7 @@ export const Enrich: FunctionComponent = () => { helpText: ( @@ -182,7 +182,7 @@ export const Enrich: FunctionComponent = () => { helpText={i18n.translate( 'xpack.ingestPipelines.pipelineEditor.enrichForm.targetFieldHelpText', { - defaultMessage: 'Field used to contain enrich data', + defaultMessage: 'Field used to contain enrich data.', } )} validations={[targetFieldValidator]} @@ -202,7 +202,7 @@ export const Enrich: FunctionComponent = () => { helpText: ( { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx index ef2aa62c4a7de5..9bb1d679938edf 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx @@ -80,7 +80,7 @@ export const GeoIP: FunctionComponent = () => { @@ -88,7 +88,7 @@ export const GeoIP: FunctionComponent = () => { helpText={i18n.translate( 'xpack.ingestPipelines.pipelineEditor.geoIPForm.targetFieldHelpText', { - defaultMessage: 'Field used to contain geo data properties', + defaultMessage: 'Field used to contain geo data properties.', } )} /> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx index 1ed9898149a679..d021038fda94fb 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx @@ -87,7 +87,7 @@ const fieldsConfig: FieldsConfig = { helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.grokForm.traceMatchFieldHelpText', { - defaultMessage: 'Add metadata about the matching expression to the document', + defaultMessage: 'Add metadata about the matching expression to the document.', } ), }, @@ -99,7 +99,7 @@ export const Grok: FunctionComponent = () => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx index 4d3445d469da2e..a0bda245d667b5 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx @@ -29,7 +29,7 @@ const fieldsConfig: FieldsConfig = { }), deserializer: String, helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.patternFieldHelpText', { - defaultMessage: 'Regular expression used to match substrings in the field', + defaultMessage: 'Regular expression used to match substrings in the field.', }), validations: [ { @@ -49,7 +49,7 @@ const fieldsConfig: FieldsConfig = { }), helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.gsubForm.replacementFieldHelpText', - { defaultMessage: 'Replacement text for matches' } + { defaultMessage: 'Replacement text for matches.' } ), validations: [ { @@ -69,7 +69,7 @@ export const Gsub: FunctionComponent = () => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/html_strip.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/html_strip.tsx index c6ca7df4cc3e77..fb1a2d97672b01 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/html_strip.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/html_strip.tsx @@ -19,7 +19,7 @@ export const HtmlStrip: FunctionComponent = () => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx index ee8d7cc55a9f19..68281fc11f3402 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx @@ -82,7 +82,7 @@ const fieldsConfig: FieldsConfig = { helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.inferenceForm.modelIDFieldHelpText', { - defaultMessage: 'ID of the model to infer against', + defaultMessage: 'ID of the model to infer against.', } ), validations: [ diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx index 712d0106459b1e..c35a5b463f5732 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx @@ -28,7 +28,7 @@ const fieldsConfig: FieldsConfig = { helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.joinForm.separatorFieldHelpText', { - defaultMessage: 'Separator character', + defaultMessage: 'Separator character.', } ), validations: [ @@ -49,7 +49,7 @@ export const Join: FunctionComponent = () => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx index 9d62c67460136d..5c4c53b65b6dc2 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx @@ -61,7 +61,7 @@ export const Json: FunctionComponent = () => { From 75232a74f3be93135ac9f1be959b2890301ad662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 25 Aug 2020 15:39:57 +0100 Subject: [PATCH 007/216] [APM] Implement nest level expand/collapse toggle for each span row (#75259) * returning an waterfallTransaction * fixing style * fixing unit test * fixing style * addressing PR comment * addressing PR comment Co-authored-by: Elastic Machine --- .../Waterfall/WaterfallItem.tsx | 6 +- .../Waterfall/accordion_waterfall.tsx | 170 ++ .../WaterfallContainer/Waterfall/index.tsx | 121 +- .../waterfall_helpers.test.ts.snap | 1949 +++++++++++++---- .../waterfall_helpers/waterfall_helpers.ts | 9 +- .../WaterfallContainer/index.tsx | 2 +- .../WaterfallWithSummmary/index.tsx | 6 +- 7 files changed, 1759 insertions(+), 504 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx index a4d42bcf51d01b..e1b5ffcd0e0f5e 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx @@ -40,7 +40,6 @@ const Container = styled.div` padding-bottom: ${px(units.plus)}; margin-right: ${(props) => px(props.timelineMargins.right)}; margin-left: ${(props) => px(props.timelineMargins.left)}; - border-top: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; background-color: ${({ isSelected, theme }) => isSelected ? theme.eui.euiColorLightestShade : 'initial'}; cursor: pointer; @@ -191,7 +190,10 @@ export function WaterfallItem({ type={item.docType} timelineMargins={timelineMargins} isSelected={isSelected} - onClick={onClick} + onClick={(e: React.MouseEvent) => { + e.stopPropagation(); + onClick(); + }} > ; + onToggleEntryTransaction?: ( + nextState: EuiAccordionProps['forceState'] + ) => void; + timelineMargins: Margins; + onClickWaterfallItem: (item: IWaterfallItem) => void; +} + +const StyledAccordion = styled(EuiAccordion).withConfig({ + shouldForwardProp: (prop) => + !['childrenCount', 'marginLeftLevel', 'hasError'].includes(prop), +})< + EuiAccordionProps & { + childrenCount: number; + marginLeftLevel: number; + hasError: boolean; + } +>` + .euiAccordion { + border-top: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; + } + .euiIEFlexWrapFix { + width: 100%; + height: 48px; + } + .euiAccordion__childWrapper { + transition: none; + } + + .euiAccordion__padding--l { + padding-top: 0; + padding-bottom: 0; + } + + .euiAccordion__iconWrapper { + display: flex; + position: relative; + &:after { + content: ${(props) => `'${props.childrenCount}'`}; + position: absolute; + left: 20px; + top: -1px; + z-index: 1; + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; + } + } + + ${(props) => { + const borderLeft = props.hasError + ? `2px solid ${props.theme.eui.euiColorDanger};` + : `1px solid ${props.theme.eui.euiColorLightShade};`; + return `.button_${props.id} { + margin-left: ${props.marginLeftLevel}px; + border-left: ${borderLeft} + &:hover { + background-color: ${props.theme.eui.euiColorLightestShade}; + } + }`; + // + }} +`; + +const WaterfallItemContainer = styled.div` + position: absolute; + width: 100%; + left: 0; +`; + +export function AccordionWaterfall(props: AccordionWaterfallProps) { + const [isOpen, setIsOpen] = useState(props.isOpen); + + const { + item, + level, + serviceColors, + duration, + childrenByParentId, + waterfallItemId, + location, + errorsPerTransaction, + timelineMargins, + onClickWaterfallItem, + } = props; + + const nextLevel = level + 1; + + const errorCount = + item.docType === 'transaction' + ? errorsPerTransaction[item.doc.transaction.id] + : 0; + + const children = childrenByParentId[item.id] || []; + + // To indent the items creating the parent/child tree + const marginLeftLevel = 8 * level; + + return ( + 0} + marginLeftLevel={marginLeftLevel} + childrenCount={children.length} + buttonContent={ + + { + onClickWaterfallItem(item); + }} + /> + + } + arrowDisplay={isEmpty(children) ? 'none' : 'left'} + initialIsOpen={true} + forceState={isOpen ? 'open' : 'closed'} + onToggle={() => setIsOpen((isCurrentOpen) => !isCurrentOpen)} + > + {children.map((child) => ( + + ))} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx index 1fd0ec761b1aec..7daf1b798749bf 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx @@ -4,21 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiCallOut } from '@elastic/eui'; +import { EuiButtonEmpty, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Location } from 'history'; -import React from 'react'; +import React, { useState } from 'react'; // @ts-ignore import { StickyContainer } from 'react-sticky'; import styled from 'styled-components'; import { px } from '../../../../../../style/variables'; import { history } from '../../../../../../utils/history'; import { Timeline } from '../../../../../shared/charts/Timeline'; +import { HeightRetainer } from '../../../../../shared/HeightRetainer'; import { fromQuery, toQuery } from '../../../../../shared/Links/url_helpers'; import { getAgentMarks } from '../Marks/get_agent_marks'; import { getErrorMarks } from '../Marks/get_error_marks'; +import { AccordionWaterfall } from './accordion_waterfall'; import { WaterfallFlyout } from './WaterfallFlyout'; -import { WaterfallItem } from './WaterfallItem'; import { IWaterfall, IWaterfallItem, @@ -32,7 +33,7 @@ const Container = styled.div` const TIMELINE_MARGINS = { top: 40, - left: 50, + left: 100, right: 50, bottom: 0, }; @@ -58,6 +59,7 @@ const WaterfallItemsContainer = styled.div<{ paddingTop: number; }>` padding-top: ${(props) => px(props.paddingTop)}; + border-bottom: 1px solid ${({ theme }) => theme.eui.euiColorMediumShade}; `; interface Props { @@ -66,72 +68,91 @@ interface Props { location: Location; exceedsMax: boolean; } - export function Waterfall({ waterfall, exceedsMax, waterfallItemId, location, }: Props) { + const [isAccordionOpen, setIsAccordionOpen] = useState(true); const itemContainerHeight = 58; // TODO: This is a nasty way to calculate the height of the svg element. A better approach should be found const waterfallHeight = itemContainerHeight * waterfall.items.length; const { serviceColors, duration } = waterfall; - const agentMarks = getAgentMarks(waterfall.entryTransaction); + const agentMarks = getAgentMarks(waterfall.entryWaterfallTransaction?.doc); const errorMarks = getErrorMarks(waterfall.errorItems, serviceColors); - function renderWaterfallItem(item: IWaterfallItem) { - const errorCount = - item.docType === 'transaction' - ? waterfall.errorsPerTransaction[item.doc.transaction.id] - : 0; - + function renderItems( + childrenByParentId: Record + ) { + const { entryWaterfallTransaction } = waterfall; + if (!entryWaterfallTransaction) { + return null; + } return ( - toggleFlyout({ item, location })} + onClickWaterfallItem={(item: IWaterfallItem) => + toggleFlyout({ item, location }) + } /> ); } return ( - - {exceedsMax && ( - - )} - - - - {waterfall.items.map(renderWaterfallItem)} - - + + + {exceedsMax && ( + + )} + +
+ { + setIsAccordionOpen((isOpen) => !isOpen); + }} + /> + +
+ + {renderItems(waterfall.childrenByParentId)} + +
- -
+ +
+ ); } diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap index c9b29e8692f447..204c5e9ae6da2b 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap @@ -2,27 +2,734 @@ exports[`waterfall_helpers getWaterfall should return full waterfall 1`] = ` Object { + "childrenByParentId": Object { + "mySpanIdA": Array [ + Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdA", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 481, + }, + "id": "mySpanIdB", + "name": "SELECT FROM products", + }, + "timestamp": Object { + "us": 1549324795825633, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 481, + "id": "mySpanIdB", + "offset": 41627, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + "parentId": "mySpanIdD", + "skew": 0, + }, + "parentId": "myTransactionId2", + "skew": 0, + }, + "parentId": "mySpanIdA", + "skew": 0, + }, + Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdA", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 532, + }, + "id": "mySpanIdC", + "name": "SELECT FROM product", + }, + "timestamp": Object { + "us": 1549324795827905, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 532, + "id": "mySpanIdC", + "offset": 43899, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + "parentId": "mySpanIdD", + "skew": 0, + }, + "parentId": "myTransactionId2", + "skew": 0, + }, + "parentId": "mySpanIdA", + "skew": 0, + }, + ], + "mySpanIdD": Array [ + Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + "parentId": "mySpanIdD", + "skew": 0, + }, + ], + "myTransactionId1": Array [ + Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + ], + "myTransactionId2": Array [ + Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + "parentId": "mySpanIdD", + "skew": 0, + }, + "parentId": "myTransactionId2", + "skew": 0, + }, + ], + "root": Array [ + Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + ], + }, "duration": 49660, - "entryTransaction": Object { - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-node", - }, - "timestamp": Object { - "us": 1549324795784006, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "duration": Object { - "us": 49660, + "entryWaterfallTransaction": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", }, - "id": "myTransactionId1", - "name": "GET /api", }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, }, "errorItems": Array [ Object { @@ -42,13 +749,115 @@ Object { "id": "myTransactionId1", }, "processor": Object { - "event": "error", + "event": "error", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795810000, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "error", + "duration": 0, + "id": "error1", + "offset": 25994, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + ], + "errorsCount": 1, + "errorsPerTransaction": Object { + "myTransactionId1": 2, + "myTransactionId2": 3, + }, + "items": Array [ + Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", }, "service": Object { - "name": "opbeans-ruby", + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", }, "timestamp": Object { - "us": 1549324795810000, + "us": 1549324795785760, }, "trace": Object { "id": "myTraceId", @@ -57,10 +866,10 @@ Object { "id": "myTransactionId1", }, }, - "docType": "error", - "duration": 0, - "id": "error1", - "offset": 25994, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, "parent": Object { "doc": Object { "processor": Object { @@ -94,387 +903,744 @@ Object { "parentId": "myTransactionId1", "skew": 0, }, - ], - "errorsCount": 1, - "errorsPerTransaction": Object { - "myTransactionId1": 2, - "myTransactionId2": 3, - }, - "items": Array [ Object { "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, "processor": Object { "event": "transaction", }, "service": Object { - "name": "opbeans-node", + "name": "opbeans-ruby", }, "timestamp": Object { - "us": 1549324795784006, + "us": 1549324795823304, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { "duration": Object { - "us": 49660, + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, }, + "docType": "transaction", + "duration": 49660, "id": "myTransactionId1", - "name": "GET /api", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + "parentId": "mySpanIdD", + "skew": 0, + }, + Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, }, + "parentId": "mySpanIdD", + "skew": 0, }, - "docType": "transaction", - "duration": 49660, - "id": "myTransactionId1", - "offset": 0, - "parent": undefined, - "parentId": undefined, + "parentId": "myTransactionId2", "skew": 0, }, Object { "doc": Object { "parent": Object { - "id": "myTransactionId1", + "id": "mySpanIdA", }, "processor": Object { "event": "span", }, "service": Object { - "name": "opbeans-node", + "name": "opbeans-ruby", }, "span": Object { "duration": Object { - "us": 47557, + "us": 481, }, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", + "id": "mySpanIdB", + "name": "SELECT FROM products", }, "timestamp": Object { - "us": 1549324795785760, + "us": 1549324795825633, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "id": "myTransactionId1", + "id": "myTransactionId2", }, }, "docType": "span", - "duration": 47557, - "id": "mySpanIdD", - "offset": 1754, + "duration": 481, + "id": "mySpanIdB", + "offset": 41627, "parent": Object { "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, "processor": Object { - "event": "transaction", + "event": "span", }, "service": Object { - "name": "opbeans-node", + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", }, "timestamp": Object { - "us": 1549324795784006, + "us": 1549324795824504, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "duration": Object { - "us": 49660, + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", }, - "id": "myTransactionId1", - "name": "GET /api", + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, }, + "parentId": "mySpanIdD", + "skew": 0, }, - "docType": "transaction", - "duration": 49660, - "id": "myTransactionId1", - "offset": 0, - "parent": undefined, - "parentId": undefined, + "parentId": "myTransactionId2", "skew": 0, }, - "parentId": "myTransactionId1", + "parentId": "mySpanIdA", "skew": 0, }, Object { "doc": Object { "parent": Object { - "id": "mySpanIdD", + "id": "mySpanIdA", }, "processor": Object { - "event": "transaction", + "event": "span", }, "service": Object { "name": "opbeans-ruby", }, + "span": Object { + "duration": Object { + "us": 532, + }, + "id": "mySpanIdC", + "name": "SELECT FROM product", + }, "timestamp": Object { - "us": 1549324795823304, + "us": 1549324795827905, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "duration": Object { - "us": 8634, - }, "id": "myTransactionId2", - "marks": Object { - "agent": Object { - "domComplete": 383, - "domInteractive": 382, - "timeToFirstByte": 14, - }, - }, - "name": "Api::ProductsController#index", }, }, - "docType": "transaction", - "duration": 8634, - "id": "myTransactionId2", - "offset": 39298, + "docType": "span", + "duration": 532, + "id": "mySpanIdC", + "offset": 43899, "parent": Object { "doc": Object { "parent": Object { - "id": "myTransactionId1", + "id": "myTransactionId2", }, "processor": Object { "event": "span", }, "service": Object { - "name": "opbeans-node", + "name": "opbeans-ruby", }, "span": Object { "duration": Object { - "us": 47557, + "us": 6161, }, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", + "id": "mySpanIdA", + "name": "Api::ProductsController#index", }, "timestamp": Object { - "us": 1549324795785760, + "us": 1549324795824504, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "id": "myTransactionId1", + "id": "myTransactionId2", }, }, "docType": "span", - "duration": 47557, - "id": "mySpanIdD", - "offset": 1754, + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, "parent": Object { "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, "processor": Object { "event": "transaction", }, "service": Object { - "name": "opbeans-node", + "name": "opbeans-ruby", }, "timestamp": Object { - "us": 1549324795784006, + "us": 1549324795823304, }, "trace": Object { "id": "myTraceId", }, - "transaction": Object { - "duration": Object { - "us": 49660, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, }, + "docType": "transaction", + "duration": 49660, "id": "myTransactionId1", - "name": "GET /api", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, }, + "parentId": "myTransactionId1", + "skew": 0, }, - "docType": "transaction", - "duration": 49660, - "id": "myTransactionId1", - "offset": 0, - "parent": undefined, - "parentId": undefined, + "parentId": "mySpanIdD", "skew": 0, }, - "parentId": "myTransactionId1", + "parentId": "myTransactionId2", "skew": 0, }, - "parentId": "mySpanIdD", + "parentId": "mySpanIdA", "skew": 0, }, - Object { - "doc": Object { - "parent": Object { - "id": "myTransactionId2", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 6161, - }, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - }, - "timestamp": Object { - "us": 1549324795824504, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", - }, + ], + "rootTransaction": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, }, - "docType": "span", - "duration": 6161, - "id": "mySpanIdA", - "offset": 40498, - "parent": Object { + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "serviceColors": Object { + "opbeans-node": "#6092c0", + "opbeans-ruby": "#54b399", + }, +} +`; + +exports[`waterfall_helpers getWaterfall should return partial waterfall 1`] = ` +Object { + "childrenByParentId": Object { + "mySpanIdA": Array [ + Object { "doc": Object { "parent": Object { - "id": "mySpanIdD", + "id": "mySpanIdA", }, "processor": Object { - "event": "transaction", + "event": "span", }, "service": Object { "name": "opbeans-ruby", }, + "span": Object { + "duration": Object { + "us": 481, + }, + "id": "mySpanIdB", + "name": "SELECT FROM products", + }, "timestamp": Object { - "us": 1549324795823304, + "us": 1549324795825633, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "duration": Object { - "us": 8634, - }, "id": "myTransactionId2", - "marks": Object { - "agent": Object { - "domComplete": 383, - "domInteractive": 382, - "timeToFirstByte": 14, - }, - }, - "name": "Api::ProductsController#index", }, }, - "docType": "transaction", - "duration": 8634, - "id": "myTransactionId2", - "offset": 39298, + "docType": "span", + "duration": 481, + "id": "mySpanIdB", + "offset": 2329, "parent": Object { "doc": Object { "parent": Object { - "id": "myTransactionId1", + "id": "myTransactionId2", }, "processor": Object { "event": "span", }, "service": Object { - "name": "opbeans-node", + "name": "opbeans-ruby", }, "span": Object { "duration": Object { - "us": 47557, + "us": 6161, }, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", + "id": "mySpanIdA", + "name": "Api::ProductsController#index", }, "timestamp": Object { - "us": 1549324795785760, + "us": 1549324795824504, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "id": "myTransactionId1", + "id": "myTransactionId2", }, }, "docType": "span", - "duration": 47557, - "id": "mySpanIdD", - "offset": 1754, + "duration": 6161, + "id": "mySpanIdA", + "offset": 1200, "parent": Object { "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, "processor": Object { "event": "transaction", }, "service": Object { - "name": "opbeans-node", + "name": "opbeans-ruby", }, "timestamp": Object { - "us": 1549324795784006, + "us": 1549324795823304, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { "duration": Object { - "us": 49660, + "us": 8634, }, - "id": "myTransactionId1", - "name": "GET /api", + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", }, }, "docType": "transaction", - "duration": 49660, - "id": "myTransactionId1", + "duration": 8634, + "id": "myTransactionId2", "offset": 0, "parent": undefined, - "parentId": undefined, + "parentId": "mySpanIdD", "skew": 0, }, - "parentId": "myTransactionId1", + "parentId": "myTransactionId2", "skew": 0, }, - "parentId": "mySpanIdD", + "parentId": "mySpanIdA", "skew": 0, }, - "parentId": "myTransactionId2", - "skew": 0, - }, - Object { - "doc": Object { - "parent": Object { - "id": "mySpanIdA", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 481, - }, - "id": "mySpanIdB", - "name": "SELECT FROM products", - }, - "timestamp": Object { - "us": 1549324795825633, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", - }, - }, - "docType": "span", - "duration": 481, - "id": "mySpanIdB", - "offset": 41627, - "parent": Object { + Object { "doc": Object { "parent": Object { - "id": "myTransactionId2", + "id": "mySpanIdA", }, "processor": Object { "event": "span", @@ -484,13 +1650,13 @@ Object { }, "span": Object { "duration": Object { - "us": 6161, + "us": 532, }, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", + "id": "mySpanIdC", + "name": "SELECT FROM product", }, "timestamp": Object { - "us": 1549324795824504, + "us": 1549324795827905, }, "trace": Object { "id": "myTraceId", @@ -500,152 +1666,132 @@ Object { }, }, "docType": "span", - "duration": 6161, - "id": "mySpanIdA", - "offset": 40498, + "duration": 532, + "id": "mySpanIdC", + "offset": 4601, "parent": Object { "doc": Object { "parent": Object { - "id": "mySpanIdD", + "id": "myTransactionId2", }, "processor": Object { - "event": "transaction", + "event": "span", }, "service": Object { "name": "opbeans-ruby", }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, "timestamp": Object { - "us": 1549324795823304, + "us": 1549324795824504, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "duration": Object { - "us": 8634, - }, "id": "myTransactionId2", - "marks": Object { - "agent": Object { - "domComplete": 383, - "domInteractive": 382, - "timeToFirstByte": 14, - }, - }, - "name": "Api::ProductsController#index", }, }, - "docType": "transaction", - "duration": 8634, - "id": "myTransactionId2", - "offset": 39298, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 1200, "parent": Object { "doc": Object { "parent": Object { - "id": "myTransactionId1", + "id": "mySpanIdD", }, "processor": Object { - "event": "span", + "event": "transaction", }, "service": Object { - "name": "opbeans-node", - }, - "span": Object { - "duration": Object { - "us": 47557, - }, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", + "name": "opbeans-ruby", }, "timestamp": Object { - "us": 1549324795785760, + "us": 1549324795823304, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "id": "myTransactionId1", - }, - }, - "docType": "span", - "duration": 47557, - "id": "mySpanIdD", - "offset": 1754, - "parent": Object { - "doc": Object { - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-node", - }, - "timestamp": Object { - "us": 1549324795784006, - }, - "trace": Object { - "id": "myTraceId", + "duration": Object { + "us": 8634, }, - "transaction": Object { - "duration": Object { - "us": 49660, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, }, - "id": "myTransactionId1", - "name": "GET /api", }, + "name": "Api::ProductsController#index", }, - "docType": "transaction", - "duration": 49660, - "id": "myTransactionId1", - "offset": 0, - "parent": undefined, - "parentId": undefined, - "skew": 0, }, - "parentId": "myTransactionId1", + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 0, + "parent": undefined, + "parentId": "mySpanIdD", "skew": 0, }, - "parentId": "mySpanIdD", + "parentId": "myTransactionId2", "skew": 0, }, - "parentId": "myTransactionId2", + "parentId": "mySpanIdA", "skew": 0, }, - "parentId": "mySpanIdA", - "skew": 0, - }, - Object { - "doc": Object { - "parent": Object { - "id": "mySpanIdA", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 532, + ], + "mySpanIdD": Array [ + Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", }, - "id": "mySpanIdC", - "name": "SELECT FROM product", - }, - "timestamp": Object { - "us": 1549324795827905, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 0, + "parent": undefined, + "parentId": "mySpanIdD", + "skew": 0, }, - "docType": "span", - "duration": 532, - "id": "mySpanIdC", - "offset": 43899, - "parent": Object { + ], + "myTransactionId2": Array [ + Object { "doc": Object { "parent": Object { "id": "myTransactionId2", @@ -676,7 +1822,7 @@ Object { "docType": "span", "duration": 6161, "id": "mySpanIdA", - "offset": 40498, + "offset": 1200, "parent": Object { "doc": Object { "parent": Object { @@ -712,143 +1858,56 @@ Object { "docType": "transaction", "duration": 8634, "id": "myTransactionId2", - "offset": 39298, - "parent": Object { - "doc": Object { - "parent": Object { - "id": "myTransactionId1", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-node", - }, - "span": Object { - "duration": Object { - "us": 47557, - }, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", - }, - "timestamp": Object { - "us": 1549324795785760, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId1", - }, - }, - "docType": "span", - "duration": 47557, - "id": "mySpanIdD", - "offset": 1754, - "parent": Object { - "doc": Object { - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-node", - }, - "timestamp": Object { - "us": 1549324795784006, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "duration": Object { - "us": 49660, - }, - "id": "myTransactionId1", - "name": "GET /api", - }, - }, - "docType": "transaction", - "duration": 49660, - "id": "myTransactionId1", - "offset": 0, - "parent": undefined, - "parentId": undefined, - "skew": 0, - }, - "parentId": "myTransactionId1", - "skew": 0, - }, + "offset": 0, + "parent": undefined, "parentId": "mySpanIdD", "skew": 0, }, "parentId": "myTransactionId2", "skew": 0, }, - "parentId": "mySpanIdA", - "skew": 0, - }, - ], - "rootTransaction": Object { - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-node", - }, - "timestamp": Object { - "us": 1549324795784006, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "duration": Object { - "us": 49660, - }, - "id": "myTransactionId1", - "name": "GET /api", - }, - }, - "serviceColors": Object { - "opbeans-node": "#6092c0", - "opbeans-ruby": "#54b399", + ], }, -} -`; - -exports[`waterfall_helpers getWaterfall should return partial waterfall 1`] = ` -Object { "duration": 8634, - "entryTransaction": Object { - "parent": Object { - "id": "mySpanIdD", - }, - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "timestamp": Object { - "us": 1549324795823304, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "duration": Object { - "us": 8634, + "entryWaterfallTransaction": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", }, - "id": "myTransactionId2", - "marks": Object { - "agent": Object { - "domComplete": 383, - "domInteractive": 382, - "timeToFirstByte": 14, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, }, + "name": "Api::ProductsController#index", }, - "name": "Api::ProductsController#index", }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 0, + "parent": undefined, + "parentId": "mySpanIdD", + "skew": 0, }, "errorItems": Array [], "errorsCount": 0, diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts index 441a51bcba646d..44e5e09e506af2 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts @@ -28,7 +28,7 @@ interface IWaterfallGroup { const ROOT_ID = 'root'; export interface IWaterfall { - entryTransaction?: Transaction; + entryWaterfallTransaction?: IWaterfallTransaction; rootTransaction?: Transaction; /** @@ -36,6 +36,7 @@ export interface IWaterfall { */ duration: number; items: IWaterfallItem[]; + childrenByParentId: Record; errorsPerTransaction: TraceAPIResponse['errorsPerTransaction']; errorsCount: number; serviceColors: IServiceColors; @@ -329,6 +330,7 @@ export function getWaterfall( errorsCount: sum(Object.values(errorsPerTransaction)), serviceColors: {}, errorItems: [], + childrenByParentId: {}, }; } @@ -357,10 +359,8 @@ export function getWaterfall( const duration = getWaterfallDuration(items); const serviceColors = getServiceColors(items); - const entryTransaction = entryWaterfallTransaction?.doc; - return { - entryTransaction, + entryWaterfallTransaction, rootTransaction, duration, items, @@ -368,5 +368,6 @@ export function getWaterfall( errorsCount: errorItems.length, serviceColors, errorItems, + childrenByParentId: getChildrenGroupedByParentId(items), }; } diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx index 6fd139b470ce15..501ca6d33d5af1 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx @@ -8,8 +8,8 @@ import { Location } from 'history'; import React from 'react'; import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; import { ServiceLegends } from './ServiceLegends'; -import { Waterfall } from './Waterfall'; import { IWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers'; +import { Waterfall } from './Waterfall'; interface Props { urlParams: IUrlParams; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx index 12676b7c15f1c0..392bd90ffbabc9 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx @@ -64,8 +64,8 @@ export function WaterfallWithSummmary({ }); }; - const { entryTransaction } = waterfall; - if (!entryTransaction) { + const { entryWaterfallTransaction } = waterfall; + if (!entryWaterfallTransaction) { const content = isLoading ? ( ) : ( @@ -84,6 +84,8 @@ export function WaterfallWithSummmary({ return {content}; } + const entryTransaction = entryWaterfallTransaction.doc; + return ( From 1dc48b3fdd0b32b1bd4aaf9169498aaa328e5a39 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 25 Aug 2020 08:07:10 -0700 Subject: [PATCH 008/216] [src/dev/build] stop including public source in distributable (#75841) Co-authored-by: spalger --- src/dev/build/tasks/copy_source_task.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/build/tasks/copy_source_task.ts b/src/dev/build/tasks/copy_source_task.ts index 7a5d84da527dbc..948e2357effb04 100644 --- a/src/dev/build/tasks/copy_source_task.ts +++ b/src/dev/build/tasks/copy_source_task.ts @@ -33,11 +33,11 @@ export const CopySource: Task = { '!src/**/{target,__tests__,__snapshots__,__mocks__}/**', '!src/test_utils/**', '!src/fixtures/**', - '!src/legacy/core_plugins/console/public/tests/**', '!src/cli/cluster/**', '!src/cli/repl/**', '!src/functional_test_runner/**', '!src/dev/**', + '!**/public/**', 'typings/**', 'config/kibana.yml', 'config/node.options', From 8f85593910f49afc48e335618f0ef4aaede5efb5 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 25 Aug 2020 09:22:13 -0600 Subject: [PATCH 009/216] [Security Solution] Fixes assert unreachable to be within the common section and the type to never (#75798) ## Summary Assert unreachable was created through advice given by both the Typescript community and through the techniques that TyepScript is trying to achieve type safety with switch statements. This fixes recent bugs by: * Re-adding the never type * Reduces the two different types by putting the helper within the common section so there's not duplication * Fixes on type that looks like it was a regular string rather than a one of the enum types The reasoning for exhaustive checks within switch statements and techniques can be seen in numerous areas such as here: https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript You can do it either way with TypeScript as long as you ensure you have a explicit return type and you do early return statements you can actually avoid having to call into the assertUnreachable. If introduced and used correctly it is there to help out like this error it is telling us that this string type is not exhaustive: Screen Shot 2020-08-24 at 10 39 42 AM You can notice that for this pull request I actually remove the assertion like so if someone accidentally removes one of the switch statements: Screen Shot 2020-08-24 at 10 42 08 AM And since the function has an explicit return type it is not needed. You will see that TypeScript improved its never types behind the scenes where it actually will tell you that it will never reach the `assertUnreachable` and want to remove it as an auto-refactor. That is ok as long as we have explicit return types and what I did with one line of code here. Screen Shot 2020-08-24 at 11 21 05 AM Without this fix, and having the never type become an unknown it introduces less safety where any code that is utilizing the assertUnknown without explicit return types will be prone to having run time errors being thrown when something new is added to their switch enum types. --- .../search_strategy/security_solution/index.ts | 2 +- .../security_solution/common/utility_types.ts | 18 ++++++++++++++++++ .../public/common/lib/helpers/index.tsx | 13 ------------- .../rules/description_step/helpers.tsx | 2 +- .../hosts/components/hosts_table/index.tsx | 2 +- .../network/components/users_table/index.tsx | 2 +- .../body/column_headers/header/helpers.ts | 2 +- .../lib/detection_engine/signals/get_filter.ts | 2 +- .../signals/rule_status_service.ts | 2 +- .../lib/events/query.last_event_time.dsl.ts | 2 +- .../server/lib/hosts/query.hosts.dsl.ts | 3 ++- .../server/lib/ip_details/query_users.dsl.ts | 3 ++- .../server/lib/network/query_dns.dsl.ts | 3 ++- .../lib/network/query_top_countries.dsl.ts | 4 ++-- .../server/lib/network/query_top_n_flow.dsl.ts | 3 ++- .../server/lib/tls/query_tls.dsl.ts | 3 ++- .../factory/hosts/dsl/query.hosts.dsl.ts | 4 +--- .../server/utils/build_query/index.ts | 7 ------- 18 files changed, 39 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index edb5dda2ca6da7..a188eb7619e6be 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -40,7 +40,7 @@ export enum Direction { } export interface SortField { - field: string; + field: 'lastSeen' | 'hostName'; direction: Direction; } diff --git a/x-pack/plugins/security_solution/common/utility_types.ts b/x-pack/plugins/security_solution/common/utility_types.ts index 43271dc40ba126..4a7bd02d0442bd 100644 --- a/x-pack/plugins/security_solution/common/utility_types.ts +++ b/x-pack/plugins/security_solution/common/utility_types.ts @@ -26,3 +26,21 @@ export const stringEnum = (enumObj: T, enumName = 'enum') => : runtimeTypes.failure(u, c), (a) => (a as unknown) as string ); + +/** + * Unreachable Assertion helper for scenarios like exhaustive switches. + * For references see: https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript + * This "x" should _always_ be a type of "never" and not change to "unknown" or any other type. See above link or the generic + * concept of exhaustive checks in switch blocks. + * + * Optionally you can avoid the use of this by using early returns and TypeScript will clear your type checking without complaints + * but there are situations and times where this function might still be needed. + * @param x Unreachable field + * @param message Message of error thrown + */ +export const assertUnreachable = ( + x: never, // This should always be a type of "never" + message = 'Unknown Field in switch statement' +): never => { + throw new Error(`${message}: ${x}`); +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/helpers/index.tsx b/x-pack/plugins/security_solution/public/common/lib/helpers/index.tsx index 96b0343efdf720..35f51b3c65f950 100644 --- a/x-pack/plugins/security_solution/public/common/lib/helpers/index.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/helpers/index.tsx @@ -24,19 +24,6 @@ export const asArrayIfExists: WrapArrayIfExitts = (value) => */ export type ValueOf = T[keyof T]; -/** - * Unreachable Assertion helper for scenarios like exhaustive switches - * - * @param x Unreachable field - * @param message Message of error thrown - */ -export const assertUnreachable = ( - x: never, - message = 'Unknown Field in switch statement' -): never => { - throw new Error(`${message}: ${x}`); -}; - /** * Global variables */ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index 600bc999849d1d..3a0a5b04c58740 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -21,6 +21,7 @@ import { isEmpty } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; +import { assertUnreachable } from '../../../../../common/utility_types'; import * as i18nSeverity from '../severity_mapping/translations'; import * as i18nRiskScore from '../risk_score_mapping/translations'; import { Threshold } from '../../../../../common/detection_engine/schemas/common/schemas'; @@ -33,7 +34,6 @@ import * as i18n from './translations'; import { BuildQueryBarDescription, BuildThreatDescription, ListItems } from './types'; import { SeverityBadge } from '../severity_badge'; import ListTreeIcon from './assets/list_tree_icon.svg'; -import { assertUnreachable } from '../../../../common/lib/helpers'; import { AboutStepRiskScore, AboutStepSeverity } from '../../../pages/detection_engine/rules/types'; import { defaultToEmptyTag } from '../../../../common/components/empty_value'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx index d72891fad8f537..8b795fca415125 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx @@ -8,6 +8,7 @@ import React, { useMemo, useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { IIndexPattern } from 'src/plugins/data/public'; +import { assertUnreachable } from '../../../../common/utility_types'; import { Direction, HostFields, @@ -17,7 +18,6 @@ import { HostsSortField, OsFields, } from '../../../graphql/types'; -import { assertUnreachable } from '../../../common/lib/helpers'; import { State } from '../../../common/store'; import { Columns, diff --git a/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx index af9d2b0ffefe30..9a971e0087d122 100644 --- a/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; +import { assertUnreachable } from '../../../../common/utility_types'; import { networkActions, networkModel, networkSelectors } from '../../store'; import { Direction, @@ -26,7 +27,6 @@ import { import { getUsersColumns } from './columns'; import * as i18n from './translations'; -import { assertUnreachable } from '../../../common/lib/helpers'; const tableType = networkModel.IpDetailsTableType.users; interface OwnProps { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts index 6d70795c422d93..609f690903bf28 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { assertUnreachable } from '../../../../../../../common/utility_types'; import { Direction } from '../../../../../../graphql/types'; -import { assertUnreachable } from '../../../../../../common/lib/helpers'; import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model'; import { Sort, SortDirection } from '../../sort'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts index 67dc1d50eefcdb..f77485f39a98d4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { assertUnreachable } from '../../../../common/utility_types'; import { getQueryFilter } from '../../../../common/detection_engine/get_query_filter'; import { LanguageOrUndefined, @@ -15,7 +16,6 @@ import { } from '../../../../common/detection_engine/schemas/common/schemas'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { AlertServices } from '../../../../../alerts/server'; -import { assertUnreachable } from '../../../utils/build_query'; import { PartialFilter } from '../types'; import { BadRequestError } from '../errors/bad_request_error'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts index 0f4b8d1472b3fa..8fdbe282eece5d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { assertUnreachable } from '../../../../common/utility_types'; import { JobStatus } from '../../../../common/detection_engine/schemas/common/schemas'; -import { assertUnreachable } from '../../../utils/build_query'; import { IRuleStatusAttributes } from '../rules/types'; import { getOrCreateRuleStatuses } from './get_or_create_rule_statuses'; import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client'; diff --git a/x-pack/plugins/security_solution/server/lib/events/query.last_event_time.dsl.ts b/x-pack/plugins/security_solution/server/lib/events/query.last_event_time.dsl.ts index 6c443fed3c99d4..02badd3ccee8f3 100644 --- a/x-pack/plugins/security_solution/server/lib/events/query.last_event_time.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/events/query.last_event_time.dsl.ts @@ -6,9 +6,9 @@ import { isEmpty } from 'lodash/fp'; +import { assertUnreachable } from '../../../common/utility_types'; import { LastEventTimeRequestOptions } from './types'; import { LastEventIndexKey } from '../../graphql/types'; -import { assertUnreachable } from '../../utils/build_query'; interface EventIndices { [key: string]: string[]; diff --git a/x-pack/plugins/security_solution/server/lib/hosts/query.hosts.dsl.ts b/x-pack/plugins/security_solution/server/lib/hosts/query.hosts.dsl.ts index 013afd5cd58f5a..dfe45a00e05132 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/query.hosts.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/query.hosts.dsl.ts @@ -6,8 +6,9 @@ import { isEmpty } from 'lodash/fp'; +import { assertUnreachable } from '../../../common/utility_types'; import { Direction, HostsFields, HostsSortField } from '../../graphql/types'; -import { assertUnreachable, createQueryFilterClauses } from '../../utils/build_query'; +import { createQueryFilterClauses } from '../../utils/build_query'; import { HostsRequestOptions } from '.'; diff --git a/x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts b/x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts index 10678dc033eb5d..293a487777fd21 100644 --- a/x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { assertUnreachable } from '../../../common/utility_types'; import { Direction, UsersFields, UsersSortField } from '../../graphql/types'; -import { assertUnreachable, createQueryFilterClauses } from '../../utils/build_query'; +import { createQueryFilterClauses } from '../../utils/build_query'; import { UsersRequestOptions } from './index'; diff --git a/x-pack/plugins/security_solution/server/lib/network/query_dns.dsl.ts b/x-pack/plugins/security_solution/server/lib/network/query_dns.dsl.ts index e7c86e1d3d66be..90781e7b48b4ae 100644 --- a/x-pack/plugins/security_solution/server/lib/network/query_dns.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/network/query_dns.dsl.ts @@ -6,8 +6,9 @@ import { isEmpty } from 'lodash/fp'; +import { assertUnreachable } from '../../../common/utility_types'; import { Direction, NetworkDnsFields, NetworkDnsSortField } from '../../graphql/types'; -import { assertUnreachable, createQueryFilterClauses } from '../../utils/build_query'; +import { createQueryFilterClauses } from '../../utils/build_query'; import { NetworkDnsRequestOptions } from './index'; diff --git a/x-pack/plugins/security_solution/server/lib/network/query_top_countries.dsl.ts b/x-pack/plugins/security_solution/server/lib/network/query_top_countries.dsl.ts index 93ffc35161fa96..be0b8fb64c76aa 100644 --- a/x-pack/plugins/security_solution/server/lib/network/query_top_countries.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/network/query_top_countries.dsl.ts @@ -10,8 +10,8 @@ import { NetworkTopTablesSortField, NetworkTopTablesFields, } from '../../graphql/types'; -import { assertUnreachable, createQueryFilterClauses } from '../../utils/build_query'; - +import { createQueryFilterClauses } from '../../utils/build_query'; +import { assertUnreachable } from '../../../common/utility_types'; import { NetworkTopCountriesRequestOptions } from './index'; const getCountAgg = (flowTarget: FlowTargetSourceDest) => ({ diff --git a/x-pack/plugins/security_solution/server/lib/network/query_top_n_flow.dsl.ts b/x-pack/plugins/security_solution/server/lib/network/query_top_n_flow.dsl.ts index 7cb8b76e7b5244..14a9c5e33aca0f 100644 --- a/x-pack/plugins/security_solution/server/lib/network/query_top_n_flow.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/network/query_top_n_flow.dsl.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { assertUnreachable } from '../../../common/utility_types'; import { Direction, FlowTargetSourceDest, NetworkTopTablesSortField, NetworkTopTablesFields, } from '../../graphql/types'; -import { assertUnreachable, createQueryFilterClauses } from '../../utils/build_query'; +import { createQueryFilterClauses } from '../../utils/build_query'; import { NetworkTopNFlowRequestOptions } from './index'; diff --git a/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts b/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts index 82f16ff58d135e..f6921ddcdf508c 100644 --- a/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createQueryFilterClauses, assertUnreachable } from '../../utils/build_query'; +import { assertUnreachable } from '../../../common/utility_types'; +import { createQueryFilterClauses } from '../../utils/build_query'; import { TlsRequestOptions } from './index'; import { TlsSortField, Direction, TlsFields } from '../../graphql/types'; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.hosts.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.hosts.dsl.ts index 3d72f98f35355c..a9101f54ada55c 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.hosts.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.hosts.dsl.ts @@ -11,7 +11,7 @@ import { HostsRequestOptions, SortField, } from '../../../../../../common/search_strategy/security_solution'; -import { assertUnreachable, createQueryFilterClauses } from '../../../../../utils/build_query'; +import { createQueryFilterClauses } from '../../../../../utils/build_query'; export const buildHostsQuery = ({ defaultIndex, @@ -83,7 +83,5 @@ const getQueryOrder = (sort: SortField): QueryOrder => { return { lastSeen: sort.direction }; case 'hostName': return { _key: sort.direction }; - default: - return assertUnreachable(sort.field); } }; diff --git a/x-pack/plugins/security_solution/server/utils/build_query/index.ts b/x-pack/plugins/security_solution/server/utils/build_query/index.ts index 233ba70968fa18..f0f4ba07ab2ae0 100644 --- a/x-pack/plugins/security_solution/server/utils/build_query/index.ts +++ b/x-pack/plugins/security_solution/server/utils/build_query/index.ts @@ -9,13 +9,6 @@ export * from './filters'; export * from './merge_fields_with_hits'; export * from './calculate_timeseries_interval'; -export const assertUnreachable = ( - x: unknown, - message: string = 'Unknown Field in switch statement' -): never => { - throw new Error(`${message} ${x}`); -}; - export const inspectStringifyObject = (obj: unknown) => { try { return JSON.stringify(obj, null, 2); From 9cafade2b9d4a22a364c921e94a30c54e82e2ee0 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 25 Aug 2020 08:27:15 -0700 Subject: [PATCH 010/216] [remove] production deps which are only used in public code (#75838) Co-authored-by: spalger --- .eslintrc.js | 2 +- package.json | 98 ++++++------ packages/kbn-optimizer/package.json | 10 +- .../plugins/kbn_tp_run_pipeline/package.json | 8 +- .../kbn_sample_panel_action/package.json | 6 +- .../kbn_tp_custom_visualizations/package.json | 6 +- x-pack/package.json | 142 +++++++++--------- x-pack/plugins/apm/e2e/package.json | 6 +- x-pack/plugins/apm/scripts/package.json | 4 +- x-pack/plugins/security_solution/package.json | 4 +- x-pack/plugins/security_solution/yarn.lock | 1 - 11 files changed, 142 insertions(+), 145 deletions(-) delete mode 120000 x-pack/plugins/security_solution/yarn.lock diff --git a/.eslintrc.js b/.eslintrc.js index 8c2a46f80a3a8d..a07d0830907b6d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -517,7 +517,7 @@ module.exports = { 'packages/kbn-interpreter/tasks/**/*.js', 'packages/kbn-interpreter/src/plugin/**/*.js', 'x-pack/{dev-tools,tasks,scripts,test,build_chromium}/**/*.js', - 'x-pack/**/{__tests__,__test__,__jest__,__fixtures__,__mocks__}/**/*.js', + 'x-pack/**/{__tests__,__test__,__jest__,__fixtures__,__mocks__,public}/**/*.js', 'x-pack/**/*.test.js', 'x-pack/test_utils/**/*', 'x-pack/gulpfile.js', diff --git a/package.json b/package.json index 46418e52d85484..84f6f30f064f9e 100644 --- a/package.json +++ b/package.json @@ -123,18 +123,13 @@ "dependencies": { "@babel/core": "^7.11.1", "@babel/register": "^7.10.5", - "@elastic/apm-rum": "^5.5.0", - "@elastic/charts": "19.8.1", "@elastic/datemath": "5.0.3", "@elastic/elasticsearch": "7.9.0-rc.2", - "@elastic/ems-client": "7.9.3", "@elastic/eui": "27.4.1", - "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "^2.5.0", "@elastic/request-crypto": "1.1.4", "@elastic/safer-lodash-set": "0.0.0", - "@elastic/ui-ace": "0.2.3", "@hapi/good-squeeze": "5.2.1", "@hapi/wreck": "^15.0.2", "@kbn/analytics": "1.0.0", @@ -151,35 +146,24 @@ "abortcontroller-polyfill": "^1.4.0", "accept": "3.0.2", "angular": "^1.8.0", - "angular-aria": "^1.8.0", "angular-elastic": "^2.5.1", - "angular-recursion": "^1.0.5", - "angular-route": "^1.8.0", "angular-sanitize": "^1.8.0", - "angular-sortable-view": "^0.0.17", "bluebird": "3.5.5", "boom": "^7.2.0", - "brace": "0.11.1", "chalk": "^2.4.2", "check-disk-space": "^2.1.0", "chokidar": "3.2.1", "color": "1.0.3", "commander": "3.0.2", - "compare-versions": "3.5.1", "core-js": "^3.6.4", - "d3": "3.5.17", - "d3-cloud": "1.2.5", "deep-freeze-strict": "^1.1.1", - "deepmerge": "^4.2.2", "del": "^5.1.0", "elastic-apm-node": "^3.7.0", "elasticsearch": "^16.7.0", - "elasticsearch-browser": "^16.7.0", "execa": "^4.0.2", "expiry-js": "0.1.7", "fast-deep-equal": "^3.1.1", "font-awesome": "4.7.0", - "fp-ts": "^2.3.1", "getos": "^3.1.0", "glob": "^7.1.2", "glob-all": "^3.2.1", @@ -188,67 +172,40 @@ "handlebars": "4.7.6", "hapi": "^17.5.3", "hapi-auth-cookie": "^9.0.0", - "history": "^4.9.0", - "hjson": "3.2.1", "hoek": "^5.0.4", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^5.0.0", - "immer": "^1.5.0", "inert": "^5.1.0", "inline-style": "^2.0.0", "joi": "^13.5.2", - "jquery": "^3.5.0", - "js-levenshtein": "^1.1.6", "js-yaml": "3.13.1", "json-stable-stringify": "^1.0.1", - "json-stringify-pretty-compact": "1.2.0", "json-stringify-safe": "5.0.1", - "leaflet": "1.5.1", - "leaflet-draw": "0.4.14", - "leaflet-responsive-popup": "0.6.4", - "leaflet-vega": "^0.8.6", - "leaflet.heat": "0.2.0", - "less": "npm:@elastic/less@2.7.3-kibana", "lodash": "^4.17.20", "lru-cache": "4.1.5", - "markdown-it": "^10.0.0", "minimatch": "^3.0.4", "moment": "^2.24.0", "moment-timezone": "^0.5.27", - "monaco-editor": "~0.17.0", "mustache": "2.3.2", - "ngreact": "0.5.1", "node-fetch": "1.7.3", "node-forge": "^0.9.1", "opn": "^5.5.0", "oppsy": "^2.0.0", "pegjs": "0.10.0", - "prop-types": "15.6.0", "proxy-from-env": "1.0.0", "query-string": "5.1.1", "re2": "^1.15.4", "react": "^16.12.0", "react-color": "^2.13.8", "react-dom": "^16.12.0", - "react-grid-layout": "^0.16.2", "react-input-range": "^1.3.0", - "react-markdown": "^4.3.1", - "react-monaco-editor": "~0.27.0", - "react-redux": "^7.2.0", - "react-resize-detector": "^4.2.0", "react-router": "^5.2.0", - "react-router-dom": "^5.2.0", - "react-sizeme": "^2.3.6", "react-use": "^13.27.0", - "reactcss": "1.2.3", - "redux": "^4.0.5", "redux-actions": "^2.6.5", "redux-thunk": "^2.3.0", "regenerator-runtime": "^0.13.3", "request": "^2.88.0", "require-in-the-middle": "^5.0.2", - "reselect": "^4.0.0", - "resize-observer-polyfill": "^1.5.0", "rison-node": "1.0.2", "rxjs": "^6.5.5", "seedrandom": "^3.0.5", @@ -258,15 +215,9 @@ "tar": "4.4.13", "tinygradient": "0.4.3", "tinymath": "1.2.1", - "topojson-client": "3.0.0", "tslib": "^2.0.0", "type-detect": "^4.0.8", - "ui-select": "0.19.8", "uuid": "3.3.2", - "vega": "^5.13.0", - "vega-lite": "^4.13.1", - "vega-schema-url-parser": "^1.1.0", - "vega-tooltip": "^0.12.0", "vision": "^5.3.3", "whatwg-fetch": "^3.0.0", "yauzl": "2.10.0" @@ -274,10 +225,15 @@ "devDependencies": { "@babel/parser": "^7.11.2", "@babel/types": "^7.11.0", + "@elastic/apm-rum": "^5.5.0", + "@elastic/charts": "19.8.1", + "@elastic/ems-client": "7.9.3", "@elastic/eslint-config-kibana": "0.15.0", "@elastic/eslint-plugin-eui": "0.0.2", + "@elastic/filesaver": "1.1.2", "@elastic/github-checks-reporter": "0.0.20b3", "@elastic/makelogs": "^6.0.0", + "@elastic/ui-ace": "0.2.3", "@kbn/dev-utils": "1.0.0", "@kbn/es": "1.0.0", "@kbn/es-archiver": "1.0.0", @@ -383,20 +339,30 @@ "@types/zen-observable": "^0.8.0", "@typescript-eslint/eslint-plugin": "^3.7.1", "@typescript-eslint/parser": "^3.7.1", + "angular-aria": "^1.8.0", "angular-mocks": "^1.7.9", + "angular-recursion": "^1.0.5", + "angular-route": "^1.8.0", + "angular-sortable-view": "^0.0.17", "archiver": "^3.1.1", "axe-core": "^3.4.1", "babel-eslint": "^10.0.3", "babel-jest": "^25.5.1", "babel-plugin-istanbul": "^6.0.0", "backport": "5.5.1", + "brace": "0.11.1", "chai": "3.5.0", "chance": "1.0.18", "cheerio": "0.22.0", "chromedriver": "^84.0.0", "classnames": "2.2.6", + "compare-versions": "3.5.1", + "d3": "3.5.17", + "d3-cloud": "1.2.5", "dedent": "^0.7.0", + "deepmerge": "^4.2.2", "delete-empty": "^2.0.0", + "elasticsearch-browser": "^16.7.0", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", "enzyme-adapter-utils": "^1.13.0", @@ -421,6 +387,7 @@ "exit-hook": "^2.2.0", "faker": "1.1.0", "fetch-mock": "^7.3.9", + "fp-ts": "^2.3.1", "geckodriver": "^1.20.0", "getopts": "^2.2.4", "grunt": "1.0.4", @@ -432,7 +399,10 @@ "gulp-babel": "^8.0.0", "gulp-sourcemaps": "2.6.5", "has-ansi": "^3.0.0", + "history": "^4.9.0", + "hjson": "3.2.1", "iedriver": "^3.14.2", + "immer": "^1.5.0", "intl-messageformat-parser": "^1.4.0", "jest": "^25.5.4", "jest-canvas-mock": "^2.2.0", @@ -441,18 +411,30 @@ "jest-environment-jsdom-thirteen": "^1.0.1", "jest-raw-loader": "^1.0.1", "jimp": "^0.14.0", + "jquery": "^3.5.0", + "js-levenshtein": "^1.1.6", + "json-stringify-pretty-compact": "1.2.0", "json5": "^1.0.1", + "leaflet": "1.5.1", + "leaflet-draw": "0.4.14", + "leaflet-responsive-popup": "0.6.4", + "leaflet-vega": "^0.8.6", + "leaflet.heat": "0.2.0", + "less": "npm:@elastic/less@2.7.3-kibana", "license-checker": "^16.0.0", "listr": "^0.14.1", "load-grunt-config": "^3.0.1", "load-json-file": "^6.2.0", + "markdown-it": "^10.0.0", "mocha": "^7.1.1", "mock-fs": "^4.12.0", "mock-http-server": "1.3.0", + "monaco-editor": "~0.17.0", "ms-chromium-edge-driver": "^0.2.3", "multistream": "^2.1.1", "murmurhash3js": "3.0.1", "mutation-observer": "^1.0.3", + "ngreact": "0.5.1", "nock": "12.0.3", "normalize-path": "^3.0.0", "nyc": "^15.0.1", @@ -461,9 +443,21 @@ "pngjs": "^3.4.0", "postcss": "^7.0.32", "prettier": "^2.0.5", + "prop-types": "15.6.0", "proxyquire": "1.8.0", + "react-grid-layout": "^0.16.2", + "react-markdown": "^4.3.1", + "react-monaco-editor": "~0.27.0", "react-popper-tooltip": "^2.10.1", + "react-redux": "^7.2.0", + "react-resize-detector": "^4.2.0", + "react-router-dom": "^5.2.0", + "react-sizeme": "^2.3.6", + "reactcss": "1.2.3", + "redux": "^4.0.5", "regenerate": "^1.4.0", + "reselect": "^4.0.0", + "resize-observer-polyfill": "^1.5.0", "sass-lint": "^1.12.1", "selenium-webdriver": "^4.0.0-alpha.7", "simple-git": "1.116.0", @@ -472,9 +466,15 @@ "supertest": "^3.1.0", "supertest-as-promised": "^4.0.2", "tape": "^4.13.0", + "topojson-client": "3.0.0", "tree-kill": "^1.2.2", "typescript": "3.9.5", "typings-tester": "^0.3.2", + "ui-select": "0.19.8", + "vega": "^5.13.0", + "vega-lite": "^4.13.1", + "vega-schema-url-parser": "^1.1.0", + "vega-tooltip": "^0.12.0", "vinyl-fs": "^3.0.3", "xml2js": "^0.4.22", "xmlbuilder": "13.0.2", diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index 740555fd87897d..b80d1365659dda 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -14,10 +14,6 @@ "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", - "@types/compression-webpack-plugin": "^2.0.2", - "@types/loader-utils": "^1.1.3", - "@types/watchpack": "^1.1.5", - "@types/webpack": "^4.41.3", "autoprefixer": "^9.7.4", "babel-loader": "^8.0.6", "clean-webpack-plugin": "^3.0.0", @@ -46,5 +42,11 @@ "watchpack": "^1.6.0", "webpack": "^4.41.5", "webpack-merge": "^4.2.2" + }, + "devDependencies": { + "@types/compression-webpack-plugin": "^2.0.2", + "@types/loader-utils": "^1.1.3", + "@types/watchpack": "^1.1.5", + "@types/webpack": "^4.41.3" } } diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index c5e080e3c81758..e1ee1153a28ac5 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -7,17 +7,15 @@ "templateVersion": "1.0.0" }, "license": "Apache-2.0", - "dependencies": { - "@elastic/eui": "27.4.1", - "react": "^16.12.0", - "react-dom": "^16.12.0" - }, "scripts": { "kbn": "node ../../../../scripts/kbn.js", "build": "rm -rf './target' && tsc" }, "devDependencies": { + "@elastic/eui": "27.4.1", "@kbn/plugin-helpers": "9.0.2", + "react": "^16.12.0", + "react-dom": "^16.12.0", "typescript": "3.9.5" } } diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json index dac901f4963048..b3cef400089b03 100644 --- a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json @@ -7,15 +7,13 @@ "templateVersion": "1.0.0" }, "license": "Apache-2.0", - "dependencies": { - "@elastic/eui": "27.4.1", - "react": "^16.12.0" - }, "scripts": { "kbn": "node ../../../../scripts/kbn.js", "build": "rm -rf './target' && tsc" }, "devDependencies": { + "@elastic/eui": "27.4.1", + "react": "^16.12.0", "typescript": "3.9.5" } } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index b7c494807672e3..9250a4499662f4 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -7,16 +7,14 @@ "templateVersion": "1.0.0" }, "license": "Apache-2.0", - "dependencies": { - "@elastic/eui": "27.4.1", - "react": "^16.12.0" - }, "scripts": { "kbn": "node ../../../../scripts/kbn.js", "build": "rm -rf './target' && tsc" }, "devDependencies": { + "@elastic/eui": "27.4.1", "@kbn/plugin-helpers": "9.0.2", + "react": "^16.12.0", "typescript": "3.9.5" } } diff --git a/x-pack/package.json b/x-pack/package.json index 992a186d41d784..247130f4895bb2 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -30,6 +30,8 @@ }, "devDependencies": { "@cypress/webpack-preprocessor": "^4.1.0", + "@elastic/apm-rum-react": "^1.2.3", + "@elastic/maki": "6.3.0", "@kbn/dev-utils": "1.0.0", "@kbn/es": "1.0.0", "@kbn/expect": "1.0.0", @@ -37,6 +39,10 @@ "@kbn/storybook": "1.0.0", "@kbn/test": "1.0.0", "@kbn/utility-types": "1.0.0", + "@mapbox/geojson-rewind": "^0.4.1", + "@mapbox/mapbox-gl-draw": "^1.2.0", + "@mapbox/mapbox-gl-rtl-text": "^0.2.3", + "@scant/router": "^0.1.0", "@storybook/addon-actions": "^5.3.19", "@storybook/addon-console": "^1.2.1", "@storybook/addon-info": "^5.3.19", @@ -47,6 +53,11 @@ "@testing-library/jest-dom": "^5.8.0", "@testing-library/react": "^9.3.2", "@testing-library/react-hooks": "^3.2.1", + "@turf/bbox": "6.0.1", + "@turf/bbox-polygon": "6.0.1", + "@turf/boolean-contains": "6.0.1", + "@turf/distance": "6.0.1", + "@turf/helpers": "6.0.1", "@types/angular": "^1.6.56", "@types/archiver": "^3.1.0", "@types/base64-js": "^1.2.5", @@ -75,6 +86,7 @@ "@types/history": "^4.7.3", "@types/hoist-non-react-statics": "^3.3.1", "@types/http-proxy": "^1.17.4", + "@types/http-proxy-agent": "^2.0.2", "@types/jest": "^25.2.3", "@types/jest-specific-snapshot": "^0.5.4", "@types/joi": "^13.4.2", @@ -124,6 +136,11 @@ "@types/xml2js": "^0.4.5", "@welldone-software/why-did-you-render": "^4.0.0", "abab": "^1.0.4", + "angular": "^1.8.0", + "angular-sanitize": "1.8.0", + "apollo-link": "^1.2.3", + "apollo-link-error": "^1.1.7", + "apollo-link-state": "^0.4.1", "autoprefixer": "^9.7.4", "axios": "^0.19.0", "babel-jest": "^25.5.1", @@ -131,14 +148,22 @@ "babel-plugin-require-context-hook": "npm:babel-plugin-require-context-hook-babel7@1.0.0", "base64-js": "^1.3.1", "base64url": "^3.0.1", + "brace": "0.11.1", + "broadcast-channel": "^3.0.3", "canvas": "^2.6.1", "chalk": "^4.1.0", "chance": "1.0.18", "cheerio": "0.22.0", "commander": "3.0.2", + "constate": "^1.3.2", + "copy-to-clipboard": "^3.0.8", "copy-webpack-plugin": "^6.0.2", + "cronstrue": "^1.51.0", "cypress": "4.11.0", "cypress-multi-reporters": "^1.2.3", + "d3": "3.5.17", + "d3-scale": "1.0.7", + "dragselect": "1.13.1", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", "enzyme-adapter-utils": "^1.13.0", @@ -146,6 +171,8 @@ "execa": "^4.0.2", "fancy-log": "^1.3.2", "fetch-mock": "^7.3.9", + "file-saver": "^1.3.8", + "formsy-react": "^1.1.5", "graphql-code-generator": "^0.18.2", "graphql-codegen-add": "^0.18.2", "graphql-codegen-introspection": "^0.18.2", @@ -155,16 +182,27 @@ "graphql-codegen-typescript-server": "^0.18.2", "gulp": "4.0.2", "hapi": "^17.5.3", + "he": "^1.2.0", + "history-extra": "^5.0.1", "hoist-non-react-statics": "^3.3.2", + "i18n-iso-countries": "^4.3.1", + "icalendar": "0.7.1", "jest": "^25.5.4", "jest-circus": "^25.5.4", "jest-cli": "^25.5.4", "jest-styled-components": "^7.0.2", + "js-search": "^1.4.3", "jsdom": "13.1.0", "jsondiffpatch": "0.4.1", + "jsts": "^1.6.2", + "kea": "^2.0.1", "loader-utils": "^1.2.3", + "lz-string": "^1.4.4", "madge": "3.4.4", + "mapbox-gl": "^1.10.0", + "mapbox-gl-draw-rectangle-mode": "^1.0.4", "marge": "^1.0.1", + "memoize-one": "^5.0.0", "mini-css-extract-plugin": "0.8.0", "mocha": "^7.1.1", "mocha-junit-reporter": "^1.23.1", @@ -174,14 +212,39 @@ "mutation-observer": "^1.0.3", "node-fetch": "^2.6.0", "null-loader": "^3.0.0", + "oboe": "^2.1.4", "pixelmatch": "^5.1.0", + "pluralize": "3.1.0", + "polished": "^1.9.2", "postcss": "^7.0.32", "postcss-loader": "^3.0.0", "postcss-prefix-selector": "^1.7.2", "proxyquire": "1.8.0", + "re-resizable": "^6.1.1", + "react-apollo": "^2.1.4", + "react-beautiful-dnd": "^12.2.0", "react-docgen-typescript-loader": "^3.1.1", + "react-dropzone": "^4.2.9", + "react-fast-compare": "^2.0.4", "react-is": "^16.8.0", + "react-markdown": "^4.3.1", + "react-reverse-portal": "^1.0.4", + "react-router": "^5.2.0", + "react-shortcuts": "^2.0.0", + "react-sticky": "^6.0.3", + "react-syntax-highlighter": "^5.7.0", "react-test-renderer": "^16.12.0", + "react-tiny-virtual-list": "^2.2.0", + "react-use": "^13.27.0", + "react-virtualized": "^9.21.2", + "react-vis": "^1.8.1", + "react-visibility-sensor": "^5.1.1", + "reduce-reducers": "^1.0.4", + "redux-actions": "^2.6.5", + "redux-saga": "^1.1.3", + "redux-thunks": "^1.0.0", + "reselect": "^4.0.0", + "resize-observer-polyfill": "^1.5.0", "rxjs-marbles": "^5.0.6", "sass-loader": "^8.0.2", "sass-resources-loader": "^2.0.1", @@ -190,10 +253,18 @@ "string-replace-loader": "^2.2.0", "supertest": "^3.1.0", "supertest-as-promised": "^4.0.2", + "suricata-sid-db": "^1.0.2", + "tinycolor2": "1.4.1", "tmp": "0.1.0", + "topojson-client": "3.0.0", "tree-kill": "^1.2.2", "ts-loader": "^6.0.4", "typescript": "3.9.5", + "typescript-fsa": "^3.0.0", + "typescript-fsa-reducers": "^1.2.1", + "unstated": "^2.1.1", + "use-resize-observer": "^6.0.0", + "venn.js": "0.2.20", "vinyl-fs": "^3.0.3", "whatwg-fetch": "^3.0.0", "xml-crypto": "^1.4.0", @@ -203,12 +274,10 @@ "@babel/core": "^7.11.1", "@babel/register": "^7.10.5", "@babel/runtime": "^7.11.2", - "@elastic/apm-rum-react": "^1.2.3", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.9.3", "@elastic/eui": "27.4.1", "@elastic/filesaver": "1.1.2", - "@elastic/maki": "6.3.0", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.0", "@elastic/safer-lodash-set": "0.0.0", @@ -217,57 +286,32 @@ "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/ui-framework": "1.0.0", - "@mapbox/geojson-rewind": "^0.4.1", - "@mapbox/mapbox-gl-draw": "^1.2.0", - "@mapbox/mapbox-gl-rtl-text": "^0.2.3", - "@scant/router": "^0.1.0", "@slack/webhook": "^5.0.0", - "@turf/bbox": "6.0.1", - "@turf/bbox-polygon": "6.0.1", - "@turf/boolean-contains": "6.0.1", "@turf/circle": "6.0.1", - "@turf/distance": "6.0.1", - "@turf/helpers": "6.0.1", - "@types/http-proxy-agent": "^2.0.2", - "angular": "^1.8.0", "angular-resource": "1.8.0", - "angular-sanitize": "1.8.0", "angular-ui-ace": "0.2.3", "apollo-cache-inmemory": "1.6.2", "apollo-client": "^2.3.8", - "apollo-link": "^1.2.3", - "apollo-link-error": "^1.1.7", "apollo-link-http": "^1.5.16", "apollo-link-schema": "^1.1.0", - "apollo-link-state": "^0.4.1", "apollo-server-errors": "^2.0.2", "apollo-server-hapi": "^1.3.6", "archiver": "3.1.1", "axios": "^0.19.0", "bluebird": "3.5.5", "boom": "^7.2.0", - "brace": "0.11.1", - "broadcast-channel": "^3.0.3", "chroma-js": "^1.4.1", "classnames": "2.2.6", "concat-stream": "1.6.2", - "constate": "^1.3.2", "content-disposition": "0.5.3", - "copy-to-clipboard": "^3.0.8", - "cronstrue": "^1.51.0", "cytoscape": "^3.10.0", - "d3": "3.5.17", "d3-array": "1.2.4", - "d3-scale": "1.0.7", "dedent": "^0.7.0", "del": "^5.1.0", - "dragselect": "1.13.1", "elasticsearch": "^16.7.0", "extract-zip": "^1.7.0", - "file-saver": "^1.3.8", "file-type": "^10.9.0", "font-awesome": "4.7.0", - "formsy-react": "^1.1.5", "fp-ts": "^2.3.1", "get-port": "^5.0.0", "getos": "^3.1.0", @@ -280,11 +324,7 @@ "graphql-tools": "^3.0.2", "h2o2": "^8.1.2", "handlebars": "4.7.6", - "he": "^1.2.0", "history": "4.9.0", - "history-extra": "^5.0.1", - "i18n-iso-countries": "^4.3.1", - "icalendar": "0.7.1", "idx": "^2.5.6", "immer": "^1.5.0", "inline-style": "^2.0.0", @@ -293,18 +333,11 @@ "isbinaryfile": "4.0.2", "joi": "^13.5.2", "jquery": "^3.5.0", - "js-search": "^1.4.3", "js-yaml": "3.13.1", "json-stable-stringify": "^1.0.1", "jsonwebtoken": "^8.5.1", - "jsts": "^1.6.2", - "kea": "^2.0.1", "lodash": "^4.17.15", - "lz-string": "^1.4.4", - "mapbox-gl": "^1.10.0", - "mapbox-gl-draw-rectangle-mode": "^1.0.4", "markdown-it": "^10.0.0", - "memoize-one": "^5.0.0", "mime": "^2.4.4", "moment": "^2.24.0", "moment-duration-format": "^2.3.2", @@ -315,54 +348,29 @@ "nodemailer": "^4.7.0", "object-hash": "^1.3.1", "object-path-immutable": "^3.1.1", - "oboe": "^2.1.4", "oppsy": "^2.0.0", "p-retry": "^4.2.0", "papaparse": "^5.2.0", "pdfmake": "^0.1.65", - "pluralize": "3.1.0", "pngjs": "3.4.0", - "polished": "^1.9.2", "prop-types": "^15.6.0", "proper-lockfile": "^3.2.0", "puid": "1.0.7", "puppeteer-core": "^1.19.0", "query-string": "5.1.1", "raw-loader": "3.1.0", - "re-resizable": "^6.1.1", "react": "^16.12.0", - "react-apollo": "^2.1.4", - "react-beautiful-dnd": "^12.2.0", "react-datetime": "^2.14.0", "react-dom": "^16.12.0", - "react-dropzone": "^4.2.9", - "react-fast-compare": "^2.0.4", - "react-markdown": "^4.3.1", "react-moment-proptypes": "^1.7.0", "react-portal": "^3.2.0", "react-redux": "^7.2.0", - "react-reverse-portal": "^1.0.4", - "react-router": "^5.2.0", "react-router-dom": "^5.2.0", - "react-shortcuts": "^2.0.0", - "react-sticky": "^6.0.3", - "react-syntax-highlighter": "^5.7.0", - "react-tiny-virtual-list": "^2.2.0", - "react-use": "^13.27.0", - "react-virtualized": "^9.21.2", - "react-vis": "^1.8.1", - "react-visibility-sensor": "^5.1.1", "recompose": "^0.26.0", - "reduce-reducers": "^1.0.4", "redux": "^4.0.5", - "redux-actions": "^2.6.5", "redux-observable": "^1.2.0", - "redux-saga": "^1.1.3", "redux-thunk": "^2.3.0", - "redux-thunks": "^1.0.0", "request": "^2.88.0", - "reselect": "^4.0.0", - "resize-observer-polyfill": "^1.5.0", "rison-node": "0.3.1", "rxjs": "^6.5.5", "semver": "5.7.0", @@ -371,18 +379,10 @@ "stats-lite": "^2.2.0", "style-it": "^2.1.3", "styled-components": "^5.1.0", - "suricata-sid-db": "^1.0.2", - "tinycolor2": "1.4.1", "tinymath": "1.2.1", - "topojson-client": "3.0.0", "tslib": "^2.0.0", - "typescript-fsa": "^3.0.0", - "typescript-fsa-reducers": "^1.2.1", "ui-select": "0.19.8", - "unstated": "^2.1.1", - "use-resize-observer": "^6.0.0", "uuid": "3.3.2", - "venn.js": "0.2.20", "vscode-languageserver": "^5.2.1", "webpack": "^4.41.5", "wellknown": "^0.5.0", diff --git a/x-pack/plugins/apm/e2e/package.json b/x-pack/plugins/apm/e2e/package.json index 5101e64235c626..d113b465fdf2f5 100644 --- a/x-pack/plugins/apm/e2e/package.json +++ b/x-pack/plugins/apm/e2e/package.json @@ -10,8 +10,6 @@ "dependencies": { "@cypress/snapshot": "^2.1.3", "@cypress/webpack-preprocessor": "^5.4.1", - "@types/cypress-cucumber-preprocessor": "^1.14.1", - "@types/node": "^14.0.14", "axios": "^0.19.2", "cypress": "^4.9.0", "cypress-cucumber-preprocessor": "^2.5.2", @@ -23,5 +21,9 @@ "wait-on": "^5.0.1", "webpack": "^4.43.0", "yargs": "^15.4.0" + }, + "devDependencies": { + "@types/cypress-cucumber-preprocessor": "^1.14.1", + "@types/node": "^14.0.14" } } diff --git a/x-pack/plugins/apm/scripts/package.json b/x-pack/plugins/apm/scripts/package.json index 4d0906514b5e17..d3e2d42f972a9b 100644 --- a/x-pack/plugins/apm/scripts/package.json +++ b/x-pack/plugins/apm/scripts/package.json @@ -6,8 +6,10 @@ "dependencies": { "@elastic/elasticsearch": "7.9.0-rc.1", "@octokit/rest": "^16.35.0", - "@types/console-stamp": "^0.2.32", "console-stamp": "^0.2.9", "hdr-histogram-js": "^1.2.0" + }, + "devDependencies": { + "@types/console-stamp": "^0.2.32" } } diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 703ef6584f1647..687099541b3d28 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -13,9 +13,7 @@ "test:generate": "ts-node --project scripts/endpoint/cli_tsconfig.json scripts/endpoint/resolver_generator.ts" }, "devDependencies": { - "@types/md5": "^2.2.0" - }, - "dependencies": { + "@types/md5": "^2.2.0", "@types/rbush": "^3.0.0", "@types/seedrandom": ">=2.0.0 <4.0.0", "querystring": "^0.2.0", diff --git a/x-pack/plugins/security_solution/yarn.lock b/x-pack/plugins/security_solution/yarn.lock deleted file mode 120000 index 6e09764ec763b0..00000000000000 --- a/x-pack/plugins/security_solution/yarn.lock +++ /dev/null @@ -1 +0,0 @@ -../../../yarn.lock \ No newline at end of file From c634208e4f2426b9762b46d88073121529df23d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Tue, 25 Aug 2020 18:59:47 +0200 Subject: [PATCH 011/216] [ILM] TS conversion of Edit policy page (#75148) * [ILM] TS conversion of Edit policy page * [ILM] Deleted some constants * [ILM] Fixed imports * [ILM] Fixed imports * [ILM] Clean up * [ILM] Clean up * [ILM] Fixed ui_metric jest test * [ILM] Fixed ui_metric jest test * [ILM] Fix review suggestions Co-authored-by: Elastic Machine --- .../edit_policy/constants.ts | 10 +- .../edit_policy/edit_policy.helpers.tsx | 2 - .../edit_policy/edit_policy.test.ts | 8 +- .../__jest__/components/edit_policy.test.js | 28 +- .../public/application/app.tsx | 2 +- .../public/application/constants/index.ts | 99 +---- .../public/application/constants/policy.ts | 60 +++ .../edit_policy/components/form_errors.tsx | 12 +- .../edit_policy/components/min_age_input.tsx | 58 ++- .../components/node_allocation.tsx | 41 +- .../components/policy_json_flyout.tsx | 15 +- .../components/set_priority_input.tsx | 31 +- .../edit_policy/edit_policy.container.js | 58 --- .../edit_policy/edit_policy.container.tsx | 83 ++++ .../sections/edit_policy/edit_policy.js | 390 ------------------ .../sections/edit_policy/edit_policy.tsx | 383 +++++++++++++++++ .../sections/edit_policy/index.d.ts | 7 - .../edit_policy/{index.js => index.ts} | 0 .../edit_policy/phases/cold_phase.tsx | 67 ++- .../edit_policy/phases/delete_phase.tsx | 34 +- .../sections/edit_policy/phases/hot_phase.tsx | 262 ++++++------ .../edit_policy/phases/warm_phase.tsx | 138 +++---- .../components/policy_table/policy_table.js | 2 +- .../public/application/services/api.ts | 13 +- .../application/services/find_errors.js | 24 -- .../services/policies/cold_phase.ts | 159 +++++++ .../services/policies/delete_phase.ts | 88 ++++ .../services/policies/hot_phase.ts | 155 +++++++ .../policies/policy_save.ts} | 30 +- .../services/policies/policy_serialization.ts | 104 +++++ .../services/policies/policy_validation.ts | 191 +++++++++ .../application/services/policies/types.ts | 140 +++++++ .../services/policies/warm_phase.ts | 219 ++++++++++ .../{ui_metric.test.js => ui_metric.test.ts} | 19 +- .../public/application/services/ui_metric.ts | 64 +-- .../application/store/actions/general.js | 11 - .../public/application/store/actions/index.js | 3 - .../public/application/store/actions/nodes.js | 8 - .../application/store/actions/policies.js | 7 - .../application/store/defaults/cold_phase.js | 30 -- .../store/defaults/delete_phase.js | 23 -- .../application/store/defaults/hot_phase.js | 35 -- .../application/store/defaults/index.d.ts | 10 - .../application/store/defaults/index.js | 10 - .../application/store/defaults/warm_phase.js | 39 -- .../application/store/reducers/general.js | 38 -- .../application/store/reducers/index.js | 4 - .../application/store/reducers/nodes.js | 50 --- .../application/store/reducers/policies.js | 97 ----- .../application/store/selectors/general.js | 9 - .../application/store/selectors/index.js | 3 - .../application/store/selectors/lifecycle.js | 287 ------------- .../application/store/selectors/nodes.js | 12 - .../application/store/selectors/policies.js | 291 +------------ 54 files changed, 1996 insertions(+), 1967 deletions(-) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.d.ts rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{index.js => index.ts} (100%) delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/find_errors.js create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts rename x-pack/plugins/index_lifecycle_management/public/application/{store/actions/lifecycle.js => services/policies/policy_save.ts} (58%) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts rename x-pack/plugins/index_lifecycle_management/public/application/services/{ui_metric.test.js => ui_metric.test.ts} (75%) delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/actions/general.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/defaults/cold_phase.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/defaults/delete_phase.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/defaults/hot_phase.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.d.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/defaults/warm_phase.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/reducers/general.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/reducers/nodes.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/selectors/general.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/selectors/nodes.js diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts index e5037a6477aca4..acf642f250a7b7 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts @@ -4,21 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PolicyFromES } from '../../../public/application/services/policies/types'; + export const POLICY_NAME = 'my_policy'; export const SNAPSHOT_POLICY_NAME = 'my_snapshot_policy'; export const NEW_SNAPSHOT_POLICY_NAME = 'my_new_snapshot_policy'; -export const DELETE_PHASE_POLICY = { +export const DELETE_PHASE_POLICY: PolicyFromES = { version: 1, - modified_date: Date.now(), + modified_date: Date.now().toString(), policy: { phases: { hot: { min_age: '0ms', actions: { - set_priority: { - priority: null, - }, rollover: { max_size: '50gb', }, @@ -36,6 +35,7 @@ export const DELETE_PHASE_POLICY = { }, }, }, + name: POLICY_NAME, }, name: POLICY_NAME, }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index ebe1c12e2a0797..6365bb8caa9631 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -13,7 +13,6 @@ import { POLICY_NAME } from './constants'; import { TestSubjects } from '../helpers'; import { EditPolicy } from '../../../public/application/sections/edit_policy'; -import { indexLifecycleManagementStore } from '../../../public/application/store'; jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); @@ -35,7 +34,6 @@ jest.mock('@elastic/eui', () => { }); const testBedConfig: TestBedConfig = { - store: () => indexLifecycleManagementStore(), memoryRouter: { initialEntries: [`/policies/edit/${POLICY_NAME}`], componentRoutePath: `/policies/edit/:policyName`, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index 06829e6ef6f1e3..36feb3f6203c80 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -40,8 +40,8 @@ describe('', () => { test('wait for snapshot policy field should correctly display snapshot policy name', () => { expect(testBed.find('snapshotPolicyCombobox').prop('data-currentvalue')).toEqual([ { - label: DELETE_PHASE_POLICY.policy.phases.delete.actions.wait_for_snapshot.policy, - value: DELETE_PHASE_POLICY.policy.phases.delete.actions.wait_for_snapshot.policy, + label: DELETE_PHASE_POLICY.policy.phases.delete?.actions.wait_for_snapshot?.policy, + value: DELETE_PHASE_POLICY.policy.phases.delete?.actions.wait_for_snapshot?.policy, }, ]); }); @@ -59,7 +59,7 @@ describe('', () => { delete: { ...DELETE_PHASE_POLICY.policy.phases.delete, actions: { - ...DELETE_PHASE_POLICY.policy.phases.delete.actions, + ...DELETE_PHASE_POLICY.policy.phases.delete?.actions, wait_for_snapshot: { policy: NEW_SNAPSHOT_POLICY_NAME, }, @@ -96,7 +96,7 @@ describe('', () => { delete: { ...DELETE_PHASE_POLICY.policy.phases.delete, actions: { - ...DELETE_PHASE_POLICY.policy.phases.delete.actions, + ...DELETE_PHASE_POLICY.policy.phases.delete?.actions, }, }, }, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js index 4fe3d5c66696e9..81c30579cd4dde 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js @@ -7,7 +7,6 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import moment from 'moment-timezone'; -import { Provider } from 'react-redux'; // axios has a $http like interface so using it to simulate $http import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; @@ -21,9 +20,7 @@ import { import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks'; import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; -import { fetchedPolicies } from '../../public/application/store/actions'; -import { indexLifecycleManagementStore } from '../../public/application/store'; -import { EditPolicy } from '../../public/application/sections/edit_policy'; +import { EditPolicy } from '../../public/application/sections/edit_policy/edit_policy'; import { init as initHttp } from '../../public/application/services/http'; import { init as initUiMetric } from '../../public/application/services/ui_metric'; import { init as initNotification } from '../../public/application/services/notification'; @@ -40,7 +37,7 @@ import { policyNameMustBeDifferentErrorMessage, policyNameAlreadyUsedErrorMessage, maximumDocumentsRequiredMessage, -} from '../../public/application/store/selectors'; +} from '../../public/application/services/policies/policy_validation'; initHttp(axios.create({ adapter: axiosXhrAdapter })); initUiMetric(usageCollectionPluginMock.createSetupContract()); @@ -51,7 +48,6 @@ initNotification( let server; let httpRequestsMockHelpers; -let store; const policy = { phases: { hot: { @@ -128,13 +124,14 @@ const save = (rendered) => { }; describe('edit policy', () => { beforeEach(() => { - store = indexLifecycleManagementStore(); component = ( - - {} }} getUrlForApp={() => {}} /> - + {} }} + getUrlForApp={() => {}} + policies={policies} + policyName={''} + /> ); - store.dispatch(fetchedPolicies(policies)); ({ server, httpRequestsMockHelpers } = initHttpRequests()); httpRequestsMockHelpers.setPoliciesResponse(policies); @@ -162,9 +159,12 @@ describe('edit policy', () => { }); test('should show error when trying to save as new policy but using the same name', () => { component = ( - - {}} /> - + {}} + history={{ push: () => {} }} + /> ); const rendered = mountWithIntl(component); findTestSubject(rendered, 'saveAsNewSwitch').simulate('click'); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/app.tsx b/x-pack/plugins/index_lifecycle_management/public/application/app.tsx index 14b0e72317c666..f7f8b30324bcad 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/app.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/app.tsx @@ -9,7 +9,7 @@ import { Router, Switch, Route, Redirect } from 'react-router-dom'; import { ScopedHistory, ApplicationStart } from 'kibana/public'; import { METRIC_TYPE } from '@kbn/analytics'; -import { UIM_APP_LOAD } from './constants'; +import { UIM_APP_LOAD } from './constants/ui_metric'; import { EditPolicy } from './sections/edit_policy'; import { PolicyTable } from './sections/policy_table'; import { trackUiMetric } from './services/ui_metric'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/index.ts index 6319fc0d68543f..61c197f2ba1496 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/constants/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/index.ts @@ -4,102 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './policy'; export * from './ui_metric'; - -export const SET_PHASE_DATA: string = 'SET_PHASE_DATA'; -export const SET_SELECTED_NODE_ATTRS: string = 'SET_SELECTED_NODE_ATTRS'; -export const PHASE_HOT: string = 'hot'; -export const PHASE_WARM: string = 'warm'; -export const PHASE_COLD: string = 'cold'; -export const PHASE_DELETE: string = 'delete'; - -export const PHASE_ENABLED: string = 'phaseEnabled'; - -export const PHASE_ROLLOVER_ENABLED: string = 'rolloverEnabled'; -export const WARM_PHASE_ON_ROLLOVER: string = 'warmPhaseOnRollover'; -export const PHASE_ROLLOVER_ALIAS: string = 'selectedAlias'; -export const PHASE_ROLLOVER_MAX_AGE: string = 'selectedMaxAge'; -export const PHASE_ROLLOVER_MAX_AGE_UNITS: string = 'selectedMaxAgeUnits'; -export const PHASE_ROLLOVER_MAX_SIZE_STORED: string = 'selectedMaxSizeStored'; -export const PHASE_ROLLOVER_MAX_DOCUMENTS: string = 'selectedMaxDocuments'; -export const PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS: string = 'selectedMaxSizeStoredUnits'; -export const PHASE_ROLLOVER_MINIMUM_AGE: string = 'selectedMinimumAge'; -export const PHASE_ROLLOVER_MINIMUM_AGE_UNITS: string = 'selectedMinimumAgeUnits'; - -export const PHASE_FORCE_MERGE_SEGMENTS: string = 'selectedForceMergeSegments'; -export const PHASE_FORCE_MERGE_ENABLED: string = 'forceMergeEnabled'; -export const PHASE_FREEZE_ENABLED: string = 'freezeEnabled'; - -export const PHASE_SHRINK_ENABLED: string = 'shrinkEnabled'; - -export const PHASE_NODE_ATTRS: string = 'selectedNodeAttrs'; -export const PHASE_PRIMARY_SHARD_COUNT: string = 'selectedPrimaryShardCount'; -export const PHASE_REPLICA_COUNT: string = 'selectedReplicaCount'; -export const PHASE_INDEX_PRIORITY: string = 'phaseIndexPriority'; - -export const PHASE_WAIT_FOR_SNAPSHOT_POLICY = 'waitForSnapshotPolicy'; - -export const PHASE_ATTRIBUTES_THAT_ARE_NUMBERS_VALIDATE: string[] = [ - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_FORCE_MERGE_SEGMENTS, - PHASE_PRIMARY_SHARD_COUNT, - PHASE_REPLICA_COUNT, - PHASE_INDEX_PRIORITY, -]; -export const PHASE_ATTRIBUTES_THAT_ARE_NUMBERS: string[] = [ - ...PHASE_ATTRIBUTES_THAT_ARE_NUMBERS_VALIDATE, - PHASE_ROLLOVER_MAX_AGE, - PHASE_ROLLOVER_MAX_SIZE_STORED, - PHASE_ROLLOVER_MAX_DOCUMENTS, -]; - -export const STRUCTURE_INDEX_TEMPLATE: string = 'indexTemplate'; -export const STRUCTURE_TEMPLATE_SELECTION: string = 'templateSelection'; -export const STRUCTURE_TEMPLATE_NAME: string = 'templateName'; -export const STRUCTURE_CONFIGURATION: string = 'configuration'; -export const STRUCTURE_NODE_ATTRS: string = 'node_attrs'; -export const STRUCTURE_PRIMARY_NODES: string = 'primary_nodes'; -export const STRUCTURE_REPLICAS: string = 'replicas'; - -export const STRUCTURE_POLICY_CONFIGURATION: string = 'policyConfiguration'; - -export const STRUCTURE_REVIEW: string = 'review'; -export const STRUCTURE_POLICY_NAME: string = 'policyName'; -export const STRUCTURE_INDEX_NAME: string = 'indexName'; -export const STRUCTURE_ALIAS_NAME: string = 'aliasName'; - -export const ERROR_STRUCTURE: any = { - [PHASE_HOT]: { - [PHASE_ROLLOVER_ALIAS]: [], - [PHASE_ROLLOVER_MAX_AGE]: [], - [PHASE_ROLLOVER_MAX_AGE_UNITS]: [], - [PHASE_ROLLOVER_MAX_SIZE_STORED]: [], - [PHASE_ROLLOVER_MAX_DOCUMENTS]: [], - [PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS]: [], - [PHASE_INDEX_PRIORITY]: [], - }, - [PHASE_WARM]: { - [PHASE_ROLLOVER_ALIAS]: [], - [PHASE_ROLLOVER_MINIMUM_AGE]: [], - [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: [], - [PHASE_NODE_ATTRS]: [], - [PHASE_PRIMARY_SHARD_COUNT]: [], - [PHASE_REPLICA_COUNT]: [], - [PHASE_FORCE_MERGE_SEGMENTS]: [], - [PHASE_INDEX_PRIORITY]: [], - }, - [PHASE_COLD]: { - [PHASE_ROLLOVER_ALIAS]: [], - [PHASE_ROLLOVER_MINIMUM_AGE]: [], - [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: [], - [PHASE_NODE_ATTRS]: [], - [PHASE_REPLICA_COUNT]: [], - [PHASE_INDEX_PRIORITY]: [], - }, - [PHASE_DELETE]: { - [PHASE_ROLLOVER_ALIAS]: [], - [PHASE_ROLLOVER_MINIMUM_AGE]: [], - [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: [], - }, - [STRUCTURE_POLICY_NAME]: [], -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts new file mode 100644 index 00000000000000..3a19f03547b5b2 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts @@ -0,0 +1,60 @@ +/* + * 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 { + SerializedPhase, + ColdPhase, + DeletePhase, + HotPhase, + WarmPhase, +} from '../services/policies/types'; + +export const defaultNewHotPhase: HotPhase = { + phaseEnabled: true, + rolloverEnabled: true, + selectedMaxAge: '30', + selectedMaxAgeUnits: 'd', + selectedMaxSizeStored: '50', + selectedMaxSizeStoredUnits: 'gb', + phaseIndexPriority: '100', + selectedMaxDocuments: '', +}; + +export const defaultNewWarmPhase: WarmPhase = { + phaseEnabled: false, + forceMergeEnabled: false, + selectedForceMergeSegments: '', + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + selectedNodeAttrs: '', + shrinkEnabled: false, + selectedPrimaryShardCount: '', + selectedReplicaCount: '', + warmPhaseOnRollover: true, + phaseIndexPriority: '50', +}; + +export const defaultNewColdPhase: ColdPhase = { + phaseEnabled: false, + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + selectedNodeAttrs: '', + selectedReplicaCount: '', + freezeEnabled: false, + phaseIndexPriority: '0', +}; + +export const defaultNewDeletePhase: DeletePhase = { + phaseEnabled: false, + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + waitForSnapshotPolicy: '', +}; + +export const serializedPhaseInitialization: SerializedPhase = { + min_age: '0ms', + actions: {}, +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx index a3278b6c231b94..9db40ebf5521f5 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx @@ -8,28 +8,22 @@ import React, { cloneElement, Children, Fragment, ReactElement } from 'react'; import { EuiFormRow, EuiFormRowProps } from '@elastic/eui'; type Props = EuiFormRowProps & { - errorKey: string; isShowingErrors: boolean; - errors: Record; + errors?: string[]; }; export const ErrableFormRow: React.FunctionComponent = ({ - errorKey, isShowingErrors, errors, children, ...rest }) => { return ( - 0} - error={errors[errorKey]} - {...rest} - > + 0} error={errors} {...rest}> {Children.map(children, (child) => cloneElement(child as ReactElement, { - isInvalid: isShowingErrors && errors[errorKey].length > 0, + isInvalid: errors && isShowingErrors && errors.length > 0, }) )} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx index c9732f2311758e..11b743ecc4bb65 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx @@ -9,40 +9,35 @@ import { i18n } from '@kbn/i18n'; import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; -import { - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_ROLLOVER_MINIMUM_AGE_UNITS, - PHASE_WARM, - PHASE_COLD, - PHASE_DELETE, -} from '../../../constants'; import { LearnMoreLink } from './learn_more_link'; import { ErrableFormRow } from './form_errors'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; +import { ColdPhase, DeletePhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; -function getTimingLabelForPhase(phase: string) { +function getTimingLabelForPhase(phase: keyof Phases) { // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. switch (phase) { - case PHASE_WARM: + case 'warm': return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeLabel', { defaultMessage: 'Timing for warm phase', }); - case PHASE_COLD: + case 'cold': return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel', { defaultMessage: 'Timing for cold phase', }); - case PHASE_DELETE: + case 'delete': return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeLabel', { defaultMessage: 'Timing for delete phase', }); } } -function getUnitsAriaLabelForPhase(phase: string) { +function getUnitsAriaLabelForPhase(phase: keyof Phases) { // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. switch (phase) { - case PHASE_WARM: + case 'warm': return i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeUnitsAriaLabel', { @@ -50,7 +45,7 @@ function getUnitsAriaLabelForPhase(phase: string) { } ); - case PHASE_COLD: + case 'cold': return i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeUnitsAriaLabel', { @@ -58,7 +53,7 @@ function getUnitsAriaLabelForPhase(phase: string) { } ); - case PHASE_DELETE: + case 'delete': return i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeUnitsAriaLabel', { @@ -68,24 +63,23 @@ function getUnitsAriaLabelForPhase(phase: string) { } } -interface Props { +interface Props { rolloverEnabled: boolean; - errors: Record; - phase: string; - // TODO add types for phaseData and setPhaseData after policy is typed - phaseData: any; - setPhaseData: (dataKey: string, value: any) => void; + errors?: PhaseValidationErrors; + phase: keyof Phases & string; + phaseData: T; + setPhaseData: (dataKey: keyof T & string, value: string) => void; isShowingErrors: boolean; } -export const MinAgeInput: React.FunctionComponent = ({ +export const MinAgeInput = ({ rolloverEnabled, errors, phaseData, phase, setPhaseData, isShowingErrors, -}) => { +}: React.PropsWithChildren>): React.ReactElement => { let daysOptionLabel; let hoursOptionLabel; let minutesOptionLabel; @@ -192,15 +186,17 @@ export const MinAgeInput: React.FunctionComponent = ({ ); } + // check that these strings are valid properties + const selectedMinimumAgeProperty = propertyof('selectedMinimumAge'); + const selectedMinimumAgeUnitsProperty = propertyof('selectedMinimumAgeUnits'); return ( = ({ } > { - setPhaseData(PHASE_ROLLOVER_MINIMUM_AGE, e.target.value); + setPhaseData(selectedMinimumAgeProperty, e.target.value); }} min={0} /> @@ -227,8 +223,8 @@ export const MinAgeInput: React.FunctionComponent = ({ setPhaseData(PHASE_ROLLOVER_MINIMUM_AGE_UNITS, e.target.value)} + value={phaseData.selectedMinimumAgeUnits} + onChange={(e) => setPhaseData(selectedMinimumAgeUnitsProperty, e.target.value)} options={[ { value: 'd', diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx index 576483a5ab9c28..0ce2c0d7ea5663 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx @@ -16,20 +16,12 @@ import { EuiButton, } from '@elastic/eui'; -import { PHASE_NODE_ATTRS } from '../../../constants'; import { LearnMoreLink } from './learn_more_link'; import { ErrableFormRow } from './form_errors'; import { useLoadNodes } from '../../../services/api'; import { NodeAttrsDetails } from './node_attrs_details'; - -interface Props { - phase: string; - errors: Record; - // TODO add types for phaseData and setPhaseData after policy is typed - phaseData: any; - setPhaseData: (dataKey: string, value: any) => void; - isShowingErrors: boolean; -} +import { ColdPhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; const learnMoreLink = ( @@ -46,13 +38,20 @@ const learnMoreLink = ( ); -export const NodeAllocation: React.FunctionComponent = ({ +interface Props { + phase: keyof Phases & string; + errors?: PhaseValidationErrors; + phaseData: T; + setPhaseData: (dataKey: keyof T & string, value: string) => void; + isShowingErrors: boolean; +} +export const NodeAllocation = ({ phase, setPhaseData, errors, phaseData, isShowingErrors, -}) => { +}: React.PropsWithChildren>) => { const { isLoading, data: nodes, error, sendRequest } = useLoadNodes(); const [selectedNodeAttrsForDetails, setSelectedNodeAttrsForDetails] = useState( @@ -140,33 +139,35 @@ export const NodeAllocation: React.FunctionComponent = ({ ); } + // check that this string is a valid property + const nodeAttrsProperty = propertyof('selectedNodeAttrs'); + return ( { - setPhaseData(PHASE_NODE_ATTRS, e.target.value); + setPhaseData(nodeAttrsProperty, e.target.value); }} /> - {!!phaseData[PHASE_NODE_ATTRS] ? ( + {!!phaseData.selectedNodeAttrs ? ( setSelectedNodeAttrsForDetails(phaseData[PHASE_NODE_ATTRS])} + onClick={() => setSelectedNodeAttrsForDetails(phaseData.selectedNodeAttrs)} > void; - // TODO add types for lifecycle after policy is typed - lifecycle: any; + policy: Policy; policyName: string; } -export const PolicyJsonFlyout: React.FunctionComponent = ({ - close, - lifecycle, - policyName, -}) => { - // @ts-ignore until store is typed - const getEsJson = ({ phases }) => { +export const PolicyJsonFlyout: React.FunctionComponent = ({ close, policy, policyName }) => { + const getEsJson = ({ phases }: Policy) => { return JSON.stringify( { policy: { @@ -45,7 +40,7 @@ export const PolicyJsonFlyout: React.FunctionComponent = ({ }; const endpoint = `PUT _ilm/policy/${policyName || ''}`; - const request = `${endpoint}\n${getEsJson(lifecycle)}`; + const request = `${endpoint}\n${getEsJson(policy)}`; return ( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx index 0034de85fce17e..1da7508049f247 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx @@ -7,27 +7,27 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFieldNumber, EuiTextColor, EuiDescribedFormGroup } from '@elastic/eui'; -import { PHASE_INDEX_PRIORITY } from '../../../constants'; - import { LearnMoreLink } from './'; import { OptionalLabel } from './'; import { ErrableFormRow } from './'; +import { ColdPhase, HotPhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; -interface Props { - errors: Record; - // TODO add types for phaseData and setPhaseData after policy is typed - phase: string; - phaseData: any; - setPhaseData: (dataKey: string, value: any) => void; +interface Props { + errors?: PhaseValidationErrors; + phase: keyof Phases & string; + phaseData: T; + setPhaseData: (dataKey: keyof T & string, value: any) => void; isShowingErrors: boolean; } -export const SetPriorityInput: React.FunctionComponent = ({ +export const SetPriorityInput = ({ errors, phaseData, phase, setPhaseData, isShowingErrors, -}) => { +}: React.PropsWithChildren>) => { + const phaseIndexPriorityProperty = propertyof('phaseIndexPriority'); return ( = ({ fullWidth > = ({ } - errorKey={PHASE_INDEX_PRIORITY} isShowingErrors={isShowingErrors} - errors={errors} + errors={errors?.phaseIndexPriority} > { - setPhaseData(PHASE_INDEX_PRIORITY, e.target.value); + setPhaseData(phaseIndexPriorityProperty, e.target.value); }} min={0} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js deleted file mode 100644 index e7f20a66d09f09..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js +++ /dev/null @@ -1,58 +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 { connect } from 'react-redux'; - -import { - getSaveAsNewPolicy, - getSelectedPolicy, - validateLifecycle, - getLifecycle, - getPolicies, - isPolicyListLoaded, - getIsNewPolicy, - getSelectedOriginalPolicyName, - getPhases, -} from '../../store/selectors'; - -import { - setSelectedPolicy, - setSelectedPolicyName, - setSaveAsNewPolicy, - saveLifecyclePolicy, - fetchPolicies, - setPhaseData, -} from '../../store/actions'; - -import { findFirstError } from '../../services/find_errors'; -import { EditPolicy as PresentationComponent } from './edit_policy'; - -export const EditPolicy = connect( - (state) => { - const errors = validateLifecycle(state); - const firstError = findFirstError(errors); - return { - firstError, - errors, - selectedPolicy: getSelectedPolicy(state), - saveAsNewPolicy: getSaveAsNewPolicy(state), - lifecycle: getLifecycle(state), - policies: getPolicies(state), - isPolicyListLoaded: isPolicyListLoaded(state), - isNewPolicy: getIsNewPolicy(state), - originalPolicyName: getSelectedOriginalPolicyName(state), - phases: getPhases(state), - }; - }, - { - setSelectedPolicy, - setSelectedPolicyName, - setSaveAsNewPolicy, - saveLifecyclePolicy, - fetchPolicies, - setPhaseData, - } -)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx new file mode 100644 index 00000000000000..359134e015f7f2 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.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 from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { EuiButton, EuiCallOut, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useLoadPoliciesList } from '../../services/api'; + +import { EditPolicy as PresentationComponent } from './edit_policy'; + +interface RouterProps { + policyName: string; +} + +interface Props { + getUrlForApp: ( + appId: string, + options?: { + path?: string; + absolute?: boolean; + } + ) => string; +} + +export const EditPolicy: React.FunctionComponent> = ({ + match: { + params: { policyName }, + }, + getUrlForApp, + history, +}) => { + const { error, isLoading, data: policies, sendRequest } = useLoadPoliciesList(false); + if (isLoading) { + return ( + } + body={ + + } + /> + ); + } + if (error || !policies) { + const { statusCode, message } = error ? error : { statusCode: '', message: '' }; + return ( + + } + color="danger" + > +

+ {message} ({statusCode}) +

+ + + +
+ ); + } + + return ( + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js deleted file mode 100644 index a29ecd07c5e45a..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js +++ /dev/null @@ -1,390 +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 React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -import { - EuiPage, - EuiPageBody, - EuiFieldText, - EuiPageContent, - EuiFormRow, - EuiTitle, - EuiText, - EuiSpacer, - EuiSwitch, - EuiHorizontalRule, - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiDescribedFormGroup, -} from '@elastic/eui'; - -import { - PHASE_HOT, - PHASE_COLD, - PHASE_DELETE, - PHASE_WARM, - STRUCTURE_POLICY_NAME, - WARM_PHASE_ON_ROLLOVER, - PHASE_ROLLOVER_ENABLED, -} from '../../constants'; - -import { toasts } from '../../services/notification'; -import { findFirstError } from '../../services/find_errors'; -import { LearnMoreLink, PolicyJsonFlyout, ErrableFormRow } from './components'; - -import { HotPhase, WarmPhase, ColdPhase, DeletePhase } from './phases'; - -export class EditPolicy extends Component { - static propTypes = { - selectedPolicy: PropTypes.object.isRequired, - errors: PropTypes.object.isRequired, - }; - - constructor(props) { - super(props); - this.state = { - isShowingErrors: false, - isShowingPolicyJsonFlyout: false, - }; - } - - selectPolicy = (policyName) => { - const { setSelectedPolicy, policies } = this.props; - - const selectedPolicy = policies.find((policy) => { - return policy.name === policyName; - }); - - if (selectedPolicy) { - setSelectedPolicy(selectedPolicy); - } - }; - - componentDidMount() { - window.scrollTo(0, 0); - - const { - isPolicyListLoaded, - fetchPolicies, - match: { params: { policyName } } = { params: {} }, - } = this.props; - - if (policyName) { - const decodedPolicyName = decodeURIComponent(policyName); - if (isPolicyListLoaded) { - this.selectPolicy(decodedPolicyName); - } else { - fetchPolicies(true, () => { - this.selectPolicy(decodedPolicyName); - }); - } - } else { - this.props.setSelectedPolicy(null); - } - } - - backToPolicyList = () => { - this.props.setSelectedPolicy(null); - this.props.history.push('/policies'); - }; - - submit = async () => { - this.setState({ isShowingErrors: true }); - const { saveLifecyclePolicy, lifecycle, saveAsNewPolicy, firstError } = this.props; - if (firstError) { - toasts.addDanger( - i18n.translate('xpack.indexLifecycleMgmt.editPolicy.formErrorsMessage', { - defaultMessage: 'Please fix the errors on this page.', - }) - ); - const errorRowId = `${firstError.replace('.', '-')}-row`; - const element = document.getElementById(errorRowId); - if (element) { - element.scrollIntoView({ block: 'center', inline: 'nearest' }); - } - } else { - const success = await saveLifecyclePolicy(lifecycle, saveAsNewPolicy); - if (success) { - this.backToPolicyList(); - } - } - }; - - togglePolicyJsonFlyout = () => { - this.setState(({ isShowingPolicyJsonFlyout }) => ({ - isShowingPolicyJsonFlyout: !isShowingPolicyJsonFlyout, - })); - }; - - render() { - const { - selectedPolicy, - errors, - setSaveAsNewPolicy, - saveAsNewPolicy, - setSelectedPolicyName, - isNewPolicy, - lifecycle, - originalPolicyName, - phases, - setPhaseData, - } = this.props; - const selectedPolicyName = selectedPolicy.name; - const { isShowingErrors, isShowingPolicyJsonFlyout } = this.state; - - return ( - - - - -

- {isNewPolicy - ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage', { - defaultMessage: 'Create an index lifecycle policy', - }) - : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage', { - defaultMessage: 'Edit index lifecycle policy {originalPolicyName}', - values: { originalPolicyName }, - })} -

-
- -
- - -

- {' '} - - } - /> -

-
- - - - - {isNewPolicy ? null : ( - - - -

- - - - .{' '} - -

-
- -
- - - { - await setSaveAsNewPolicy(e.target.checked); - }} - label={ - - - - } - /> - -
- )} - - {saveAsNewPolicy || isNewPolicy ? ( - - - - -
- } - titleSize="s" - fullWidth - > - - } - > - { - await setSelectedPolicyName(e.target.value); - }} - /> - - - ) : null} - - - - - setPhaseData(PHASE_HOT, key, value)} - phaseData={phases[PHASE_HOT]} - setWarmPhaseOnRollover={(value) => - setPhaseData(PHASE_WARM, WARM_PHASE_ON_ROLLOVER, value) - } - /> - - - - setPhaseData(PHASE_WARM, key, value)} - phaseData={phases[PHASE_WARM]} - hotPhaseRolloverEnabled={phases[PHASE_HOT][PHASE_ROLLOVER_ENABLED]} - /> - - - - setPhaseData(PHASE_COLD, key, value)} - phaseData={phases[PHASE_COLD]} - hotPhaseRolloverEnabled={phases[PHASE_HOT][PHASE_ROLLOVER_ENABLED]} - /> - - - - setPhaseData(PHASE_DELETE, key, value)} - phaseData={phases[PHASE_DELETE]} - hotPhaseRolloverEnabled={phases[PHASE_HOT][PHASE_ROLLOVER_ENABLED]} - /> - - - - - - - - - {saveAsNewPolicy ? ( - - ) : ( - - )} - - - - - - - - - - - - - - {isShowingPolicyJsonFlyout ? ( - - ) : ( - - )} - - - - - {this.state.isShowingPolicyJsonFlyout ? ( - this.setState({ isShowingPolicyJsonFlyout: false })} - /> - ) : null} - -
-
-
- ); - } -} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx new file mode 100644 index 00000000000000..6cffde577b35ee --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -0,0 +1,383 @@ +/* + * 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, useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiButton, + EuiButtonEmpty, + EuiDescribedFormGroup, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiHorizontalRule, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiSpacer, + EuiSwitch, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { toasts } from '../../services/notification'; + +import { Policy, PolicyFromES } from '../../services/policies/types'; +import { + validatePolicy, + ValidationErrors, + findFirstError, +} from '../../services/policies/policy_validation'; +import { savePolicy } from '../../services/policies/policy_save'; +import { + deserializePolicy, + getPolicyByName, + initializeNewPolicy, +} from '../../services/policies/policy_serialization'; + +import { ErrableFormRow, LearnMoreLink, PolicyJsonFlyout } from './components'; +import { ColdPhase, DeletePhase, HotPhase, WarmPhase } from './phases'; + +interface Props { + policies: PolicyFromES[]; + policyName: string; + getUrlForApp: ( + appId: string, + options?: { + path?: string; + absolute?: boolean; + } + ) => string; + history: any; +} +export const EditPolicy: React.FunctionComponent = ({ + policies, + policyName, + history, + getUrlForApp, +}) => { + useEffect(() => { + window.scrollTo(0, 0); + }, []); + + const [isShowingErrors, setIsShowingErrors] = useState(false); + const [errors, setErrors] = useState(); + const [isShowingPolicyJsonFlyout, setIsShowingPolicyJsonFlyout] = useState(false); + + const existingPolicy = getPolicyByName(policies, policyName); + + const [policy, setPolicy] = useState( + existingPolicy ? deserializePolicy(existingPolicy) : initializeNewPolicy(policyName) + ); + + const isNewPolicy: boolean = !Boolean(existingPolicy); + const [saveAsNew, setSaveAsNew] = useState(isNewPolicy); + const originalPolicyName: string = existingPolicy ? existingPolicy.name : ''; + + const backToPolicyList = () => { + history.push('/policies'); + }; + + const submit = async () => { + setIsShowingErrors(true); + const [isValid, validationErrors] = validatePolicy( + saveAsNew, + policy, + policies, + originalPolicyName + ); + setErrors(validationErrors); + + if (!isValid) { + toasts.addDanger( + i18n.translate('xpack.indexLifecycleMgmt.editPolicy.formErrorsMessage', { + defaultMessage: 'Please fix the errors on this page.', + }) + ); + const firstError = findFirstError(validationErrors); + const errorRowId = `${firstError ? firstError.replace('.', '-') : ''}-row`; + const element = document.getElementById(errorRowId); + if (element) { + element.scrollIntoView({ block: 'center', inline: 'nearest' }); + } + } else { + const success = await savePolicy(policy, isNewPolicy || saveAsNew, existingPolicy); + if (success) { + backToPolicyList(); + } + } + }; + + const togglePolicyJsonFlyout = () => { + setIsShowingPolicyJsonFlyout(!isShowingPolicyJsonFlyout); + }; + + const setPhaseData = (phase: 'hot' | 'warm' | 'cold' | 'delete', key: string, value: any) => { + setPolicy({ + ...policy, + phases: { + ...policy.phases, + [phase]: { ...policy.phases[phase], [key]: value }, + }, + }); + }; + + const setWarmPhaseOnRollover = (value: boolean) => { + setPolicy({ + ...policy, + phases: { + ...policy.phases, + hot: { + ...policy.phases.hot, + rolloverEnabled: value, + }, + warm: { + ...policy.phases.warm, + warmPhaseOnRollover: value, + }, + }, + }); + }; + + return ( + + + + +

+ {isNewPolicy + ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage', { + defaultMessage: 'Create an index lifecycle policy', + }) + : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage', { + defaultMessage: 'Edit index lifecycle policy {originalPolicyName}', + values: { originalPolicyName }, + })} +

+
+ +
+ + +

+ {' '} + + } + /> +

+
+ + + + {isNewPolicy ? null : ( + + +

+ + + + .{' '} + +

+
+ + + + { + setSaveAsNew(e.target.checked); + }} + label={ + + + + } + /> + +
+ )} + + {saveAsNew || isNewPolicy ? ( + + + + +
+ } + titleSize="s" + fullWidth + > + + } + > + { + setPolicy({ ...policy, name: e.target.value }); + }} + /> + + + ) : null} + + + + 0} + setPhaseData={(key, value) => setPhaseData('hot', key, value)} + phaseData={policy.phases.hot} + setWarmPhaseOnRollover={setWarmPhaseOnRollover} + /> + + + + 0} + setPhaseData={(key, value) => setPhaseData('warm', key, value)} + phaseData={policy.phases.warm} + hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} + /> + + + + 0} + setPhaseData={(key, value) => setPhaseData('cold', key, value)} + phaseData={policy.phases.cold} + hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} + /> + + + + 0} + getUrlForApp={getUrlForApp} + setPhaseData={(key, value) => setPhaseData('delete', key, value)} + phaseData={policy.phases.delete} + hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} + /> + + + + + + + + + {saveAsNew ? ( + + ) : ( + + )} + + + + + + + + + + + + + + {isShowingPolicyJsonFlyout ? ( + + ) : ( + + )} + + + + + {isShowingPolicyJsonFlyout ? ( + setIsShowingPolicyJsonFlyout(false)} + /> + ) : null} + +
+
+
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.d.ts deleted file mode 100644 index 5f15d929a4916e..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.d.ts +++ /dev/null @@ -1,7 +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. - */ - -export declare const EditPolicy: any; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.ts diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx index babbbf7638ebef..fb32752fe24ea6 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx @@ -18,12 +18,9 @@ import { EuiTextColor, } from '@elastic/eui'; -import { - PHASE_COLD, - PHASE_ENABLED, - PHASE_REPLICA_COUNT, - PHASE_FREEZE_ENABLED, -} from '../../../constants'; +import { ColdPhase as ColdPhaseInterface, Phases } from '../../../services/policies/types'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; + import { LearnMoreLink, ActiveBadge, @@ -35,14 +32,21 @@ import { SetPriorityInput, } from '../components'; +const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel', { + defaultMessage: 'Freeze index', +}); + +const coldProperty = propertyof('cold'); +const phaseProperty = (propertyName: keyof ColdPhaseInterface) => + propertyof(propertyName); + interface Props { - setPhaseData: (key: string, value: any) => void; - phaseData: any; + setPhaseData: (key: keyof ColdPhaseInterface & string, value: string | boolean) => void; + phaseData: ColdPhaseInterface; isShowingErrors: boolean; - errors: Record; + errors?: PhaseValidationErrors; hotPhaseRolloverEnabled: boolean; } - export class ColdPhase extends PureComponent { render() { const { @@ -53,10 +57,6 @@ export class ColdPhase extends PureComponent { hotPhaseRolloverEnabled, } = this.props; - const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel', { - defaultMessage: 'Freeze index', - }); - return (
{ defaultMessage="Cold phase" /> {' '} - {phaseData[PHASE_ENABLED] && !isShowingErrors ? : null} + {phaseData.phaseEnabled && !isShowingErrors ? : null}
} @@ -91,10 +91,10 @@ export class ColdPhase extends PureComponent { defaultMessage="Activate cold phase" /> } - id={`${PHASE_COLD}-${PHASE_ENABLED}`} - checked={phaseData[PHASE_ENABLED]} + id={`${coldProperty}-${phaseProperty('phaseEnabled')}`} + checked={phaseData.phaseEnabled} onChange={(e) => { - setPhaseData(PHASE_ENABLED, e.target.checked); + setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); }} aria-controls="coldPhaseContent" /> @@ -103,20 +103,20 @@ export class ColdPhase extends PureComponent { fullWidth > - {phaseData[PHASE_ENABLED] ? ( + {phaseData.phaseEnabled ? ( - errors={errors} phaseData={phaseData} - phase={PHASE_COLD} + phase={coldProperty} isShowingErrors={isShowingErrors} setPhaseData={setPhaseData} rolloverEnabled={hotPhaseRolloverEnabled} /> - + phase={coldProperty} setPhaseData={setPhaseData} errors={errors} phaseData={phaseData} @@ -126,7 +126,7 @@ export class ColdPhase extends PureComponent { { } - errorKey={PHASE_REPLICA_COUNT} isShowingErrors={isShowingErrors} - errors={errors} + errors={errors?.freezeEnabled} helpText={i18n.translate( 'xpack.indexLifecycleMgmt.coldPhase.replicaCountHelpText', { @@ -147,10 +146,10 @@ export class ColdPhase extends PureComponent { )} > { - setPhaseData(PHASE_REPLICA_COUNT, e.target.value); + setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value); }} min={0} /> @@ -163,7 +162,7 @@ export class ColdPhase extends PureComponent { )} - {phaseData[PHASE_ENABLED] ? ( + {phaseData.phaseEnabled ? ( { > { - setPhaseData(PHASE_FREEZE_ENABLED, e.target.checked); + setPhaseData(phaseProperty('freezeEnabled'), e.target.checked); }} label={freezeLabel} aria-label={freezeLabel} /> - errors={errors} phaseData={phaseData} - phase={PHASE_COLD} + phase={coldProperty} isShowingErrors={isShowingErrors} setPhaseData={setPhaseData} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx index 0143cc4af24e33..d3c73090f25f2d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx @@ -8,7 +8,9 @@ import React, { PureComponent, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui'; -import { PHASE_DELETE, PHASE_ENABLED, PHASE_WAIT_FOR_SNAPSHOT_POLICY } from '../../../constants'; +import { DeletePhase as DeletePhaseInterface, Phases } from '../../../services/policies/types'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; + import { ActiveBadge, LearnMoreLink, @@ -18,11 +20,15 @@ import { SnapshotPolicies, } from '../components'; +const deleteProperty = propertyof('delete'); +const phaseProperty = (propertyName: keyof DeletePhaseInterface) => + propertyof(propertyName); + interface Props { - setPhaseData: (key: string, value: any) => void; - phaseData: any; + setPhaseData: (key: keyof DeletePhaseInterface & string, value: string | boolean) => void; + phaseData: DeletePhaseInterface; isShowingErrors: boolean; - errors: Record; + errors?: PhaseValidationErrors; hotPhaseRolloverEnabled: boolean; getUrlForApp: ( appId: string, @@ -55,7 +61,7 @@ export class DeletePhase extends PureComponent { defaultMessage="Delete phase" /> {' '} - {phaseData[PHASE_ENABLED] && !isShowingErrors ? : null} + {phaseData.phaseEnabled && !isShowingErrors ? : null} } @@ -76,10 +82,10 @@ export class DeletePhase extends PureComponent { defaultMessage="Activate delete phase" /> } - id={`${PHASE_DELETE}-${PHASE_ENABLED}`} - checked={phaseData[PHASE_ENABLED]} + id={`${deleteProperty}-${phaseProperty('phaseEnabled')}`} + checked={phaseData.phaseEnabled} onChange={(e) => { - setPhaseData(PHASE_ENABLED, e.target.checked); + setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); }} aria-controls="deletePhaseContent" /> @@ -87,11 +93,11 @@ export class DeletePhase extends PureComponent { } fullWidth > - {phaseData[PHASE_ENABLED] ? ( - errors={errors} phaseData={phaseData} - phase={PHASE_DELETE} + phase={deleteProperty} isShowingErrors={isShowingErrors} setPhaseData={setPhaseData} rolloverEnabled={hotPhaseRolloverEnabled} @@ -100,7 +106,7 @@ export class DeletePhase extends PureComponent {
)} - {phaseData[PHASE_ENABLED] ? ( + {phaseData.phaseEnabled ? ( @@ -135,8 +141,8 @@ export class DeletePhase extends PureComponent { } > setPhaseData(PHASE_WAIT_FOR_SNAPSHOT_POLICY, value)} + value={phaseData.waitForSnapshotPolicy} + onChange={(value) => setPhaseData(phaseProperty('waitForSnapshotPolicy'), value)} getUrlForApp={getUrlForApp} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx index dbd48f3a856345..22f0114d16afe4 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx @@ -7,7 +7,6 @@ import React, { Fragment, PureComponent } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; - import { EuiFlexGroup, EuiFlexItem, @@ -19,15 +18,9 @@ import { EuiDescribedFormGroup, } from '@elastic/eui'; -import { - PHASE_HOT, - PHASE_ROLLOVER_MAX_AGE, - PHASE_ROLLOVER_MAX_AGE_UNITS, - PHASE_ROLLOVER_MAX_DOCUMENTS, - PHASE_ROLLOVER_MAX_SIZE_STORED, - PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS, - PHASE_ROLLOVER_ENABLED, -} from '../../../constants'; +import { HotPhase as HotPhaseInterface, Phases } from '../../../services/policies/types'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; + import { LearnMoreLink, ActiveBadge, @@ -36,11 +29,98 @@ import { SetPriorityInput, } from '../components'; +const maxSizeStoredUnits = [ + { + value: 'gb', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.gigabytesLabel', { + defaultMessage: 'gigabytes', + }), + }, + { + value: 'mb', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.megabytesLabel', { + defaultMessage: 'megabytes', + }), + }, + { + value: 'b', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.bytesLabel', { + defaultMessage: 'bytes', + }), + }, + { + value: 'kb', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.kilobytesLabel', { + defaultMessage: 'kilobytes', + }), + }, + { + value: 'tb', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.terabytesLabel', { + defaultMessage: 'terabytes', + }), + }, + { + value: 'pb', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.petabytesLabel', { + defaultMessage: 'petabytes', + }), + }, +]; + +const maxAgeUnits = [ + { + value: 'd', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.daysLabel', { + defaultMessage: 'days', + }), + }, + { + value: 'h', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.hoursLabel', { + defaultMessage: 'hours', + }), + }, + { + value: 'm', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.minutesLabel', { + defaultMessage: 'minutes', + }), + }, + { + value: 's', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.secondsLabel', { + defaultMessage: 'seconds', + }), + }, + { + value: 'ms', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.millisecondsLabel', { + defaultMessage: 'milliseconds', + }), + }, + { + value: 'micros', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.microsecondsLabel', { + defaultMessage: 'microseconds', + }), + }, + { + value: 'nanos', + text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.nanosecondsLabel', { + defaultMessage: 'nanoseconds', + }), + }, +]; +const hotProperty = propertyof('hot'); +const phaseProperty = (propertyName: keyof HotPhaseInterface) => + propertyof(propertyName); + interface Props { - errors: Record; + errors?: PhaseValidationErrors; isShowingErrors: boolean; - phaseData: any; - setPhaseData: (key: string, value: any) => void; + phaseData: HotPhaseInterface; + setPhaseData: (key: keyof HotPhaseInterface & string, value: string | boolean) => void; setWarmPhaseOnRollover: (value: boolean) => void; } @@ -104,39 +184,36 @@ export class HotPhase extends PureComponent { > { - const { checked } = e.target; - setPhaseData(PHASE_ROLLOVER_ENABLED, checked); - setWarmPhaseOnRollover(checked); + setWarmPhaseOnRollover(e.target.checked); }} label={i18n.translate('xpack.indexLifecycleMgmt.hotPhase.enableRolloverLabel', { defaultMessage: 'Enable rollover', })} /> - {phaseData[PHASE_ROLLOVER_ENABLED] ? ( + {phaseData.rolloverEnabled ? ( { - setPhaseData(PHASE_ROLLOVER_MAX_SIZE_STORED, e.target.value); + setPhaseData(phaseProperty('selectedMaxSizeStored'), e.target.value); }} min={1} /> @@ -144,11 +221,10 @@ export class HotPhase extends PureComponent { { defaultMessage: 'Maximum index size units', } )} - value={phaseData[PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS]} + value={phaseData.selectedMaxSizeStoredUnits} onChange={(e) => { - setPhaseData(PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS, e.target.value); + setPhaseData(phaseProperty('selectedMaxSizeStoredUnits'), e.target.value); }} - options={[ - { - value: 'gb', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.gigabytesLabel', { - defaultMessage: 'gigabytes', - }), - }, - { - value: 'mb', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.megabytesLabel', { - defaultMessage: 'megabytes', - }), - }, - { - value: 'b', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.bytesLabel', { - defaultMessage: 'bytes', - }), - }, - { - value: 'kb', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.kilobytesLabel', { - defaultMessage: 'kilobytes', - }), - }, - { - value: 'tb', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.terabytesLabel', { - defaultMessage: 'terabytes', - }), - }, - { - value: 'pb', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.petabytesLabel', { - defaultMessage: 'petabytes', - }), - }, - ]} + options={maxSizeStoredUnits} /> @@ -207,22 +246,21 @@ export class HotPhase extends PureComponent { { - setPhaseData(PHASE_ROLLOVER_MAX_DOCUMENTS, e.target.value); + setPhaseData(phaseProperty('selectedMaxDocuments'), e.target.value); }} min={1} /> @@ -233,19 +271,18 @@ export class HotPhase extends PureComponent { { - setPhaseData(PHASE_ROLLOVER_MAX_AGE, e.target.value); + setPhaseData(phaseProperty('selectedMaxAge'), e.target.value); }} min={1} /> @@ -253,11 +290,10 @@ export class HotPhase extends PureComponent { { defaultMessage: 'Maximum age units', } )} - value={phaseData[PHASE_ROLLOVER_MAX_AGE_UNITS]} + value={phaseData.selectedMaxAgeUnits} onChange={(e) => { - setPhaseData(PHASE_ROLLOVER_MAX_AGE_UNITS, e.target.value); + setPhaseData(phaseProperty('selectedMaxAgeUnits'), e.target.value); }} - options={[ - { - value: 'd', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.daysLabel', { - defaultMessage: 'days', - }), - }, - { - value: 'h', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.hoursLabel', { - defaultMessage: 'hours', - }), - }, - { - value: 'm', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.minutesLabel', { - defaultMessage: 'minutes', - }), - }, - { - value: 's', - text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.secondsLabel', { - defaultMessage: 'seconds', - }), - }, - { - value: 'ms', - text: i18n.translate( - 'xpack.indexLifecycleMgmt.hotPhase.millisecondsLabel', - { - defaultMessage: 'milliseconds', - } - ), - }, - { - value: 'micros', - text: i18n.translate( - 'xpack.indexLifecycleMgmt.hotPhase.microsecondsLabel', - { - defaultMessage: 'microseconds', - } - ), - }, - { - value: 'nanos', - text: i18n.translate( - 'xpack.indexLifecycleMgmt.hotPhase.nanosecondsLabel', - { - defaultMessage: 'nanoseconds', - } - ), - }, - ]} + options={maxAgeUnits} /> @@ -330,10 +314,10 @@ export class HotPhase extends PureComponent { ) : null} - errors={errors} phaseData={phaseData} - phase={PHASE_HOT} + phase={hotProperty} isShowingErrors={isShowingErrors} setPhaseData={setPhaseData} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx index 6ed81bf8f45d51..f7b8c60a5c71f1 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx @@ -18,16 +18,6 @@ import { EuiDescribedFormGroup, } from '@elastic/eui'; -import { - PHASE_WARM, - PHASE_ENABLED, - WARM_PHASE_ON_ROLLOVER, - PHASE_FORCE_MERGE_ENABLED, - PHASE_FORCE_MERGE_SEGMENTS, - PHASE_PRIMARY_SHARD_COUNT, - PHASE_REPLICA_COUNT, - PHASE_SHRINK_ENABLED, -} from '../../../constants'; import { LearnMoreLink, ActiveBadge, @@ -39,11 +29,33 @@ import { MinAgeInput, } from '../components'; +import { Phases, WarmPhase as WarmPhaseInterface } from '../../../services/policies/types'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; + +const shrinkLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', { + defaultMessage: 'Shrink index', +}); + +const moveToWarmPhaseOnRolloverLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel', + { + defaultMessage: 'Move to warm phase on rollover', + } +); + +const forcemergeLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.forceMergeDataLabel', { + defaultMessage: 'Force merge data', +}); + +const warmProperty = propertyof('warm'); +const phaseProperty = (propertyName: keyof WarmPhaseInterface) => + propertyof(propertyName); + interface Props { - setPhaseData: (key: string, value: any) => void; - phaseData: any; + setPhaseData: (key: keyof WarmPhaseInterface & string, value: boolean | string) => void; + phaseData: WarmPhaseInterface; isShowingErrors: boolean; - errors: Record; + errors?: PhaseValidationErrors; hotPhaseRolloverEnabled: boolean; } export class WarmPhase extends PureComponent { @@ -56,24 +68,6 @@ export class WarmPhase extends PureComponent { hotPhaseRolloverEnabled, } = this.props; - const shrinkLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', { - defaultMessage: 'Shrink index', - }); - - const moveToWarmPhaseOnRolloverLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel', - { - defaultMessage: 'Move to warm phase on rollover', - } - ); - - const forcemergeLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.forceMergeDataLabel', - { - defaultMessage: 'Force merge data', - } - ); - return (
{ defaultMessage="Warm phase" /> {' '} - {phaseData[PHASE_ENABLED] && !isShowingErrors ? : null} + {phaseData.phaseEnabled && !isShowingErrors ? : null}
} @@ -108,10 +102,10 @@ export class WarmPhase extends PureComponent { defaultMessage="Activate warm phase" /> } - id={`${PHASE_WARM}-${PHASE_ENABLED}`} - checked={phaseData[PHASE_ENABLED]} + id={`${warmProperty}-${phaseProperty('phaseEnabled')}`} + checked={phaseData.phaseEnabled} onChange={(e) => { - setPhaseData(PHASE_ENABLED, e.target.checked); + setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); }} aria-controls="warmPhaseContent" /> @@ -120,28 +114,28 @@ export class WarmPhase extends PureComponent { fullWidth > - {phaseData[PHASE_ENABLED] ? ( + {phaseData.phaseEnabled ? ( {hotPhaseRolloverEnabled ? ( - + { - setPhaseData(WARM_PHASE_ON_ROLLOVER, e.target.checked); + setPhaseData(phaseProperty('warmPhaseOnRollover'), e.target.checked); }} /> ) : null} - {!phaseData[WARM_PHASE_ON_ROLLOVER] ? ( + {!phaseData.warmPhaseOnRollover ? ( - errors={errors} phaseData={phaseData} - phase={PHASE_WARM} + phase={warmProperty} isShowingErrors={isShowingErrors} setPhaseData={setPhaseData} rolloverEnabled={hotPhaseRolloverEnabled} @@ -151,8 +145,8 @@ export class WarmPhase extends PureComponent { - + phase={warmProperty} setPhaseData={setPhaseData} errors={errors} phaseData={phaseData} @@ -162,7 +156,7 @@ export class WarmPhase extends PureComponent { { } - errorKey={PHASE_REPLICA_COUNT} isShowingErrors={isShowingErrors} - errors={errors} + errors={errors?.selectedReplicaCount} helpText={i18n.translate( 'xpack.indexLifecycleMgmt.warmPhase.replicaCountHelpText', { @@ -183,10 +176,10 @@ export class WarmPhase extends PureComponent { )} > { - setPhaseData(PHASE_REPLICA_COUNT, e.target.value); + setPhaseData('selectedReplicaCount', e.target.value); }} min={0} /> @@ -199,7 +192,7 @@ export class WarmPhase extends PureComponent { ) : null} - {phaseData[PHASE_ENABLED] ? ( + {phaseData.phaseEnabled ? ( { { - setPhaseData(PHASE_SHRINK_ENABLED, e.target.checked); + setPhaseData(phaseProperty('shrinkEnabled'), e.target.checked); }} label={shrinkLabel} aria-label={shrinkLabel} @@ -235,28 +228,30 @@ export class WarmPhase extends PureComponent { />
- {phaseData[PHASE_SHRINK_ENABLED] ? ( + {phaseData.shrinkEnabled ? ( { - setPhaseData(PHASE_PRIMARY_SHARD_COUNT, e.target.value); + setPhaseData( + phaseProperty('selectedPrimaryShardCount'), + e.target.value + ); }} min={1} /> @@ -294,33 +289,32 @@ export class WarmPhase extends PureComponent { data-test-subj="forceMergeSwitch" label={forcemergeLabel} aria-label={forcemergeLabel} - checked={phaseData[PHASE_FORCE_MERGE_ENABLED]} + checked={phaseData.forceMergeEnabled} onChange={(e) => { - setPhaseData(PHASE_FORCE_MERGE_ENABLED, e.target.checked); + setPhaseData(phaseProperty('forceMergeEnabled'), e.target.checked); }} aria-controls="forcemergeContent" />
- {phaseData[PHASE_FORCE_MERGE_ENABLED] ? ( + {phaseData.forceMergeEnabled ? ( { - setPhaseData(PHASE_FORCE_MERGE_SEGMENTS, e.target.value); + setPhaseData(phaseProperty('selectedForceMergeSegments'), e.target.value); }} min={1} /> @@ -328,10 +322,10 @@ export class WarmPhase extends PureComponent { ) : null}
- errors={errors} phaseData={phaseData} - phase={PHASE_WARM} + phase={warmProperty} isShowingErrors={isShowingErrors} setPhaseData={setPhaseData} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js index 500ab44d966941..ec1cdb987f4b32 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js @@ -38,7 +38,7 @@ import { import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import { reactRouterNavigate } from '../../../../../../../../../src/plugins/kibana_react/public'; import { getIndexListUri } from '../../../../../../../index_management/public'; -import { UIM_EDIT_CLICK } from '../../../../constants'; +import { UIM_EDIT_CLICK } from '../../../../constants/ui_metric'; import { getPolicyPath } from '../../../../services/navigation'; import { flattenPanelTree } from '../../../../services/flatten_panel_tree'; import { trackUiMetric } from '../../../../services/ui_metric'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts index 61de37bbfad11a..b80e9e70c54faa 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts @@ -12,10 +12,11 @@ import { UIM_POLICY_ATTACH_INDEX_TEMPLATE, UIM_POLICY_DETACH_INDEX, UIM_INDEX_RETRY_STEP, -} from '../constants'; +} from '../constants/ui_metric'; import { trackUiMetric } from './ui_metric'; import { sendGet, sendPost, sendDelete, useRequest } from './http'; +import { PolicyFromES, SerializedPolicy } from './policies/types'; interface GenericObject { [key: string]: any; @@ -44,7 +45,15 @@ export async function loadPolicies(withIndices: boolean) { return await sendGet('policies', { withIndices }); } -export async function savePolicy(policy: GenericObject) { +export const useLoadPoliciesList = (withIndices: boolean) => { + return useRequest({ + path: `policies`, + method: 'get', + query: { withIndices }, + }); +}; + +export async function savePolicy(policy: SerializedPolicy) { return await sendPost(`policies`, policy); } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/find_errors.js b/x-pack/plugins/index_lifecycle_management/public/application/services/find_errors.js deleted file mode 100644 index 12b53ad1eaf524..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/find_errors.js +++ /dev/null @@ -1,24 +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. - */ - -export const findFirstError = (object, topLevel = true) => { - let firstError; - const keys = topLevel ? ['policyName', 'hot', 'warm', 'cold', 'delete'] : Object.keys(object); - for (const key of keys) { - const value = object[key]; - if (Array.isArray(value) && value.length > 0) { - firstError = key; - break; - } else if (value) { - firstError = findFirstError(value, false); - if (firstError) { - firstError = `${key}.${firstError}`; - break; - } - } - } - return firstError; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts new file mode 100644 index 00000000000000..d9ed7a0bf51eb9 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts @@ -0,0 +1,159 @@ +/* + * 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 { isEmpty } from 'lodash'; +import { serializedPhaseInitialization } from '../../constants'; +import { AllocateAction, ColdPhase, SerializedColdPhase } from './types'; +import { isNumber, splitSizeAndUnits } from './policy_serialization'; +import { + numberRequiredMessage, + PhaseValidationErrors, + positiveNumberRequiredMessage, +} from './policy_validation'; + +const coldPhaseInitialization: ColdPhase = { + phaseEnabled: false, + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + selectedNodeAttrs: '', + selectedReplicaCount: '', + freezeEnabled: false, + phaseIndexPriority: '', +}; + +export const coldPhaseFromES = (phaseSerialized?: SerializedColdPhase): ColdPhase => { + const phase = { ...coldPhaseInitialization }; + if (phaseSerialized === undefined || phaseSerialized === null) { + return phase; + } + + phase.phaseEnabled = true; + + if (phaseSerialized.min_age) { + const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); + phase.selectedMinimumAge = minAge; + phase.selectedMinimumAgeUnits = minAgeUnits; + } + + if (phaseSerialized.actions) { + const actions = phaseSerialized.actions; + if (actions.allocate) { + const allocate = actions.allocate; + if (allocate.require) { + Object.entries(allocate.require).forEach((entry) => { + phase.selectedNodeAttrs = entry.join(':'); + }); + if (allocate.number_of_replicas) { + phase.selectedReplicaCount = allocate.number_of_replicas.toString(); + } + } + } + + if (actions.freeze) { + phase.freezeEnabled = true; + } + + if (actions.set_priority) { + phase.phaseIndexPriority = actions.set_priority.priority + ? actions.set_priority.priority.toString() + : ''; + } + } + + return phase; +}; + +export const coldPhaseToES = ( + phase: ColdPhase, + originalPhase: SerializedColdPhase | undefined +): SerializedColdPhase => { + if (!originalPhase) { + originalPhase = { ...serializedPhaseInitialization }; + } + + const esPhase = { ...originalPhase }; + + if (isNumber(phase.selectedMinimumAge)) { + esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; + } + + esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; + + if (phase.selectedNodeAttrs) { + const [name, value] = phase.selectedNodeAttrs.split(':'); + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.require = { + [name]: value, + }; + } else { + if (esPhase.actions.allocate) { + delete esPhase.actions.allocate.require; + } + } + + if (isNumber(phase.selectedReplicaCount)) { + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.number_of_replicas = parseInt(phase.selectedReplicaCount, 10); + } else { + if (esPhase.actions.allocate) { + delete esPhase.actions.allocate.number_of_replicas; + } + } + + if ( + esPhase.actions.allocate && + !esPhase.actions.allocate.require && + !isNumber(esPhase.actions.allocate.number_of_replicas) && + isEmpty(esPhase.actions.allocate.include) && + isEmpty(esPhase.actions.allocate.exclude) + ) { + // remove allocate action if it does not define require or number of nodes + // and both include and exclude are empty objects (ES will fail to parse if we don't) + delete esPhase.actions.allocate; + } + + if (phase.freezeEnabled) { + esPhase.actions.freeze = {}; + } else { + delete esPhase.actions.freeze; + } + + if (isNumber(phase.phaseIndexPriority)) { + esPhase.actions.set_priority = { + priority: parseInt(phase.phaseIndexPriority, 10), + }; + } else { + delete esPhase.actions.set_priority; + } + + return esPhase; +}; + +export const validateColdPhase = (phase: ColdPhase): PhaseValidationErrors => { + if (!phase.phaseEnabled) { + return {}; + } + + const phaseErrors = {} as PhaseValidationErrors; + + // index priority is optional, but if it's set, it needs to be a positive number + if (phase.phaseIndexPriority) { + if (!isNumber(phase.phaseIndexPriority)) { + phaseErrors.phaseIndexPriority = [numberRequiredMessage]; + } else if (parseInt(phase.phaseIndexPriority, 10) < 0) { + phaseErrors.phaseIndexPriority = [positiveNumberRequiredMessage]; + } + } + + // min age needs to be a positive number + if (!isNumber(phase.selectedMinimumAge)) { + phaseErrors.phaseIndexPriority = [numberRequiredMessage]; + } else if (parseInt(phase.selectedMinimumAge, 10) < 0) { + phaseErrors.phaseIndexPriority = [positiveNumberRequiredMessage]; + } + + return { ...phaseErrors }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts new file mode 100644 index 00000000000000..70e7c21da8cb68 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts @@ -0,0 +1,88 @@ +/* + * 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 { serializedPhaseInitialization } from '../../constants'; +import { DeletePhase, SerializedDeletePhase } from './types'; +import { isNumber, splitSizeAndUnits } from './policy_serialization'; +import { + numberRequiredMessage, + PhaseValidationErrors, + positiveNumberRequiredMessage, +} from './policy_validation'; + +const deletePhaseInitialization: DeletePhase = { + phaseEnabled: false, + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + waitForSnapshotPolicy: '', +}; + +export const deletePhaseFromES = (phaseSerialized?: SerializedDeletePhase): DeletePhase => { + const phase = { ...deletePhaseInitialization }; + if (phaseSerialized === undefined || phaseSerialized === null) { + return phase; + } + + phase.phaseEnabled = true; + if (phaseSerialized.min_age) { + const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); + phase.selectedMinimumAge = minAge; + phase.selectedMinimumAgeUnits = minAgeUnits; + } + + if (phaseSerialized.actions) { + const actions = phaseSerialized.actions; + + if (actions.wait_for_snapshot) { + phase.waitForSnapshotPolicy = actions.wait_for_snapshot.policy; + } + } + + return phase; +}; + +export const deletePhaseToES = ( + phase: DeletePhase, + originalEsPhase?: SerializedDeletePhase +): SerializedDeletePhase => { + if (!originalEsPhase) { + originalEsPhase = { ...serializedPhaseInitialization }; + } + const esPhase = { ...originalEsPhase }; + + if (isNumber(phase.selectedMinimumAge)) { + esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; + } + + esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; + + if (phase.waitForSnapshotPolicy) { + esPhase.actions.wait_for_snapshot = { + policy: phase.waitForSnapshotPolicy, + }; + } else { + delete esPhase.actions.wait_for_snapshot; + } + + return esPhase; +}; + +export const validateDeletePhase = (phase: DeletePhase): PhaseValidationErrors => { + if (!phase.phaseEnabled) { + return {}; + } + + const phaseErrors = {} as PhaseValidationErrors; + + // min age needs to be a positive number + if (!isNumber(phase.selectedMinimumAge)) { + phaseErrors.selectedMinimumAge = [numberRequiredMessage]; + } else if (parseInt(phase.selectedMinimumAge, 10) < 0) { + phaseErrors.selectedMinimumAge = [positiveNumberRequiredMessage]; + } + + return { ...phaseErrors }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts new file mode 100644 index 00000000000000..34ac8f3e270e6d --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts @@ -0,0 +1,155 @@ +/* + * 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 { serializedPhaseInitialization } from '../../constants'; +import { isNumber, splitSizeAndUnits } from './policy_serialization'; +import { HotPhase, SerializedHotPhase } from './types'; +import { + maximumAgeRequiredMessage, + maximumDocumentsRequiredMessage, + maximumSizeRequiredMessage, + numberRequiredMessage, + PhaseValidationErrors, + positiveNumberRequiredMessage, + positiveNumbersAboveZeroErrorMessage, +} from './policy_validation'; + +const hotPhaseInitialization: HotPhase = { + phaseEnabled: false, + rolloverEnabled: false, + selectedMaxAge: '', + selectedMaxAgeUnits: 'd', + selectedMaxSizeStored: '', + selectedMaxSizeStoredUnits: 'gb', + phaseIndexPriority: '', + selectedMaxDocuments: '', +}; + +export const hotPhaseFromES = (phaseSerialized?: SerializedHotPhase): HotPhase => { + const phase: HotPhase = { ...hotPhaseInitialization }; + + if (phaseSerialized === undefined || phaseSerialized === null) { + return phase; + } + + phase.phaseEnabled = true; + + if (phaseSerialized.actions) { + const actions = phaseSerialized.actions; + + if (actions.rollover) { + const rollover = actions.rollover; + phase.rolloverEnabled = true; + if (rollover.max_age) { + const { size: maxAge, units: maxAgeUnits } = splitSizeAndUnits(rollover.max_age); + phase.selectedMaxAge = maxAge; + phase.selectedMaxAgeUnits = maxAgeUnits; + } + if (rollover.max_size) { + const { size: maxSize, units: maxSizeUnits } = splitSizeAndUnits(rollover.max_size); + phase.selectedMaxSizeStored = maxSize; + phase.selectedMaxSizeStoredUnits = maxSizeUnits; + } + if (rollover.max_docs) { + phase.selectedMaxDocuments = rollover.max_docs.toString(); + } + } + + if (actions.set_priority) { + phase.phaseIndexPriority = actions.set_priority.priority + ? actions.set_priority.priority.toString() + : ''; + } + } + + return phase; +}; + +export const hotPhaseToES = ( + phase: HotPhase, + originalPhase?: SerializedHotPhase +): SerializedHotPhase => { + if (!originalPhase) { + originalPhase = { ...serializedPhaseInitialization }; + } + + const esPhase = { ...originalPhase }; + + esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; + + if (phase.rolloverEnabled) { + if (!esPhase.actions.rollover) { + esPhase.actions.rollover = {}; + } + if (isNumber(phase.selectedMaxAge)) { + esPhase.actions.rollover.max_age = `${phase.selectedMaxAge}${phase.selectedMaxAgeUnits}`; + } + if (isNumber(phase.selectedMaxSizeStored)) { + esPhase.actions.rollover.max_size = `${phase.selectedMaxSizeStored}${phase.selectedMaxSizeStoredUnits}`; + } + if (isNumber(phase.selectedMaxDocuments)) { + esPhase.actions.rollover.max_docs = parseInt(phase.selectedMaxDocuments, 10); + } + } else { + delete esPhase.actions.rollover; + } + + if (isNumber(phase.phaseIndexPriority)) { + esPhase.actions.set_priority = { + priority: parseInt(phase.phaseIndexPriority, 10), + }; + } else { + delete esPhase.actions.set_priority; + } + + return esPhase; +}; + +export const validateHotPhase = (phase: HotPhase): PhaseValidationErrors => { + if (!phase.phaseEnabled) { + return {}; + } + + const phaseErrors = {} as PhaseValidationErrors; + + // index priority is optional, but if it's set, it needs to be a positive number + if (phase.phaseIndexPriority) { + if (!isNumber(phase.phaseIndexPriority)) { + phaseErrors.phaseIndexPriority = [numberRequiredMessage]; + } else if (parseInt(phase.phaseIndexPriority, 10) < 0) { + phaseErrors.phaseIndexPriority = [positiveNumberRequiredMessage]; + } + } + + // if rollover is enabled + if (phase.rolloverEnabled) { + // either max_age, max_size or max_documents need to be set + if ( + !isNumber(phase.selectedMaxAge) && + !isNumber(phase.selectedMaxSizeStored) && + !isNumber(phase.selectedMaxDocuments) + ) { + phaseErrors.selectedMaxAge = [maximumAgeRequiredMessage]; + phaseErrors.selectedMaxSizeStored = [maximumSizeRequiredMessage]; + phaseErrors.selectedMaxDocuments = [maximumDocumentsRequiredMessage]; + } + + // max age, max size and max docs need to be above zero if set + if (isNumber(phase.selectedMaxAge) && parseInt(phase.selectedMaxAge, 10) < 1) { + phaseErrors.selectedMaxAge = [positiveNumbersAboveZeroErrorMessage]; + } + if (isNumber(phase.selectedMaxSizeStored) && parseInt(phase.selectedMaxSizeStored, 10) < 1) { + phaseErrors.selectedMaxSizeStored = [positiveNumbersAboveZeroErrorMessage]; + } + if (isNumber(phase.selectedMaxDocuments) && parseInt(phase.selectedMaxDocuments, 10) < 1) { + phaseErrors.selectedMaxDocuments = [positiveNumbersAboveZeroErrorMessage]; + } + } + + return { + ...phaseErrors, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/lifecycle.js b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts similarity index 58% rename from x-pack/plugins/index_lifecycle_management/public/application/store/actions/lifecycle.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts index 0bb6543482bd68..12df0715449528 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/lifecycle.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts @@ -5,28 +5,36 @@ */ import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; -import { UIM_POLICY_CREATE, UIM_POLICY_UPDATE } from '../../constants'; -import { showApiError } from '../../services/api_errors'; -import { toasts } from '../../services/notification'; -import { savePolicy as savePolicyApi } from '../../services/api'; -import { trackUiMetric, getUiMetricsForPhases } from '../../services/ui_metric'; +import { savePolicy as savePolicyApi } from '../api'; +import { showApiError } from '../api_errors'; +import { getUiMetricsForPhases, trackUiMetric } from '../ui_metric'; +import { UIM_POLICY_CREATE, UIM_POLICY_UPDATE } from '../../constants/ui_metric'; +import { toasts } from '../notification'; +import { Policy, PolicyFromES } from './types'; +import { serializePolicy } from './policy_serialization'; -export const saveLifecyclePolicy = (lifecycle, isNew) => async () => { +export const savePolicy = async ( + policy: Policy, + isNew: boolean, + originalEsPolicy?: PolicyFromES +): Promise => { + const serializedPolicy = serializePolicy(policy, originalEsPolicy?.policy); try { - await savePolicyApi(lifecycle); + await savePolicyApi(serializedPolicy); } catch (err) { const title = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.saveErrorMessage', { defaultMessage: 'Error saving lifecycle policy {lifecycleName}', - values: { lifecycleName: lifecycle.name }, + values: { lifecycleName: policy.name }, }); showApiError(err, title); return false; } - const uiMetrics = getUiMetricsForPhases(lifecycle.phases); + const uiMetrics = getUiMetricsForPhases(serializedPolicy.phases); uiMetrics.push(isNew ? UIM_POLICY_CREATE : UIM_POLICY_UPDATE); - trackUiMetric('count', uiMetrics); + trackUiMetric(METRIC_TYPE.COUNT, uiMetrics); const message = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.successfulSaveMessage', { defaultMessage: '{verb} lifecycle policy "{lifecycleName}"', @@ -38,7 +46,7 @@ export const saveLifecyclePolicy = (lifecycle, isNew) => async () => { : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.updatedMessage', { defaultMessage: 'Updated', }), - lifecycleName: lifecycle.name, + lifecycleName: policy.name, }, }); toasts.addSuccess(message); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts new file mode 100644 index 00000000000000..3953521df18179 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts @@ -0,0 +1,104 @@ +/* + * 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 { + defaultNewColdPhase, + defaultNewDeletePhase, + defaultNewHotPhase, + defaultNewWarmPhase, + serializedPhaseInitialization, +} from '../../constants'; + +import { Policy, PolicyFromES, SerializedPolicy } from './types'; + +import { hotPhaseFromES, hotPhaseToES } from './hot_phase'; +import { warmPhaseFromES, warmPhaseToES } from './warm_phase'; +import { coldPhaseFromES, coldPhaseToES } from './cold_phase'; +import { deletePhaseFromES, deletePhaseToES } from './delete_phase'; + +export const splitSizeAndUnits = (field: string): { size: string; units: string } => { + let size = ''; + let units = ''; + + const result = /(\d+)(\w+)/.exec(field); + if (result) { + size = result[1]; + units = result[2]; + } + + return { + size, + units, + }; +}; + +export const isNumber = (value: any): boolean => value !== '' && value !== null && isFinite(value); + +export const getPolicyByName = ( + policies: PolicyFromES[] | null | undefined, + policyName: string = '' +): PolicyFromES | undefined => { + if (policies && policies.length > 0) { + return policies.find((policy: PolicyFromES) => policy.name === policyName); + } +}; + +export const initializeNewPolicy = (newPolicyName: string = ''): Policy => { + return { + name: newPolicyName, + phases: { + hot: { ...defaultNewHotPhase }, + warm: { ...defaultNewWarmPhase }, + cold: { ...defaultNewColdPhase }, + delete: { ...defaultNewDeletePhase }, + }, + }; +}; + +export const deserializePolicy = (policy: PolicyFromES): Policy => { + const { + name, + policy: { phases }, + } = policy; + + return { + name, + phases: { + hot: hotPhaseFromES(phases.hot), + warm: warmPhaseFromES(phases.warm), + cold: coldPhaseFromES(phases.cold), + delete: deletePhaseFromES(phases.delete), + }, + }; +}; + +export const serializePolicy = ( + policy: Policy, + originalEsPolicy: SerializedPolicy = { + name: policy.name, + phases: { hot: { ...serializedPhaseInitialization } }, + } +): SerializedPolicy => { + const serializedPolicy = { + name: policy.name, + phases: { hot: hotPhaseToES(policy.phases.hot, originalEsPolicy.phases.hot) }, + } as SerializedPolicy; + if (policy.phases.warm.phaseEnabled) { + serializedPolicy.phases.warm = warmPhaseToES(policy.phases.warm, originalEsPolicy.phases.warm); + } + + if (policy.phases.cold.phaseEnabled) { + serializedPolicy.phases.cold = coldPhaseToES(policy.phases.cold, originalEsPolicy.phases.cold); + } + + if (policy.phases.delete.phaseEnabled) { + serializedPolicy.phases.delete = deletePhaseToES( + policy.phases.delete, + originalEsPolicy.phases.delete + ); + } + return serializedPolicy; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts new file mode 100644 index 00000000000000..545488be2cd5e0 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts @@ -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 { i18n } from '@kbn/i18n'; +import { validateHotPhase } from './hot_phase'; +import { validateWarmPhase } from './warm_phase'; +import { validateColdPhase } from './cold_phase'; +import { validateDeletePhase } from './delete_phase'; +import { ColdPhase, DeletePhase, HotPhase, Phase, Policy, PolicyFromES, WarmPhase } from './types'; + +export const propertyof = (propertyName: keyof T & string) => propertyName; + +export const numberRequiredMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.numberRequiredError', + { + defaultMessage: 'A number is required.', + } +); + +// TODO validation includes 0 -> should be non-negative number? +export const positiveNumberRequiredMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.positiveNumberRequiredError', + { + defaultMessage: 'Only positive numbers are allowed.', + } +); + +export const maximumAgeRequiredMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.maximumAgeMissingError', + { + defaultMessage: 'A maximum age is required.', + } +); + +export const maximumSizeRequiredMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError', + { + defaultMessage: 'A maximum index size is required.', + } +); + +export const maximumDocumentsRequiredMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.maximumDocumentsMissingError', + { + defaultMessage: 'Maximum documents is required.', + } +); + +export const positiveNumbersAboveZeroErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.positiveNumberAboveZeroRequiredError', + { + defaultMessage: 'Only numbers above 0 are allowed.', + } +); + +export const policyNameRequiredMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.policyNameRequiredError', + { + defaultMessage: 'A policy name is required.', + } +); + +export const policyNameStartsWithUnderscoreErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.policyNameStartsWithUnderscoreError', + { + defaultMessage: 'A policy name cannot start with an underscore.', + } +); +export const policyNameContainsCommaErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.policyNameContainsCommaError', + { + defaultMessage: 'A policy name cannot include a comma.', + } +); +export const policyNameContainsSpaceErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.policyNameContainsSpaceError', + { + defaultMessage: 'A policy name cannot include a space.', + } +); + +export const policyNameTooLongErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.policyNameTooLongError', + { + defaultMessage: 'A policy name cannot be longer than 255 bytes.', + } +); +export const policyNameMustBeDifferentErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.differentPolicyNameRequiredError', + { + defaultMessage: 'The policy name must be different.', + } +); +export const policyNameAlreadyUsedErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError', + { + defaultMessage: 'That policy name is already used.', + } +); +export type PhaseValidationErrors = { + [P in keyof Partial]: string[]; +}; + +export interface ValidationErrors { + hot: PhaseValidationErrors; + warm: PhaseValidationErrors; + cold: PhaseValidationErrors; + delete: PhaseValidationErrors; + policyName: string[]; +} + +export const validatePolicy = ( + saveAsNew: boolean, + policy: Policy, + policies: PolicyFromES[], + originalPolicyName: string +): [boolean, ValidationErrors] => { + const policyNameErrors: string[] = []; + if (!policy.name) { + policyNameErrors.push(policyNameRequiredMessage); + } else { + if (policy.name.startsWith('_')) { + policyNameErrors.push(policyNameStartsWithUnderscoreErrorMessage); + } + if (policy.name.includes(',')) { + policyNameErrors.push(policyNameContainsCommaErrorMessage); + } + if (policy.name.includes(' ')) { + policyNameErrors.push(policyNameContainsSpaceErrorMessage); + } + if (window.TextEncoder && new window.TextEncoder().encode(policy.name).length > 255) { + policyNameErrors.push(policyNameTooLongErrorMessage); + } + + if (saveAsNew && policy.name === originalPolicyName) { + policyNameErrors.push(policyNameMustBeDifferentErrorMessage); + } else if (policy.name !== originalPolicyName) { + const policyNames = policies.map((existingPolicy) => existingPolicy.name); + if (policyNames.includes(policy.name)) { + policyNameErrors.push(policyNameAlreadyUsedErrorMessage); + } + } + } + + const hotPhaseErrors = validateHotPhase(policy.phases.hot); + const warmPhaseErrors = validateWarmPhase(policy.phases.warm); + const coldPhaseErrors = validateColdPhase(policy.phases.cold); + const deletePhaseErrors = validateDeletePhase(policy.phases.delete); + const isValid = + policyNameErrors.length === 0 && + Object.keys(hotPhaseErrors).length === 0 && + Object.keys(warmPhaseErrors).length === 0 && + Object.keys(coldPhaseErrors).length === 0 && + Object.keys(deletePhaseErrors).length === 0; + return [ + isValid, + { + policyName: [...policyNameErrors], + hot: hotPhaseErrors, + warm: warmPhaseErrors, + cold: coldPhaseErrors, + delete: deletePhaseErrors, + }, + ]; +}; + +export const findFirstError = (errors?: ValidationErrors): string | undefined => { + if (!errors) { + return; + } + + if (errors.policyName.length > 0) { + return propertyof('policyName'); + } + + if (Object.keys(errors.hot).length > 0) { + return `${propertyof('hot')}.${Object.keys(errors.hot)[0]}`; + } + if (Object.keys(errors.warm).length > 0) { + return `${propertyof('warm')}.${Object.keys(errors.warm)[0]}`; + } + if (Object.keys(errors.cold).length > 0) { + return `${propertyof('cold')}.${Object.keys(errors.cold)[0]}`; + } + if (Object.keys(errors.delete).length > 0) { + return `${propertyof('delete')}.${Object.keys(errors.delete)[0]}`; + } +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts new file mode 100644 index 00000000000000..2e2ed5b38bb871 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface SerializedPolicy { + name: string; + phases: Phases; +} + +export interface Phases { + hot?: SerializedHotPhase; + warm?: SerializedWarmPhase; + cold?: SerializedColdPhase; + delete?: SerializedDeletePhase; +} + +export interface PolicyFromES { + modified_date: string; + name: string; + policy: SerializedPolicy; + version: number; +} + +export interface SerializedPhase { + min_age: string; + actions: { + [action: string]: any; + }; +} + +export interface SerializedHotPhase extends SerializedPhase { + actions: { + rollover?: { + max_size?: string; + max_age?: string; + max_docs?: number; + }; + set_priority?: { + priority: number | null; + }; + }; +} + +export interface SerializedWarmPhase extends SerializedPhase { + actions: { + allocate?: AllocateAction; + shrink?: { + number_of_shards: number; + }; + forcemerge?: { + max_num_segments: number; + }; + set_priority?: { + priority: number | null; + }; + }; +} + +export interface SerializedColdPhase extends SerializedPhase { + actions: { + freeze?: {}; + allocate?: AllocateAction; + set_priority?: { + priority: number | null; + }; + }; +} + +export interface SerializedDeletePhase extends SerializedPhase { + actions: { + wait_for_snapshot?: { + policy: string; + }; + delete?: { + delete_searchable_snapshot: boolean; + }; + }; +} + +export interface AllocateAction { + number_of_replicas: number; + include: {}; + exclude: {}; + require: { + [attribute: string]: string; + }; +} + +export interface Policy { + name: string; + phases: { + hot: HotPhase; + warm: WarmPhase; + cold: ColdPhase; + delete: DeletePhase; + }; +} + +export interface Phase { + phaseEnabled: boolean; +} +export interface HotPhase extends Phase { + rolloverEnabled: boolean; + selectedMaxSizeStored: string; + selectedMaxSizeStoredUnits: string; + selectedMaxDocuments: string; + selectedMaxAge: string; + selectedMaxAgeUnits: string; + phaseIndexPriority: string; +} + +export interface WarmPhase extends Phase { + warmPhaseOnRollover: boolean; + selectedMinimumAge: string; + selectedMinimumAgeUnits: string; + selectedNodeAttrs: string; + selectedReplicaCount: string; + shrinkEnabled: boolean; + selectedPrimaryShardCount: string; + forceMergeEnabled: boolean; + selectedForceMergeSegments: string; + phaseIndexPriority: string; +} + +export interface ColdPhase extends Phase { + selectedMinimumAge: string; + selectedMinimumAgeUnits: string; + selectedNodeAttrs: string; + selectedReplicaCount: string; + freezeEnabled: boolean; + phaseIndexPriority: string; +} + +export interface DeletePhase extends Phase { + selectedMinimumAge: string; + selectedMinimumAgeUnits: string; + waitForSnapshotPolicy: string; +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts new file mode 100644 index 00000000000000..3ca1a1cc83371d --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts @@ -0,0 +1,219 @@ +/* + * 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 { isEmpty } from 'lodash'; +import { serializedPhaseInitialization } from '../../constants'; +import { AllocateAction, WarmPhase, SerializedWarmPhase } from './types'; +import { isNumber, splitSizeAndUnits } from './policy_serialization'; + +import { + numberRequiredMessage, + PhaseValidationErrors, + positiveNumberRequiredMessage, + positiveNumbersAboveZeroErrorMessage, +} from './policy_validation'; + +const warmPhaseInitialization: WarmPhase = { + phaseEnabled: false, + warmPhaseOnRollover: false, + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + selectedNodeAttrs: '', + selectedReplicaCount: '', + shrinkEnabled: false, + selectedPrimaryShardCount: '', + forceMergeEnabled: false, + selectedForceMergeSegments: '', + phaseIndexPriority: '', +}; + +export const warmPhaseFromES = (phaseSerialized?: SerializedWarmPhase): WarmPhase => { + const phase: WarmPhase = { ...warmPhaseInitialization }; + + if (phaseSerialized === undefined || phaseSerialized === null) { + return phase; + } + + phase.phaseEnabled = true; + + if (phaseSerialized.min_age) { + if (phaseSerialized.min_age === '0ms') { + phase.warmPhaseOnRollover = true; + } else { + const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); + phase.selectedMinimumAge = minAge; + phase.selectedMinimumAgeUnits = minAgeUnits; + } + } + if (phaseSerialized.actions) { + const actions = phaseSerialized.actions; + if (actions.allocate) { + const allocate = actions.allocate; + if (allocate.require) { + Object.entries(allocate.require).forEach((entry) => { + phase.selectedNodeAttrs = entry.join(':'); + }); + if (allocate.number_of_replicas) { + phase.selectedReplicaCount = allocate.number_of_replicas.toString(); + } + } + } + + if (actions.forcemerge) { + const forcemerge = actions.forcemerge; + phase.forceMergeEnabled = true; + phase.selectedForceMergeSegments = forcemerge.max_num_segments.toString(); + } + + if (actions.shrink) { + phase.shrinkEnabled = true; + phase.selectedPrimaryShardCount = actions.shrink.number_of_shards + ? actions.shrink.number_of_shards.toString() + : ''; + } + } + return phase; +}; + +export const warmPhaseToES = ( + phase: WarmPhase, + originalEsPhase?: SerializedWarmPhase +): SerializedWarmPhase => { + if (!originalEsPhase) { + originalEsPhase = { ...serializedPhaseInitialization }; + } + + const esPhase = { ...originalEsPhase }; + + if (isNumber(phase.selectedMinimumAge)) { + esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; + } + + // If warm phase on rollover is enabled, delete min age field + // An index lifecycle switches to warm phase when rollover occurs, so you cannot specify a warm phase time + // They are mutually exclusive + if (phase.warmPhaseOnRollover) { + delete esPhase.min_age; + } + + esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; + + if (phase.selectedNodeAttrs) { + const [name, value] = phase.selectedNodeAttrs.split(':'); + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.require = { + [name]: value, + }; + } else { + if (esPhase.actions.allocate) { + delete esPhase.actions.allocate.require; + } + } + + if (isNumber(phase.selectedReplicaCount)) { + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.number_of_replicas = parseInt(phase.selectedReplicaCount, 10); + } else { + if (esPhase.actions.allocate) { + delete esPhase.actions.allocate.number_of_replicas; + } + } + + if ( + esPhase.actions.allocate && + !esPhase.actions.allocate.require && + !isNumber(esPhase.actions.allocate.number_of_replicas) && + isEmpty(esPhase.actions.allocate.include) && + isEmpty(esPhase.actions.allocate.exclude) + ) { + // remove allocate action if it does not define require or number of nodes + // and both include and exclude are empty objects (ES will fail to parse if we don't) + delete esPhase.actions.allocate; + } + + if (phase.forceMergeEnabled) { + esPhase.actions.forcemerge = { + max_num_segments: parseInt(phase.selectedForceMergeSegments, 10), + }; + } else { + delete esPhase.actions.forcemerge; + } + + if (phase.shrinkEnabled && isNumber(phase.selectedPrimaryShardCount)) { + esPhase.actions.shrink = { + number_of_shards: parseInt(phase.selectedPrimaryShardCount, 10), + }; + } else { + delete esPhase.actions.shrink; + } + + if (isNumber(phase.phaseIndexPriority)) { + esPhase.actions.set_priority = { + priority: parseInt(phase.phaseIndexPriority, 10), + }; + } else { + delete esPhase.actions.set_priority; + } + + return esPhase; +}; + +export const validateWarmPhase = (phase: WarmPhase): PhaseValidationErrors => { + if (!phase.phaseEnabled) { + return {}; + } + + const phaseErrors = {} as PhaseValidationErrors; + + // index priority is optional, but if it's set, it needs to be a positive number + if (phase.phaseIndexPriority) { + if (!isNumber(phase.phaseIndexPriority)) { + phaseErrors.phaseIndexPriority = [numberRequiredMessage]; + } else if (parseInt(phase.phaseIndexPriority, 10) < 0) { + phaseErrors.phaseIndexPriority = [positiveNumberRequiredMessage]; + } + } + + // if warm phase on rollover is disabled, min age needs to be a positive number + if (!phase.warmPhaseOnRollover) { + if (!isNumber(phase.selectedMinimumAge)) { + phaseErrors.selectedMinimumAge = [numberRequiredMessage]; + } else if (parseInt(phase.selectedMinimumAge, 10) < 0) { + phaseErrors.selectedMinimumAge = [positiveNumberRequiredMessage]; + } + } + + // if forcemerge is enabled, force merge segments needs to be a number above zero + if (phase.forceMergeEnabled) { + if (!isNumber(phase.selectedForceMergeSegments)) { + phaseErrors.selectedForceMergeSegments = [numberRequiredMessage]; + } else if (parseInt(phase.selectedForceMergeSegments, 10) < 1) { + phaseErrors.selectedForceMergeSegments = [positiveNumbersAboveZeroErrorMessage]; + } + } + + // if shrink is enabled, primary shard count needs to be a number above zero + if (phase.shrinkEnabled) { + if (!isNumber(phase.selectedPrimaryShardCount)) { + phaseErrors.selectedPrimaryShardCount = [numberRequiredMessage]; + } else if (parseInt(phase.selectedPrimaryShardCount, 10) < 1) { + phaseErrors.selectedPrimaryShardCount = [positiveNumbersAboveZeroErrorMessage]; + } + } + + // replica count is optional, but if it's set, it needs to be a positive number + if (phase.selectedReplicaCount) { + if (!isNumber(phase.selectedReplicaCount)) { + phaseErrors.selectedReplicaCount = [numberRequiredMessage]; + } else if (parseInt(phase.selectedReplicaCount, 10) < 0) { + phaseErrors.selectedReplicaCount = [numberRequiredMessage]; + } + } + + return { + ...phaseErrors, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.js b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts similarity index 75% rename from x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts index 99e6bfb99472c3..7c7c0b70c0eedf 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts @@ -5,14 +5,13 @@ */ import { - PHASE_INDEX_PRIORITY, UIM_CONFIG_COLD_PHASE, UIM_CONFIG_WARM_PHASE, UIM_CONFIG_SET_PRIORITY, UIM_CONFIG_FREEZE_INDEX, -} from '../constants'; - -import { defaultColdPhase, defaultWarmPhase } from '../store/defaults'; + defaultNewWarmPhase, + defaultNewColdPhase, +} from '../constants/'; import { getUiMetricsForPhases } from './ui_metric'; jest.mock('ui/new_platform'); @@ -22,9 +21,10 @@ describe('getUiMetricsForPhases', () => { expect( getUiMetricsForPhases({ cold: { + min_age: '0ms', actions: { set_priority: { - priority: defaultColdPhase[PHASE_INDEX_PRIORITY], + priority: parseInt(defaultNewColdPhase.phaseIndexPriority, 10), }, }, }, @@ -36,9 +36,10 @@ describe('getUiMetricsForPhases', () => { expect( getUiMetricsForPhases({ warm: { + min_age: '0ms', actions: { set_priority: { - priority: defaultWarmPhase[PHASE_INDEX_PRIORITY], + priority: parseInt(defaultNewWarmPhase.phaseIndexPriority, 10), }, }, }, @@ -50,9 +51,10 @@ describe('getUiMetricsForPhases', () => { expect( getUiMetricsForPhases({ warm: { + min_age: '0ms', actions: { set_priority: { - priority: defaultWarmPhase[PHASE_INDEX_PRIORITY] + 1, + priority: parseInt(defaultNewWarmPhase.phaseIndexPriority, 10) + 1, }, }, }, @@ -64,10 +66,11 @@ describe('getUiMetricsForPhases', () => { expect( getUiMetricsForPhases({ cold: { + min_age: '0ms', actions: { freeze: {}, set_priority: { - priority: defaultColdPhase[PHASE_INDEX_PRIORITY], + priority: parseInt(defaultNewColdPhase.phaseIndexPriority, 10), }, }, }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts index d71e38d0b31dee..b38a7347705466 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts @@ -4,24 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; - import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { UiStatsMetricType } from '@kbn/analytics'; import { UIM_APP_NAME, UIM_CONFIG_COLD_PHASE, - UIM_CONFIG_WARM_PHASE, - UIM_CONFIG_SET_PRIORITY, UIM_CONFIG_FREEZE_INDEX, - PHASE_HOT, - PHASE_WARM, - PHASE_COLD, - PHASE_INDEX_PRIORITY, + UIM_CONFIG_SET_PRIORITY, + UIM_CONFIG_WARM_PHASE, + defaultNewColdPhase, + defaultNewHotPhase, + defaultNewWarmPhase, } from '../constants'; -import { defaultColdPhase, defaultWarmPhase, defaultHotPhase } from '../store/defaults'; +import { Phases } from './policies/types'; export let trackUiMetric = (metricType: UiStatsMetricType, eventName: string) => {}; @@ -31,49 +28,54 @@ export function init(usageCollection?: UsageCollectionSetup): void { } } -export function getUiMetricsForPhases(phases: any): any { +export function getUiMetricsForPhases(phases: Phases): any { const phaseUiMetrics = [ { metric: UIM_CONFIG_COLD_PHASE, - isTracked: () => Boolean(phases[PHASE_COLD]), + isTracked: () => Boolean(phases.cold), }, { metric: UIM_CONFIG_WARM_PHASE, - isTracked: () => Boolean(phases[PHASE_WARM]), + isTracked: () => Boolean(phases.warm), }, { metric: UIM_CONFIG_SET_PRIORITY, isTracked: () => { - const phaseToDefaultIndexPriorityMap = { - [PHASE_HOT]: defaultHotPhase[PHASE_INDEX_PRIORITY], - [PHASE_WARM]: defaultWarmPhase[PHASE_INDEX_PRIORITY], - [PHASE_COLD]: defaultColdPhase[PHASE_INDEX_PRIORITY], - }; - // We only care about whether the user has interacted with the priority of *any* phase at all. - return [PHASE_HOT, PHASE_WARM, PHASE_COLD].some((phase) => { - // If the priority is different than the default, we'll consider it a user interaction, - // even if the user has set it to undefined. - return ( - phases[phase] && - get(phases[phase], 'actions.set_priority.priority') !== - phaseToDefaultIndexPriorityMap[phase] - ); - }); + const isHotPhasePriorityChanged = + phases.hot && + phases.hot.actions.set_priority && + phases.hot.actions.set_priority.priority !== + parseInt(defaultNewHotPhase.phaseIndexPriority, 10); + + const isWarmPhasePriorityChanged = + phases.warm && + phases.warm.actions.set_priority && + phases.warm.actions.set_priority.priority !== + parseInt(defaultNewWarmPhase.phaseIndexPriority, 10); + + const isColdPhasePriorityChanged = + phases.cold && + phases.cold.actions.set_priority && + phases.cold.actions.set_priority.priority !== + parseInt(defaultNewColdPhase.phaseIndexPriority, 10); + // If the priority is different than the default, we'll consider it a user interaction, + // even if the user has set it to undefined. + return ( + isHotPhasePriorityChanged || isWarmPhasePriorityChanged || isColdPhasePriorityChanged + ); }, }, { metric: UIM_CONFIG_FREEZE_INDEX, - isTracked: () => phases[PHASE_COLD] && get(phases[PHASE_COLD], 'actions.freeze'), + isTracked: () => phases.cold && phases.cold.actions.freeze, }, ]; - const trackedUiMetrics = phaseUiMetrics.reduce((tracked: any, { metric, isTracked }) => { + return phaseUiMetrics.reduce((tracked: any, { metric, isTracked }) => { if (isTracked()) { tracked.push(metric); } return tracked; }, []); - - return trackedUiMetrics; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/general.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/general.js deleted file mode 100644 index 28719fde87b0c4..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/general.js +++ /dev/null @@ -1,11 +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 { createAction } from 'redux-actions'; - -export const setBootstrapEnabled = createAction('SET_BOOTSTRAP_ENABLED'); -export const setIndexName = createAction('SET_INDEX_NAME'); -export const setAliasName = createAction('SET_ALIAS_NAME'); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js index ea539578c885c6..fef79c7782bb05 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './nodes'; export * from './policies'; -export * from './lifecycle'; -export * from './general'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js deleted file mode 100644 index 45a8e63f70e835..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js +++ /dev/null @@ -1,8 +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 { createAction } from 'redux-actions'; -export const setSelectedPrimaryShardCount = createAction('SET_SELECTED_PRIMARY_SHARED_COUNT'); -export const setSelectedReplicaCount = createAction('SET_SELECTED_REPLICA_COUNT'); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js index aa20c0eb1d3262..d47136679604fd 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js @@ -9,7 +9,6 @@ import { createAction } from 'redux-actions'; import { showApiError } from '../../services/api_errors'; import { loadPolicies } from '../../services/api'; -import { SET_PHASE_DATA } from '../../constants'; export const fetchedPolicies = createAction('FETCHED_POLICIES'); export const setSelectedPolicy = createAction('SET_SELECTED_POLICY'); @@ -41,9 +40,3 @@ export const fetchPolicies = (withIndices, callback) => async (dispatch) => { callback && callback(); return policies; }; - -export const setPhaseData = createAction(SET_PHASE_DATA, (phase, key, value) => ({ - phase, - key, - value, -})); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/cold_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/cold_phase.js deleted file mode 100644 index a8f7fd3f4bdfa9..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/cold_phase.js +++ /dev/null @@ -1,30 +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 { - PHASE_ENABLED, - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_NODE_ATTRS, - PHASE_REPLICA_COUNT, - PHASE_ROLLOVER_MINIMUM_AGE_UNITS, - PHASE_ROLLOVER_ALIAS, - PHASE_FREEZE_ENABLED, - PHASE_INDEX_PRIORITY, -} from '../../constants'; - -export const defaultColdPhase = { - [PHASE_ENABLED]: false, - [PHASE_ROLLOVER_ALIAS]: '', - [PHASE_ROLLOVER_MINIMUM_AGE]: 0, - [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: 'd', - [PHASE_NODE_ATTRS]: '', - [PHASE_REPLICA_COUNT]: '', - [PHASE_FREEZE_ENABLED]: false, - [PHASE_INDEX_PRIORITY]: 0, -}; -export const defaultEmptyColdPhase = { - ...defaultColdPhase, - [PHASE_INDEX_PRIORITY]: '', -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/delete_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/delete_phase.js deleted file mode 100644 index 8534893e7e3b38..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/delete_phase.js +++ /dev/null @@ -1,23 +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 { - PHASE_ENABLED, - PHASE_ROLLOVER_ENABLED, - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_ROLLOVER_MINIMUM_AGE_UNITS, - PHASE_ROLLOVER_ALIAS, - PHASE_WAIT_FOR_SNAPSHOT_POLICY, -} from '../../constants'; - -export const defaultDeletePhase = { - [PHASE_ENABLED]: false, - [PHASE_ROLLOVER_ENABLED]: false, - [PHASE_ROLLOVER_ALIAS]: '', - [PHASE_ROLLOVER_MINIMUM_AGE]: 0, - [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: 'd', - [PHASE_WAIT_FOR_SNAPSHOT_POLICY]: '', -}; -export const defaultEmptyDeletePhase = defaultDeletePhase; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/hot_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/hot_phase.js deleted file mode 100644 index 1f5b5c399a642d..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/hot_phase.js +++ /dev/null @@ -1,35 +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 { - PHASE_ENABLED, - PHASE_ROLLOVER_ENABLED, - PHASE_ROLLOVER_MAX_AGE, - PHASE_ROLLOVER_MAX_AGE_UNITS, - PHASE_ROLLOVER_MAX_SIZE_STORED, - PHASE_ROLLOVER_MAX_DOCUMENTS, - PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS, - PHASE_INDEX_PRIORITY, -} from '../../constants'; - -export const defaultHotPhase = { - [PHASE_ENABLED]: true, - [PHASE_ROLLOVER_ENABLED]: true, - [PHASE_ROLLOVER_MAX_AGE]: 30, - [PHASE_ROLLOVER_MAX_AGE_UNITS]: 'd', - [PHASE_ROLLOVER_MAX_SIZE_STORED]: 50, - [PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS]: 'gb', - [PHASE_INDEX_PRIORITY]: 100, - [PHASE_ROLLOVER_MAX_DOCUMENTS]: '', -}; -export const defaultEmptyHotPhase = { - ...defaultHotPhase, - [PHASE_ENABLED]: false, - [PHASE_ROLLOVER_ENABLED]: false, - [PHASE_ROLLOVER_MAX_AGE]: '', - [PHASE_ROLLOVER_MAX_SIZE_STORED]: '', - [PHASE_INDEX_PRIORITY]: '', - [PHASE_ROLLOVER_MAX_DOCUMENTS]: '', -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.d.ts deleted file mode 100644 index abf6db416c7f4a..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.d.ts +++ /dev/null @@ -1,10 +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. - */ - -export declare const defaultDeletePhase: any; -export declare const defaultColdPhase: any; -export declare const defaultWarmPhase: any; -export declare const defaultHotPhase: any; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.js deleted file mode 100644 index f5661eae91a8ca..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.js +++ /dev/null @@ -1,10 +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. - */ - -export * from './delete_phase'; -export * from './cold_phase'; -export * from './hot_phase'; -export * from './warm_phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/warm_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/warm_phase.js deleted file mode 100644 index f02ac2096675fc..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/warm_phase.js +++ /dev/null @@ -1,39 +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 { - PHASE_ENABLED, - PHASE_FORCE_MERGE_SEGMENTS, - PHASE_FORCE_MERGE_ENABLED, - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_NODE_ATTRS, - PHASE_PRIMARY_SHARD_COUNT, - PHASE_REPLICA_COUNT, - PHASE_ROLLOVER_MINIMUM_AGE_UNITS, - PHASE_ROLLOVER_ALIAS, - PHASE_SHRINK_ENABLED, - WARM_PHASE_ON_ROLLOVER, - PHASE_INDEX_PRIORITY, -} from '../../constants'; - -export const defaultWarmPhase = { - [PHASE_ENABLED]: false, - [PHASE_ROLLOVER_ALIAS]: '', - [PHASE_FORCE_MERGE_SEGMENTS]: '', - [PHASE_FORCE_MERGE_ENABLED]: false, - [PHASE_ROLLOVER_MINIMUM_AGE]: 0, - [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: 'd', - [PHASE_NODE_ATTRS]: '', - [PHASE_SHRINK_ENABLED]: false, - [PHASE_PRIMARY_SHARD_COUNT]: '', - [PHASE_REPLICA_COUNT]: '', - [WARM_PHASE_ON_ROLLOVER]: true, - [PHASE_INDEX_PRIORITY]: 50, -}; -export const defaultEmptyWarmPhase = { - ...defaultWarmPhase, - [WARM_PHASE_ON_ROLLOVER]: false, - [PHASE_INDEX_PRIORITY]: '', -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/general.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/general.js deleted file mode 100644 index fcba2fd1358b01..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/general.js +++ /dev/null @@ -1,38 +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 { handleActions } from 'redux-actions'; -import { setIndexName, setAliasName, setBootstrapEnabled } from '../actions/general'; - -const defaultState = { - bootstrapEnabled: false, - indexName: '', - aliasName: '', -}; - -export const general = handleActions( - { - [setIndexName](state, { payload: indexName }) { - return { - ...state, - indexName, - }; - }, - [setAliasName](state, { payload: aliasName }) { - return { - ...state, - aliasName, - }; - }, - [setBootstrapEnabled](state, { payload: bootstrapEnabled }) { - return { - ...state, - bootstrapEnabled, - }; - }, - }, - defaultState -); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js index 60126b85c313e8..7fe7134f5f5db6 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js @@ -5,12 +5,8 @@ */ import { combineReducers } from 'redux'; -import { nodes } from './nodes'; import { policies } from './policies'; -import { general } from './general'; export const indexLifecycleManagement = combineReducers({ - nodes, policies, - general, }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/nodes.js deleted file mode 100644 index 383e61b5aacde2..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/nodes.js +++ /dev/null @@ -1,50 +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 { handleActions } from 'redux-actions'; -import { setSelectedPrimaryShardCount, setSelectedReplicaCount } from '../actions'; - -const defaultState = { - isLoading: false, - selectedNodeAttrs: '', - selectedPrimaryShardCount: 1, - selectedReplicaCount: 1, - nodes: undefined, - details: {}, -}; - -export const nodes = handleActions( - { - [setSelectedPrimaryShardCount](state, { payload }) { - let selectedPrimaryShardCount = parseInt(payload); - if (isNaN(selectedPrimaryShardCount)) { - selectedPrimaryShardCount = ''; - } - return { - ...state, - selectedPrimaryShardCount, - }; - }, - [setSelectedReplicaCount](state, { payload }) { - let selectedReplicaCount; - if (payload != null) { - selectedReplicaCount = parseInt(payload); - if (isNaN(selectedReplicaCount)) { - selectedReplicaCount = ''; - } - } else { - // default value for Elasticsearch - selectedReplicaCount = 1; - } - - return { - ...state, - selectedReplicaCount, - }; - }, - }, - defaultState -); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js index a94e875a71845e..ca9d59e295a29d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js @@ -7,49 +7,17 @@ import { handleActions } from 'redux-actions'; import { fetchedPolicies, - setSelectedPolicy, - unsetSelectedPolicy, - setSelectedPolicyName, - setSaveAsNewPolicy, - setPhaseData, policyFilterChanged, policyPageChanged, policyPageSizeChanged, policySortChanged, } from '../actions'; -import { policyFromES } from '../selectors'; -import { - PHASE_HOT, - PHASE_WARM, - PHASE_COLD, - PHASE_DELETE, - PHASE_ATTRIBUTES_THAT_ARE_NUMBERS, -} from '../../constants'; - -import { - defaultColdPhase, - defaultDeletePhase, - defaultHotPhase, - defaultWarmPhase, -} from '../defaults'; -export const defaultPolicy = { - name: '', - saveAsNew: true, - isNew: true, - phases: { - [PHASE_HOT]: defaultHotPhase, - [PHASE_WARM]: defaultWarmPhase, - [PHASE_COLD]: defaultColdPhase, - [PHASE_DELETE]: defaultDeletePhase, - }, -}; const defaultState = { isLoading: false, isLoaded: false, originalPolicyName: undefined, selectedPolicySet: false, - selectedPolicy: defaultPolicy, policies: [], sort: { sortField: 'name', @@ -70,71 +38,6 @@ export const policies = handleActions( policies, }; }, - [setSelectedPolicy](state, { payload: selectedPolicy }) { - if (!selectedPolicy) { - return { - ...state, - selectedPolicy: defaultPolicy, - selectedPolicySet: true, - }; - } - - return { - ...state, - originalPolicyName: selectedPolicy.name, - selectedPolicySet: true, - selectedPolicy: { - ...defaultPolicy, - ...policyFromES(selectedPolicy), - }, - }; - }, - [unsetSelectedPolicy]() { - return defaultState; - }, - [setSelectedPolicyName](state, { payload: name }) { - return { - ...state, - selectedPolicy: { - ...state.selectedPolicy, - name, - }, - }; - }, - [setSaveAsNewPolicy](state, { payload: saveAsNew }) { - return { - ...state, - selectedPolicy: { - ...state.selectedPolicy, - saveAsNew, - }, - }; - }, - [setPhaseData](state, { payload }) { - const { phase, key } = payload; - - let value = payload.value; - if (PHASE_ATTRIBUTES_THAT_ARE_NUMBERS.includes(key)) { - value = parseInt(value); - if (isNaN(value)) { - value = ''; - } - } - - return { - ...state, - selectedPolicy: { - ...state.selectedPolicy, - phases: { - ...state.selectedPolicy.phases, - [phase]: { - ...state.selectedPolicy.phases[phase], - [key]: value, - }, - }, - }, - }; - }, [policyFilterChanged](state, action) { const { filter } = action.payload; return { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/general.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/general.js deleted file mode 100644 index 2d01749be30873..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/general.js +++ /dev/null @@ -1,9 +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. - */ - -export const getBootstrapEnabled = (state) => state.general.bootstrapEnabled; -export const getIndexName = (state) => state.general.indexName; -export const getAliasName = (state) => state.general.aliasName; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js index ea539578c885c6..fef79c7782bb05 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './nodes'; export * from './policies'; -export * from './lifecycle'; -export * from './general'; 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 deleted file mode 100644 index 03538fad9aa83a..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js +++ /dev/null @@ -1,287 +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 { i18n } from '@kbn/i18n'; - -import { - PHASE_HOT, - PHASE_WARM, - PHASE_COLD, - PHASE_DELETE, - PHASE_ENABLED, - PHASE_ROLLOVER_ENABLED, - PHASE_ROLLOVER_MAX_AGE, - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_ROLLOVER_MAX_SIZE_STORED, - STRUCTURE_POLICY_NAME, - ERROR_STRUCTURE, - PHASE_ATTRIBUTES_THAT_ARE_NUMBERS_VALIDATE, - PHASE_PRIMARY_SHARD_COUNT, - PHASE_SHRINK_ENABLED, - PHASE_FORCE_MERGE_ENABLED, - PHASE_FORCE_MERGE_SEGMENTS, - PHASE_REPLICA_COUNT, - WARM_PHASE_ON_ROLLOVER, - PHASE_INDEX_PRIORITY, - PHASE_ROLLOVER_MAX_DOCUMENTS, -} from '../../constants'; - -import { - getPhase, - getPhases, - phaseToES, - getSelectedPolicyName, - isNumber, - getSaveAsNewPolicy, - getSelectedOriginalPolicyName, - getPolicies, -} from '.'; - -import { getPolicyByName } from './policies'; - -export const numberRequiredMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.numberRequiredError', - { - defaultMessage: 'A number is required.', - } -); - -export const positiveNumberRequiredMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.positiveNumberRequiredError', - { - defaultMessage: 'Only positive numbers are allowed.', - } -); - -export const maximumAgeRequiredMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.maximumAgeMissingError', - { - defaultMessage: 'A maximum age is required.', - } -); - -export const maximumSizeRequiredMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError', - { - defaultMessage: 'A maximum index size is required.', - } -); - -export const maximumDocumentsRequiredMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.maximumDocumentsMissingError', - { - defaultMessage: 'Maximum documents is required.', - } -); - -export const positiveNumbersAboveZeroErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.positiveNumberAboveZeroRequiredError', - { - defaultMessage: 'Only numbers above 0 are allowed.', - } -); - -export const validatePhase = (type, phase, errors) => { - const phaseErrors = {}; - - if (!phase[PHASE_ENABLED]) { - return; - } - - for (const numberedAttribute of PHASE_ATTRIBUTES_THAT_ARE_NUMBERS_VALIDATE) { - if (phase.hasOwnProperty(numberedAttribute)) { - // If WARM_PHASE_ON_ROLLOVER or PHASE_HOT there is no need to validate this - if ( - numberedAttribute === PHASE_ROLLOVER_MINIMUM_AGE && - (phase[WARM_PHASE_ON_ROLLOVER] || type === PHASE_HOT) - ) { - continue; - } - // If shrink is disabled, there is no need to validate this - if (numberedAttribute === PHASE_PRIMARY_SHARD_COUNT && !phase[PHASE_SHRINK_ENABLED]) { - continue; - } - // If forcemerge is disabled, there is no need to validate this - if (numberedAttribute === PHASE_FORCE_MERGE_SEGMENTS && !phase[PHASE_FORCE_MERGE_ENABLED]) { - continue; - } - // PHASE_REPLICA_COUNT is optional and can be zero - if (numberedAttribute === PHASE_REPLICA_COUNT && !phase[numberedAttribute]) { - continue; - } - // PHASE_INDEX_PRIORITY is optional and can be zero - if (numberedAttribute === PHASE_INDEX_PRIORITY && !phase[numberedAttribute]) { - continue; - } - if (!isNumber(phase[numberedAttribute])) { - phaseErrors[numberedAttribute] = [numberRequiredMessage]; - } else if (phase[numberedAttribute] < 0) { - phaseErrors[numberedAttribute] = [positiveNumberRequiredMessage]; - } - } - } - if (phase[PHASE_ROLLOVER_ENABLED]) { - if ( - !isNumber(phase[PHASE_ROLLOVER_MAX_AGE]) && - !isNumber(phase[PHASE_ROLLOVER_MAX_SIZE_STORED]) && - !isNumber(phase[PHASE_ROLLOVER_MAX_DOCUMENTS]) - ) { - phaseErrors[PHASE_ROLLOVER_MAX_AGE] = [maximumAgeRequiredMessage]; - phaseErrors[PHASE_ROLLOVER_MAX_SIZE_STORED] = [maximumSizeRequiredMessage]; - phaseErrors[PHASE_ROLLOVER_MAX_DOCUMENTS] = [maximumDocumentsRequiredMessage]; - } - if (isNumber(phase[PHASE_ROLLOVER_MAX_AGE]) && phase[PHASE_ROLLOVER_MAX_AGE] < 1) { - phaseErrors[PHASE_ROLLOVER_MAX_AGE] = [positiveNumbersAboveZeroErrorMessage]; - } - if ( - isNumber(phase[PHASE_ROLLOVER_MAX_SIZE_STORED]) && - phase[PHASE_ROLLOVER_MAX_SIZE_STORED] < 1 - ) { - phaseErrors[PHASE_ROLLOVER_MAX_SIZE_STORED] = [positiveNumbersAboveZeroErrorMessage]; - } - if (isNumber(phase[PHASE_ROLLOVER_MAX_DOCUMENTS]) && phase[PHASE_ROLLOVER_MAX_DOCUMENTS] < 1) { - phaseErrors[PHASE_ROLLOVER_MAX_DOCUMENTS] = [positiveNumbersAboveZeroErrorMessage]; - } - } - if (phase[PHASE_SHRINK_ENABLED]) { - if (!isNumber(phase[PHASE_PRIMARY_SHARD_COUNT])) { - phaseErrors[PHASE_PRIMARY_SHARD_COUNT] = [numberRequiredMessage]; - } else if (phase[PHASE_PRIMARY_SHARD_COUNT] < 1) { - phaseErrors[PHASE_PRIMARY_SHARD_COUNT] = [positiveNumbersAboveZeroErrorMessage]; - } - } - - if (phase[PHASE_FORCE_MERGE_ENABLED]) { - if (!isNumber(phase[PHASE_FORCE_MERGE_SEGMENTS])) { - phaseErrors[PHASE_FORCE_MERGE_SEGMENTS] = [numberRequiredMessage]; - } else if (phase[PHASE_FORCE_MERGE_SEGMENTS] < 1) { - phaseErrors[PHASE_FORCE_MERGE_SEGMENTS] = [positiveNumbersAboveZeroErrorMessage]; - } - } - errors[type] = { - ...errors[type], - ...phaseErrors, - }; -}; - -export const policyNameRequiredMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.policyNameRequiredError', - { - defaultMessage: 'A policy name is required.', - } -); -export const policyNameStartsWithUnderscoreErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.policyNameStartsWithUnderscoreError', - { - defaultMessage: 'A policy name cannot start with an underscore.', - } -); -export const policyNameContainsCommaErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.policyNameContainsCommaError', - { - defaultMessage: 'A policy name cannot include a comma.', - } -); -export const policyNameContainsSpaceErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.policyNameContainsSpaceError', - { - defaultMessage: 'A policy name cannot include a space.', - } -); -export const policyNameTooLongErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.policyNameTooLongError', - { - defaultMessage: 'A policy name cannot be longer than 255 bytes.', - } -); -export const policyNameMustBeDifferentErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.differentPolicyNameRequiredError', - { - defaultMessage: 'The policy name must be different.', - } -); -export const policyNameAlreadyUsedErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError', - { - defaultMessage: 'That policy name is already used.', - } -); -export const validateLifecycle = (state) => { - // This method of deep copy does not always work but it should be fine here - const errors = JSON.parse(JSON.stringify(ERROR_STRUCTURE)); - const policyName = getSelectedPolicyName(state); - if (!policyName) { - errors[STRUCTURE_POLICY_NAME].push(policyNameRequiredMessage); - } else { - if (policyName.startsWith('_')) { - errors[STRUCTURE_POLICY_NAME].push(policyNameStartsWithUnderscoreErrorMessage); - } - if (policyName.includes(',')) { - errors[STRUCTURE_POLICY_NAME].push(policyNameContainsCommaErrorMessage); - } - if (policyName.includes(' ')) { - errors[STRUCTURE_POLICY_NAME].push(policyNameContainsSpaceErrorMessage); - } - if (window.TextEncoder && new window.TextEncoder('utf-8').encode(policyName).length > 255) { - errors[STRUCTURE_POLICY_NAME].push(policyNameTooLongErrorMessage); - } - } - - if ( - getSaveAsNewPolicy(state) && - getSelectedOriginalPolicyName(state) === getSelectedPolicyName(state) - ) { - errors[STRUCTURE_POLICY_NAME].push(policyNameMustBeDifferentErrorMessage); - } else if (getSelectedOriginalPolicyName(state) !== getSelectedPolicyName(state)) { - const policyNames = getPolicies(state).map((policy) => policy.name); - if (policyNames.includes(getSelectedPolicyName(state))) { - errors[STRUCTURE_POLICY_NAME].push(policyNameAlreadyUsedErrorMessage); - } - } - - const hotPhase = getPhase(state, PHASE_HOT); - const warmPhase = getPhase(state, PHASE_WARM); - const coldPhase = getPhase(state, PHASE_COLD); - const deletePhase = getPhase(state, PHASE_DELETE); - - validatePhase(PHASE_HOT, hotPhase, errors); - validatePhase(PHASE_WARM, warmPhase, errors); - validatePhase(PHASE_COLD, coldPhase, errors); - validatePhase(PHASE_DELETE, deletePhase, errors); - return errors; -}; - -export const getLifecycle = (state) => { - const policyName = getSelectedPolicyName(state); - const phases = Object.entries(getPhases(state)).reduce((accum, [phaseName, phase]) => { - // Hot is ALWAYS enabled - if (phaseName === PHASE_HOT) { - phase[PHASE_ENABLED] = true; - } - const esPolicy = getPolicyByName(state, policyName).policy || {}; - const esPhase = esPolicy.phases ? esPolicy.phases[phaseName] : {}; - if (phase[PHASE_ENABLED]) { - accum[phaseName] = phaseToES(phase, esPhase); - - // These seem to be constants - if (phaseName === PHASE_DELETE) { - accum[phaseName].actions = { - ...accum[phaseName].actions, - delete: { - ...accum[phaseName].actions.delete, - }, - }; - } - } - return accum; - }, {}); - - return { - name: getSelectedPolicyName(state), - //type, TODO: figure this out (jsut store it and not let the user change it?) - phases, - }; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/nodes.js deleted file mode 100644 index 72bfd4b15a78ad..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/nodes.js +++ /dev/null @@ -1,12 +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. - */ - -export const getNodes = (state) => state.nodes.nodes; - -export const getSelectedPrimaryShardCount = (state) => state.nodes.selectedPrimaryShardCount; - -export const getSelectedReplicaCount = (state) => - state.nodes.selectedReplicaCount !== undefined ? state.nodes.selectedReplicaCount : 1; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js index 5bea22f0b3a76b..e1c89314a2ec56 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js @@ -7,49 +7,9 @@ import { createSelector } from 'reselect'; import { Pager } from '@elastic/eui'; -import { - PHASE_HOT, - PHASE_WARM, - PHASE_COLD, - PHASE_DELETE, - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_ROLLOVER_MINIMUM_AGE_UNITS, - PHASE_ROLLOVER_ENABLED, - PHASE_ROLLOVER_MAX_AGE, - PHASE_ROLLOVER_MAX_AGE_UNITS, - PHASE_ROLLOVER_MAX_SIZE_STORED, - PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS, - PHASE_NODE_ATTRS, - PHASE_FORCE_MERGE_ENABLED, - PHASE_FORCE_MERGE_SEGMENTS, - PHASE_PRIMARY_SHARD_COUNT, - PHASE_REPLICA_COUNT, - PHASE_ENABLED, - PHASE_ATTRIBUTES_THAT_ARE_NUMBERS, - WARM_PHASE_ON_ROLLOVER, - PHASE_SHRINK_ENABLED, - PHASE_FREEZE_ENABLED, - PHASE_INDEX_PRIORITY, - PHASE_ROLLOVER_MAX_DOCUMENTS, - PHASE_WAIT_FOR_SNAPSHOT_POLICY, -} from '../../constants'; - import { filterItems, sortTable } from '../../services'; -import { - defaultEmptyDeletePhase, - defaultEmptyColdPhase, - defaultEmptyWarmPhase, - defaultEmptyHotPhase, -} from '../defaults'; - export const getPolicies = (state) => state.policies.policies; -export const getPolicyByName = (state, name) => - getPolicies(state).find((policy) => policy.name === name) || {}; -export const getIsNewPolicy = (state) => state.policies.selectedPolicy.isNew; -export const getSelectedPolicy = (state) => state.policies.selectedPolicy; -export const getIsSelectedPolicySet = (state) => state.policies.selectedPolicySet; -export const getSelectedOriginalPolicyName = (state) => state.policies.originalPolicyName; export const getPolicyFilter = (state) => state.policies.filter; export const getPolicySort = (state) => state.policies.sort; export const getPolicyCurrentPage = (state) => state.policies.currentPage; @@ -77,255 +37,6 @@ export const getPageOfPolicies = createSelector( (filteredPolicies, sort, pager) => { const sortedPolicies = sortTable(filteredPolicies, sort.sortField, sort.isSortAscending); const { firstItemIndex, lastItemIndex } = pager; - const pagedPolicies = sortedPolicies.slice(firstItemIndex, lastItemIndex + 1); - return pagedPolicies; + return sortedPolicies.slice(firstItemIndex, lastItemIndex + 1); } ); -export const getSaveAsNewPolicy = (state) => state.policies.selectedPolicy.saveAsNew; - -export const getSelectedPolicyName = (state) => { - if (!getSaveAsNewPolicy(state)) { - return getSelectedOriginalPolicyName(state); - } - return state.policies.selectedPolicy.name; -}; - -export const getPhases = (state) => state.policies.selectedPolicy.phases; - -export const getPhase = (state, phase) => getPhases(state)[phase]; - -export const getPhaseData = (state, phase, key) => { - if (PHASE_ATTRIBUTES_THAT_ARE_NUMBERS.includes(key)) { - return parseInt(getPhase(state, phase)[key]); - } - return getPhase(state, phase)[key]; -}; - -export const splitSizeAndUnits = (field) => { - let size; - let units; - - const result = /(\d+)(\w+)/.exec(field); - if (result) { - size = parseInt(result[1]) || 0; - units = result[2]; - } - - return { - size, - units, - }; -}; - -export const isNumber = (value) => typeof value === 'number'; -export const isEmptyObject = (obj) => { - return !obj || (Object.entries(obj).length === 0 && obj.constructor === Object); -}; - -const phaseFromES = (phase, phaseName, defaultEmptyPolicy) => { - const policy = { ...defaultEmptyPolicy }; - if (!phase) { - return policy; - } - - policy[PHASE_ENABLED] = true; - - if (phase.min_age) { - if (phaseName === PHASE_WARM && phase.min_age === '0ms') { - policy[WARM_PHASE_ON_ROLLOVER] = true; - } else { - const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phase.min_age); - policy[PHASE_ROLLOVER_MINIMUM_AGE] = minAge; - policy[PHASE_ROLLOVER_MINIMUM_AGE_UNITS] = minAgeUnits; - } - } - if (phaseName === PHASE_WARM) { - policy[PHASE_SHRINK_ENABLED] = false; - policy[PHASE_FORCE_MERGE_ENABLED] = false; - } - if (phase.actions) { - const actions = phase.actions; - - if (actions.rollover) { - const rollover = actions.rollover; - policy[PHASE_ROLLOVER_ENABLED] = true; - if (rollover.max_age) { - const { size: maxAge, units: maxAgeUnits } = splitSizeAndUnits(rollover.max_age); - policy[PHASE_ROLLOVER_MAX_AGE] = maxAge; - policy[PHASE_ROLLOVER_MAX_AGE_UNITS] = maxAgeUnits; - } - if (rollover.max_size) { - const { size: maxSize, units: maxSizeUnits } = splitSizeAndUnits(rollover.max_size); - policy[PHASE_ROLLOVER_MAX_SIZE_STORED] = maxSize; - policy[PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS] = maxSizeUnits; - } - if (rollover.max_docs) { - policy[PHASE_ROLLOVER_MAX_DOCUMENTS] = rollover.max_docs; - } - } - - if (actions.allocate) { - const allocate = actions.allocate; - if (allocate.require) { - Object.entries(allocate.require).forEach((entry) => { - policy[PHASE_NODE_ATTRS] = entry.join(':'); - }); - // checking for null or undefined here - if (allocate.number_of_replicas != null) { - policy[PHASE_REPLICA_COUNT] = allocate.number_of_replicas; - } - } - } - - if (actions.forcemerge) { - const forcemerge = actions.forcemerge; - policy[PHASE_FORCE_MERGE_ENABLED] = true; - policy[PHASE_FORCE_MERGE_SEGMENTS] = forcemerge.max_num_segments; - } - - if (actions.shrink) { - policy[PHASE_SHRINK_ENABLED] = true; - policy[PHASE_PRIMARY_SHARD_COUNT] = actions.shrink.number_of_shards; - } - - if (actions.freeze) { - policy[PHASE_FREEZE_ENABLED] = true; - } - - if (actions.set_priority) { - const { priority } = actions.set_priority; - - policy[PHASE_INDEX_PRIORITY] = priority ?? ''; - } - - if (actions.wait_for_snapshot) { - policy[PHASE_WAIT_FOR_SNAPSHOT_POLICY] = actions.wait_for_snapshot.policy; - } - } - return policy; -}; - -export const policyFromES = (policy) => { - const { - name, - policy: { phases }, - } = policy; - - return { - name, - phases: { - [PHASE_HOT]: phaseFromES(phases[PHASE_HOT], PHASE_HOT, defaultEmptyHotPhase), - [PHASE_WARM]: phaseFromES(phases[PHASE_WARM], PHASE_WARM, defaultEmptyWarmPhase), - [PHASE_COLD]: phaseFromES(phases[PHASE_COLD], PHASE_COLD, defaultEmptyColdPhase), - [PHASE_DELETE]: phaseFromES(phases[PHASE_DELETE], PHASE_DELETE, defaultEmptyDeletePhase), - }, - isNew: false, - saveAsNew: false, - }; -}; - -export const phaseToES = (phase, originalEsPhase) => { - const esPhase = { ...originalEsPhase }; - - if (!phase[PHASE_ENABLED]) { - return {}; - } - if (isNumber(phase[PHASE_ROLLOVER_MINIMUM_AGE])) { - esPhase.min_age = `${phase[PHASE_ROLLOVER_MINIMUM_AGE]}${phase[PHASE_ROLLOVER_MINIMUM_AGE_UNITS]}`; - } - - // If warm phase on rollover is enabled, delete min age field - // An index lifecycle switches to warm phase when rollover occurs, so you cannot specify a warm phase time - // They are mutually exclusive - if (phase[WARM_PHASE_ON_ROLLOVER]) { - delete esPhase.min_age; - } - - esPhase.actions = esPhase.actions || {}; - - if (phase[PHASE_ROLLOVER_ENABLED]) { - esPhase.actions.rollover = {}; - - if (isNumber(phase[PHASE_ROLLOVER_MAX_AGE])) { - esPhase.actions.rollover.max_age = `${phase[PHASE_ROLLOVER_MAX_AGE]}${phase[PHASE_ROLLOVER_MAX_AGE_UNITS]}`; - } - if (isNumber(phase[PHASE_ROLLOVER_MAX_SIZE_STORED])) { - esPhase.actions.rollover.max_size = `${phase[PHASE_ROLLOVER_MAX_SIZE_STORED]}${phase[PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS]}`; - } - if (isNumber(phase[PHASE_ROLLOVER_MAX_DOCUMENTS])) { - esPhase.actions.rollover.max_docs = phase[PHASE_ROLLOVER_MAX_DOCUMENTS]; - } - } else { - delete esPhase.actions.rollover; - } - if (phase[PHASE_NODE_ATTRS]) { - const [name, value] = phase[PHASE_NODE_ATTRS].split(':'); - esPhase.actions.allocate = esPhase.actions.allocate || {}; - esPhase.actions.allocate.require = { - [name]: value, - }; - } else { - if (esPhase.actions.allocate) { - delete esPhase.actions.allocate.require; - } - } - if (isNumber(phase[PHASE_REPLICA_COUNT])) { - esPhase.actions.allocate = esPhase.actions.allocate || {}; - esPhase.actions.allocate.number_of_replicas = phase[PHASE_REPLICA_COUNT]; - } else { - if (esPhase.actions.allocate) { - delete esPhase.actions.allocate.number_of_replicas; - } - } - if ( - esPhase.actions.allocate && - !esPhase.actions.allocate.require && - !isNumber(esPhase.actions.allocate.number_of_replicas) && - isEmptyObject(esPhase.actions.allocate.include) && - isEmptyObject(esPhase.actions.allocate.exclude) - ) { - // remove allocate action if it does not define require or number of nodes - // and both include and exclude are empty objects (ES will fail to parse if we don't) - delete esPhase.actions.allocate; - } - - if (phase[PHASE_FORCE_MERGE_ENABLED]) { - esPhase.actions.forcemerge = { - max_num_segments: phase[PHASE_FORCE_MERGE_SEGMENTS], - }; - } else { - delete esPhase.actions.forcemerge; - } - - if (phase[PHASE_SHRINK_ENABLED] && isNumber(phase[PHASE_PRIMARY_SHARD_COUNT])) { - esPhase.actions.shrink = { - number_of_shards: phase[PHASE_PRIMARY_SHARD_COUNT], - }; - } else { - delete esPhase.actions.shrink; - } - - if (phase[PHASE_FREEZE_ENABLED]) { - esPhase.actions.freeze = {}; - } else { - delete esPhase.actions.freeze; - } - if (isNumber(phase[PHASE_INDEX_PRIORITY])) { - esPhase.actions.set_priority = { - priority: phase[PHASE_INDEX_PRIORITY], - }; - } else if (phase[PHASE_INDEX_PRIORITY] === '') { - esPhase.actions.set_priority = { - priority: null, - }; - } - - if (phase[PHASE_WAIT_FOR_SNAPSHOT_POLICY]) { - esPhase.actions.wait_for_snapshot = { - policy: phase[PHASE_WAIT_FOR_SNAPSHOT_POLICY], - }; - } else { - delete esPhase.actions.wait_for_snapshot; - } - return esPhase; -}; From e9446b2060efd1d25f3a7bda4ee9298cb7844e06 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Tue, 25 Aug 2020 13:34:29 -0400 Subject: [PATCH 012/216] [Resolver] restore function to the resolverTest plugin. (#75799) Restore the resolverTest plugin. This will allow us to run the test plugin and try out Resolver using our mock data access layers. Eventually this could be expanded to support multiple different data access layers. It could even be expanded to allow us to control the data access layer via the browser. Another option: we could export the APIs from the server and use those in this test plugin. We eventually expect other plugins to use Resolver. This test plugin could allow us to test Resolver via the FTR (separately of the Security Solution.) This would also be useful for writing tests than use the FTR but which are essentially unit tests. For example: taking screenshots, using the mouse to zoom/pan. Start using: `yarn start --plugin-path x-pack/test/plugin_functional/plugins/resolver_test/` --- .../public/common/store/epic.ts | 12 +- .../public/common/store/store.ts | 6 +- .../security_solution/public/plugin.tsx | 9 +- .../public/resolver/index.ts | 30 ++++ .../public/resolver/store/index.ts | 2 +- .../test_utilities/simulator/index.tsx | 2 +- .../public/resolver/types.ts | 42 ++++- .../public/resolver/view/index.tsx | 4 +- .../public/timelines/store/timeline/types.ts | 4 +- .../plugins/security_solution/public/types.ts | 6 +- .../plugins/resolver_test/kibana.json | 9 +- .../applications/resolver_test/index.tsx | 158 ++++++++---------- .../plugins/resolver_test/public/plugin.ts | 48 +++--- 13 files changed, 195 insertions(+), 137 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/resolver/index.ts diff --git a/x-pack/plugins/security_solution/public/common/store/epic.ts b/x-pack/plugins/security_solution/public/common/store/epic.ts index d9de7951a86f4e..51a9377b9fd04e 100644 --- a/x-pack/plugins/security_solution/public/common/store/epic.ts +++ b/x-pack/plugins/security_solution/public/common/store/epic.ts @@ -4,14 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { combineEpics } from 'redux-observable'; +import { combineEpics, Epic } from 'redux-observable'; +import { Action } from 'redux'; + import { createTimelineEpic } from '../../timelines/store/timeline/epic'; import { createTimelineFavoriteEpic } from '../../timelines/store/timeline/epic_favorite'; import { createTimelineNoteEpic } from '../../timelines/store/timeline/epic_note'; import { createTimelinePinnedEventEpic } from '../../timelines/store/timeline/epic_pinned_event'; import { createTimelineLocalStorageEpic } from '../../timelines/store/timeline/epic_local_storage'; +import { TimelineEpicDependencies } from '../../timelines/store/timeline/types'; -export const createRootEpic = () => +export const createRootEpic = (): Epic< + Action, + Action, + State, + TimelineEpicDependencies +> => combineEpics( createTimelineEpic(), createTimelineFavoriteEpic(), diff --git a/x-pack/plugins/security_solution/public/common/store/store.ts b/x-pack/plugins/security_solution/public/common/store/store.ts index a39c9f18bcdb8d..f041e1fd82a9f5 100644 --- a/x-pack/plugins/security_solution/public/common/store/store.ts +++ b/x-pack/plugins/security_solution/public/common/store/store.ts @@ -13,6 +13,7 @@ import { Middleware, Dispatch, PreloadedState, + CombinedState, } from 'redux'; import { createEpicMiddleware } from 'redux-observable'; @@ -30,6 +31,7 @@ import { Immutable } from '../../../common/endpoint/types'; import { State } from './types'; import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; import { CoreStart } from '../../../../../../src/core/public'; +import { TimelineEpicDependencies } from '../../timelines/store/timeline/types'; type ComposeType = typeof compose; declare global { @@ -56,7 +58,7 @@ export const createStore = ( ): Store => { const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - const middlewareDependencies = { + const middlewareDependencies: TimelineEpicDependencies = { apolloClient$: apolloClient, kibana$: kibana, selectAllTimelineQuery: inputsSelectors.globalQueryByIdSelector, @@ -80,7 +82,7 @@ export const createStore = ( ) ); - epicMiddleware.run(createRootEpic()); + epicMiddleware.run(createRootEpic>()); return store; }; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index f1a933fb34d663..a691dd98e70815 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -66,7 +66,7 @@ export class Plugin implements IPlugin, plugins: SetupPlugins) { + public setup(core: CoreSetup, plugins: SetupPlugins): PluginSetup { initTelemetry(plugins.usageCollection, APP_ID); plugins.home.featureCatalogue.register({ @@ -319,7 +319,12 @@ export class Plugin implements IPlugin { + const { resolverPluginSetup } = await import('./resolver'); + return resolverPluginSetup(); + }, + }; } public start(core: CoreStart, plugins: StartPlugins) { diff --git a/x-pack/plugins/security_solution/public/resolver/index.ts b/x-pack/plugins/security_solution/public/resolver/index.ts new file mode 100644 index 00000000000000..409f82c9d1560d --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/index.ts @@ -0,0 +1,30 @@ +/* + * 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 { Provider } from 'react-redux'; +import { ResolverPluginSetup } from './types'; +import { resolverStoreFactory } from './store/index'; +import { ResolverWithoutProviders } from './view/resolver_without_providers'; +import { noAncestorsTwoChildren } from './data_access_layer/mocks/no_ancestors_two_children'; + +/** + * These exports are used by the plugin 'resolverTest' defined in x-pack's plugin_functional suite. + */ + +/** + * Provide access to Resolver APIs. + */ +export function resolverPluginSetup(): ResolverPluginSetup { + return { + Provider, + storeFactory: resolverStoreFactory, + ResolverWithoutProviders, + mocks: { + dataAccessLayer: { + noAncestorsTwoChildren, + }, + }, + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/store/index.ts b/x-pack/plugins/security_solution/public/resolver/store/index.ts index 950a61db33f177..ed8a5129c7ff6d 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/index.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/index.ts @@ -11,7 +11,7 @@ import { resolverReducer } from './reducer'; import { resolverMiddlewareFactory } from './middleware'; import { ResolverAction } from './actions'; -export const storeFactory = ( +export const resolverStoreFactory = ( dataAccessLayer: DataAccessLayer ): Store => { const actionsDenylist: Array = ['userMovedPointer']; diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index b79b7df48a6de3..a6520c8f0e06f3 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { Store, createStore, applyMiddleware } from 'redux'; import { mount, ReactWrapper } from 'enzyme'; -import { createMemoryHistory, History as HistoryPackageHistoryInterface } from 'history'; +import { History as HistoryPackageHistoryInterface, createMemoryHistory } from 'history'; import { CoreStart } from '../../../../../../../src/core/public'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { spyMiddlewareFactory } from '../spy_middleware_factory'; diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 97d97700b11aee..33f7a1d97db139 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -9,6 +9,7 @@ import { Store } from 'redux'; import { Middleware, Dispatch } from 'redux'; import { BBox } from 'rbush'; +import { Provider } from 'react-redux'; import { ResolverAction } from './store/actions'; import { ResolverRelatedEvents, @@ -410,7 +411,7 @@ export interface SideEffectSimulator { /** * Mocked `SideEffectors`. */ - mock: jest.Mocked> & Pick; + mock: SideEffectors; } /** @@ -532,3 +533,42 @@ export interface SpyMiddleware { */ debugActions: () => () => void; } + +/** + * values of this type are exposed by the Security Solution plugin's setup phase. + */ +export interface ResolverPluginSetup { + /** + * Provide access to the instance of the `react-redux` `Provider` that Resolver recognizes. + */ + Provider: typeof Provider; + /** + * Takes a `DataAccessLayer`, which could be a mock one, and returns an redux Store. + * All data acess (e.g. HTTP requests) are done through the store. + */ + storeFactory: (dataAccessLayer: DataAccessLayer) => Store; + + /** + * The Resolver component without the required Providers. + * You must wrap this component in: `I18nProvider`, `Router` (from react-router,) `KibanaContextProvider`, + * and the `Provider` component provided by this object. + */ + ResolverWithoutProviders: React.MemoExoticComponent< + React.ForwardRefExoticComponent> + >; + + /** + * A collection of mock objects that can be used in examples or in testing. + */ + mocks: { + /** + * Mock `DataAccessLayer`s. All of Resolver's HTTP access is provided by a `DataAccessLayer`. + */ + dataAccessLayer: { + /** + * A mock `DataAccessLayer` that returns a tree that has no ancestor nodes but which has 2 children nodes. + */ + noAncestorsTwoChildren: () => { dataAccessLayer: DataAccessLayer }; + }; + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/view/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/index.tsx index d9a0bf291d0e43..bcc420435e5d9d 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/index.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import { Provider } from 'react-redux'; -import { storeFactory } from '../store'; +import { resolverStoreFactory } from '../store'; import { StartServices } from '../../types'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { DataAccessLayer, ResolverProps } from '../types'; @@ -24,7 +24,7 @@ export const Resolver = React.memo((props: ResolverProps) => { ]); const store = useMemo(() => { - return storeFactory(dataAccessLayer); + return resolverStoreFactory(dataAccessLayer); }, [dataAccessLayer]); return ( diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts index c64ed608339b67..8a5344e0754db6 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts @@ -10,9 +10,9 @@ import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; import { AppApolloClient } from '../../../common/lib/lib'; import { inputsModel } from '../../../common/store/inputs'; import { NotesById } from '../../../common/store/app/model'; -import { StartServices } from '../../../types'; import { TimelineModel } from './model'; +import { CoreStart } from '../../../../../../../src/core/public'; export interface AutoSavedWarningMsg { timelineId: string | null; @@ -55,6 +55,6 @@ export interface TimelineEpicDependencies { selectAllTimelineQuery: () => (state: State, id: string) => inputsModel.GlobalQuery; selectNotesByIdSelector: (state: State) => NotesById; apolloClient$: Observable; - kibana$: Observable; + kibana$: Observable; storage: Storage; } diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 3913b96b3e11a1..fd1ff566a77198 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -21,6 +21,7 @@ import { } from '../../triggers_actions_ui/public'; import { SecurityPluginSetup } from '../../security/public'; import { AppFrontendLibs } from './common/lib/lib'; +import { ResolverPluginSetup } from './resolver/types'; export interface SetupPlugins { home: HomePublicPluginSetup; @@ -46,8 +47,9 @@ export type StartServices = CoreStart & storage: Storage; }; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface PluginSetup {} +export interface PluginSetup { + resolver: () => Promise; +} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PluginStart {} diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/kibana.json b/x-pack/test/plugin_functional/plugins/resolver_test/kibana.json index c715a0aaa3b207..499983561e89d3 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/kibana.json +++ b/x-pack/test/plugin_functional/plugins/resolver_test/kibana.json @@ -2,8 +2,13 @@ "id": "resolver_test", "version": "1.0.0", "kibanaVersion": "kibana", - "configPath": ["xpack", "resolver_test"], - "requiredPlugins": ["embeddable"], + "configPath": ["xpack", "resolverTest"], + "requiredPlugins": [ + "securitySolution" + ], + "requiredBundles": [ + "kibanaReact" + ], "server": false, "ui": true } diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx index 79665b6a393df7..4afd71fd67a69c 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx @@ -4,119 +4,95 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import { Router } from 'react-router-dom'; + +import React from 'react'; import ReactDOM from 'react-dom'; -import { AppMountParameters } from 'kibana/public'; -import { I18nProvider } from '@kbn/i18n/react'; -import { IEmbeddable } from 'src/plugins/embeddable/public'; -import { useEffect } from 'react'; +import { AppMountParameters, CoreStart } from 'kibana/public'; +import { useMemo } from 'react'; import styled from 'styled-components'; +import { I18nProvider } from '@kbn/i18n/react'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +import { + DataAccessLayer, + ResolverPluginSetup, +} from '../../../../../../../plugins/security_solution/public/resolver/types'; /** * Render the Resolver Test app. Returns a cleanup function. */ export function renderApp( - { element }: AppMountParameters, - embeddable: Promise + coreStart: CoreStart, + parameters: AppMountParameters, + resolverPluginSetup: ResolverPluginSetup ) { /** * The application DOM node should take all available space. */ - element.style.display = 'flex'; - element.style.flexGrow = '1'; + parameters.element.style.display = 'flex'; + parameters.element.style.flexGrow = '1'; ReactDOM.render( - - - , - element + , + parameters.element ); return () => { - ReactDOM.unmountComponentAtNode(element); + ReactDOM.unmountComponentAtNode(parameters.element); }; } -const AppRoot = styled( - React.memo( - ({ - embeddable: embeddablePromise, - className, - }: { - /** - * A promise which resolves to the Resolver embeddable. - */ - embeddable: Promise; - /** - * A `className` string provided by `styled` - */ - className?: string; - }) => { - /** - * This state holds the reference to the embeddable, once resolved. - */ - const [embeddable, setEmbeddable] = React.useState(undefined); - /** - * This state holds the reference to the DOM node that will contain the embeddable. - */ - const [renderTarget, setRenderTarget] = React.useState(null); - - /** - * Keep component state with the Resolver embeddable. - * - * If the reference to the embeddablePromise changes, we ignore the stale promise. - */ - useEffect(() => { - /** - * A promise rejection function that will prevent a stale embeddable promise from being resolved - * as the current eembeddable. - * - * If the embeddablePromise itself changes before the old one is resolved, we cancel and restart this effect. - */ - let cleanUp; - - const cleanupPromise = new Promise((_resolve, reject) => { - cleanUp = reject; - }); - - /** - * Either set the embeddable in state, or cancel and restart this process. - */ - Promise.race([cleanupPromise, embeddablePromise]).then((value) => { - setEmbeddable(value); - }); +const AppRoot = React.memo( + ({ + coreStart, + parameters, + resolverPluginSetup, + }: { + coreStart: CoreStart; + parameters: AppMountParameters; + resolverPluginSetup: ResolverPluginSetup; + }) => { + const { + Provider, + storeFactory, + ResolverWithoutProviders, + mocks: { + dataAccessLayer: { noAncestorsTwoChildren }, + }, + } = resolverPluginSetup; + const dataAccessLayer: DataAccessLayer = useMemo( + () => noAncestorsTwoChildren().dataAccessLayer, + [noAncestorsTwoChildren] + ); - /** - * If `embeddablePromise` is changed, the cleanup function is run. - */ - return cleanUp; - }, [embeddablePromise]); + const store = useMemo(() => { + return storeFactory(dataAccessLayer); + }, [storeFactory, dataAccessLayer]); - /** - * Render the eembeddable into the DOM node. - */ - useEffect(() => { - if (embeddable && renderTarget) { - embeddable.render(renderTarget); - /** - * If the embeddable or DOM node changes then destroy the old embeddable. - */ - return () => { - embeddable.destroy(); - }; - } - }, [embeddable, renderTarget]); + return ( + + + + + + + + + + + + ); + } +); - return ( -
- ); - } - ) -)` +const Wrapper = styled.div` /** * Take all available space. */ diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts index 853265ae6e5deb..3da30442835564 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, CoreSetup } from 'kibana/public'; +import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; import { i18n } from '@kbn/i18n'; -import { IEmbeddable, EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; +import { PluginSetup as SecuritySolutionPluginSetup } from '../../../../../plugins/security_solution/public'; export type ResolverTestPluginSetup = void; export type ResolverTestPluginStart = void; -export interface ResolverTestPluginSetupDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface -export interface ResolverTestPluginStartDependencies { - embeddable: EmbeddableStart; +export interface ResolverTestPluginSetupDependencies { + securitySolution: SecuritySolutionPluginSetup; } +export interface ResolverTestPluginStartDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface export class ResolverTestPlugin implements @@ -23,34 +23,24 @@ export class ResolverTestPlugin ResolverTestPluginSetupDependencies, ResolverTestPluginStartDependencies > { - public setup(core: CoreSetup) { + public setup( + core: CoreSetup, + setupDependencies: ResolverTestPluginSetupDependencies + ) { core.application.register({ - id: 'resolver_test', - title: i18n.translate('xpack.resolver_test.pluginTitle', { + id: 'resolverTest', + title: i18n.translate('xpack.resolverTest.pluginTitle', { defaultMessage: 'Resolver Test', }), - mount: async (_context, params) => { - let resolveEmbeddable: ( - value: IEmbeddable | undefined | PromiseLike | undefined - ) => void; + mount: async (params: AppMountParameters) => { + const startServices = await core.getStartServices(); + const [coreStart] = startServices; - const promise = new Promise((resolve) => { - resolveEmbeddable = resolve; - }); - - (async () => { - const [, { embeddable }] = await core.getStartServices(); - const factory = embeddable.getEmbeddableFactory('resolver'); - if (factory) { - resolveEmbeddable!(factory.create({ id: 'test basic render' })); - } - })(); - - const { renderApp } = await import('./applications/resolver_test'); - /** - * Pass a promise which resolves to the Resolver embeddable. - */ - return renderApp(params, promise); + const [{ renderApp }, resolverPluginSetup] = await Promise.all([ + import('./applications/resolver_test'), + setupDependencies.securitySolution.resolver(), + ]); + return renderApp(coreStart, params, resolverPluginSetup); }, }); } From e236bdf4af95a8219e30a9e176d3f2169cb19ab8 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 25 Aug 2020 12:13:57 -0600 Subject: [PATCH 013/216] [Maps] add message to empty add tooltip card (#75809) * [Maps] add message to empty add tooltip card * use suggested text --- .../tooltip_selector/tooltip_selector.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.tsx b/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.tsx index 84316a1b9105d2..9bab590d1f5ea9 100644 --- a/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.tsx +++ b/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.tsx @@ -13,8 +13,10 @@ import { EuiDroppable, EuiText, EuiTextAlign, + EuiTextColor, EuiSpacer, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { AddTooltipFieldPopover, FieldProps } from './add_tooltip_field_popover'; import { IField } from '../../classes/fields/field'; @@ -156,7 +158,18 @@ export class TooltipSelector extends Component { _renderProperties() { if (!this.state.selectedFieldProps.length) { - return null; + return ( + +

+ + + +

+
+ ); } return ( From f2fef70282d7d2e7ecbf4e38b6b9cc075b51f361 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 25 Aug 2020 14:21:35 -0600 Subject: [PATCH 014/216] Migrate legacy map UI settings (#75887) * Migrate legacy map UI settings * i18n fixes --- .../kibana/server/ui_setting_defaults.js | 83 +------------ src/plugins/maps_legacy/server/index.ts | 7 +- src/plugins/maps_legacy/server/ui_settings.ts | 113 ++++++++++++++++++ src/plugins/region_map/server/index.ts | 7 +- src/plugins/region_map/server/ui_settings.ts | 42 +++++++ .../translations/translations/ja-JP.json | 8 -- .../translations/translations/zh-CN.json | 8 -- 7 files changed, 167 insertions(+), 101 deletions(-) create mode 100644 src/plugins/maps_legacy/server/ui_settings.ts create mode 100644 src/plugins/region_map/server/ui_settings.ts 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 2562657a716244..7de5fb581643ac 100644 --- a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js @@ -17,88 +17,7 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; - export function getUiSettingDefaults() { // wrapped in provider so that a new instance is given to each app/test - return { - 'visualization:tileMap:maxPrecision': { - name: i18n.translate('kbn.advancedSettings.visualization.tileMap.maxPrecisionTitle', { - defaultMessage: 'Maximum tile map precision', - }), - value: 7, - description: i18n.translate('kbn.advancedSettings.visualization.tileMap.maxPrecisionText', { - defaultMessage: - 'The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, 12 is the max. {cellDimensionsLink}', - description: - 'Part of composite text: kbn.advancedSettings.visualization.tileMap.maxPrecisionText + ' + - 'kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText', - values: { - cellDimensionsLink: - `` + - i18n.translate( - 'kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText', - { - defaultMessage: 'Explanation of cell dimensions', - } - ) + - '', - }, - }), - category: ['visualization'], - }, - 'visualization:tileMap:WMSdefaults': { - name: i18n.translate('kbn.advancedSettings.visualization.tileMap.wmsDefaultsTitle', { - defaultMessage: 'Default WMS properties', - }), - value: JSON.stringify( - { - enabled: false, - url: undefined, - options: { - version: undefined, - layers: undefined, - format: 'image/png', - transparent: true, - attribution: undefined, - styles: undefined, - }, - }, - null, - 2 - ), - type: 'json', - description: i18n.translate('kbn.advancedSettings.visualization.tileMap.wmsDefaultsText', { - defaultMessage: - 'Default {propertiesLink} for the WMS map server support in the coordinate map', - description: - 'Part of composite text: kbn.advancedSettings.visualization.tileMap.wmsDefaultsText + ' + - 'kbn.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText', - values: { - propertiesLink: - '' + - i18n.translate( - 'kbn.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText', - { - defaultMessage: 'properties', - } - ) + - '', - }, - }), - category: ['visualization'], - }, - 'visualization:regionmap:showWarnings': { - name: i18n.translate('kbn.advancedSettings.visualization.showRegionMapWarningsTitle', { - defaultMessage: 'Show region map warning', - }), - value: true, - description: i18n.translate('kbn.advancedSettings.visualization.showRegionMapWarningsText', { - defaultMessage: - 'Whether the region map shows a warning when terms cannot be joined to a shape on the map.', - }), - category: ['visualization'], - }, - }; + return {}; } diff --git a/src/plugins/maps_legacy/server/index.ts b/src/plugins/maps_legacy/server/index.ts index 5da3ce1a84408c..79ecbb238314a2 100644 --- a/src/plugins/maps_legacy/server/index.ts +++ b/src/plugins/maps_legacy/server/index.ts @@ -18,9 +18,10 @@ */ import { Plugin, PluginConfigDescriptor } from 'kibana/server'; -import { PluginInitializerContext } from 'src/core/server'; +import { CoreSetup, PluginInitializerContext } from 'src/core/server'; import { Observable } from 'rxjs'; import { configSchema, ConfigSchema } from '../config'; +import { getUiSettings } from './ui_settings'; export const config: PluginConfigDescriptor = { exposeToBrowser: { @@ -49,7 +50,9 @@ export class MapsLegacyPlugin implements Plugin { this._initializerContext = initializerContext; } - public setup() { + public setup(core: CoreSetup) { + core.uiSettings.register(getUiSettings()); + // @ts-ignore const config$ = this._initializerContext.config.create(); return { diff --git a/src/plugins/maps_legacy/server/ui_settings.ts b/src/plugins/maps_legacy/server/ui_settings.ts new file mode 100644 index 00000000000000..f92ccf848f4093 --- /dev/null +++ b/src/plugins/maps_legacy/server/ui_settings.ts @@ -0,0 +1,113 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { UiSettingsParams } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + +export function getUiSettings(): Record> { + return { + 'visualization:tileMap:maxPrecision': { + name: i18n.translate('maps_legacy.advancedSettings.visualization.tileMap.maxPrecisionTitle', { + defaultMessage: 'Maximum tile map precision', + }), + value: 7, + description: i18n.translate( + 'maps_legacy.advancedSettings.visualization.tileMap.maxPrecisionText', + { + defaultMessage: + 'The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, 12 is the max. {cellDimensionsLink}', + description: + 'Part of composite text: maps_legacy.advancedSettings.visualization.tileMap.maxPrecisionText + ' + + 'maps_legacy.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText', + values: { + cellDimensionsLink: + `` + + i18n.translate( + 'maps_legacy.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText', + { + defaultMessage: 'Explanation of cell dimensions', + } + ) + + '', + }, + } + ), + schema: schema.number(), + category: ['visualization'], + }, + 'visualization:tileMap:WMSdefaults': { + name: i18n.translate('maps_legacy.advancedSettings.visualization.tileMap.wmsDefaultsTitle', { + defaultMessage: 'Default WMS properties', + }), + value: JSON.stringify( + { + enabled: false, + url: '', + options: { + version: '', + layers: '', + format: 'image/png', + transparent: true, + attribution: '', + styles: '', + }, + }, + null, + 2 + ), + type: 'json', + description: i18n.translate( + 'maps_legacy.advancedSettings.visualization.tileMap.wmsDefaultsText', + { + defaultMessage: + 'Default {propertiesLink} for the WMS map server support in the coordinate map', + description: + 'Part of composite text: maps_legacy.advancedSettings.visualization.tileMap.wmsDefaultsText + ' + + 'maps_legacy.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText', + values: { + propertiesLink: + '' + + i18n.translate( + 'maps_legacy.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText', + { + defaultMessage: 'properties', + } + ) + + '', + }, + } + ), + schema: schema.object({ + enabled: schema.boolean(), + url: schema.string(), + options: schema.object({ + version: schema.string(), + layers: schema.string(), + format: schema.string(), + transparent: schema.boolean(), + attribution: schema.string(), + styles: schema.string(), + }), + }), + category: ['visualization'], + }, + }; +} diff --git a/src/plugins/region_map/server/index.ts b/src/plugins/region_map/server/index.ts index e2c544d2d0ba65..f4684e1c60349e 100644 --- a/src/plugins/region_map/server/index.ts +++ b/src/plugins/region_map/server/index.ts @@ -18,7 +18,9 @@ */ import { PluginConfigDescriptor } from 'kibana/server'; +import { CoreSetup } from 'src/core/server'; import { configSchema, ConfigSchema } from '../config'; +import { getUiSettings } from './ui_settings'; export const config: PluginConfigDescriptor = { exposeToBrowser: { @@ -29,6 +31,9 @@ export const config: PluginConfigDescriptor = { }; export const plugin = () => ({ - setup() {}, + setup(core: CoreSetup) { + core.uiSettings.register(getUiSettings()); + }, + start() {}, }); diff --git a/src/plugins/region_map/server/ui_settings.ts b/src/plugins/region_map/server/ui_settings.ts new file mode 100644 index 00000000000000..9c404676b9ffd0 --- /dev/null +++ b/src/plugins/region_map/server/ui_settings.ts @@ -0,0 +1,42 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { UiSettingsParams } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + +export function getUiSettings(): Record> { + return { + 'visualization:regionmap:showWarnings': { + name: i18n.translate('regionMap.advancedSettings.visualization.showRegionMapWarningsTitle', { + defaultMessage: 'Show region map warning', + }), + value: true, + description: i18n.translate( + 'regionMap.advancedSettings.visualization.showRegionMapWarningsText', + { + defaultMessage: + 'Whether the region map shows a warning when terms cannot be joined to a shape on the map.', + } + ), + schema: schema.boolean(), + category: ['visualization'], + }, + }; +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 118362f494b474..0b51c00475d7e1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2775,14 +2775,6 @@ "inspector.requests.statisticsTabLabel": "統計", "inspector.title": "インスペクター", "inspector.view": "{viewName} を表示", - "kbn.advancedSettings.visualization.showRegionMapWarningsText": "用語がマップの形に合わない場合に地域マップに警告を表示するかどうかです。", - "kbn.advancedSettings.visualization.showRegionMapWarningsTitle": "地域マップに警告を表示", - "kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "ディメンションの説明", - "kbn.advancedSettings.visualization.tileMap.maxPrecisionText": "マップに表示されるジオハッシュの最高精度です。7 が高い、10 が非常に高い、12 が最高を意味します。{cellDimensionsLink}", - "kbn.advancedSettings.visualization.tileMap.maxPrecisionTitle": "タイルマップの最高精度", - "kbn.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText": "プロパティ", - "kbn.advancedSettings.visualization.tileMap.wmsDefaultsText": "座標マップの WMS マップサーバーサポートのデフォルトの {propertiesLink} です。", - "kbn.advancedSettings.visualization.tileMap.wmsDefaultsTitle": "デフォルトの WMS プロパティ", "kibana_legacy.notify.fatalError.errorStatusMessage": "エラー {errStatus} {errStatusText}: {errMessage}", "kibana_legacy.notify.fatalError.unavailableServerErrorMessage": "HTTP リクエストで接続に失敗しました。Kibana サーバーが実行されていて、ご使用のブラウザの接続が正常に動作していることを確認するか、システム管理者にお問い合わせください。", "kibana_legacy.notify.toaster.errorMessage": "エラー: {errorMessage}\n {errorStack}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index de1f206118447e..d520f63fe74845 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2776,14 +2776,6 @@ "inspector.requests.statisticsTabLabel": "统计信息", "inspector.title": "检查器", "inspector.view": "视图:{viewName}", - "kbn.advancedSettings.visualization.showRegionMapWarningsText": "词无法联接到地图上的形状时,区域地图是否显示警告。", - "kbn.advancedSettings.visualization.showRegionMapWarningsTitle": "显示区域地图警告", - "kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "单元格维度的解释", - "kbn.advancedSettings.visualization.tileMap.maxPrecisionText": "在磁贴地图上显示的最大 geoHash 精确度:7 为高,10 为很高,12 为最大值。{cellDimensionsLink}", - "kbn.advancedSettings.visualization.tileMap.maxPrecisionTitle": "最大磁贴地图精确度", - "kbn.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText": "属性", - "kbn.advancedSettings.visualization.tileMap.wmsDefaultsText": "坐标地图中 WMS 地图服务器支持的默认{propertiesLink}", - "kbn.advancedSettings.visualization.tileMap.wmsDefaultsTitle": "默认 WMS 属性", "kibana_legacy.notify.fatalError.errorStatusMessage": "错误 {errStatus} {errStatusText}:{errMessage}", "kibana_legacy.notify.fatalError.unavailableServerErrorMessage": "HTTP 请求无法连接。请检查 Kibana 服务器是否正在运行以及您的浏览器是否具有有效的连接,或请联系您的系统管理员。", "kibana_legacy.notify.toaster.errorMessage": "错误:{errorMessage}\n {errorStack}", From c3e226cf31899203c69d8d0616861c7dadecfc3e Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 25 Aug 2020 14:24:14 -0600 Subject: [PATCH 015/216] [Maps] Originating App Breadcrumb (#75692) * [Maps] Originating App Breadcrumb * pass getHasUnsavedChanges instead of passing boolean Co-authored-by: Elastic Machine --- .../routes/maps_app/get_breadcrumbs.test.tsx | 36 +++++++++++ .../routes/maps_app/get_breadcrumbs.tsx | 59 +++++++++++++++++++ .../routing/routes/maps_app/maps_app_view.js | 36 ++++------- 3 files changed, 105 insertions(+), 26 deletions(-) create mode 100644 x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.test.tsx create mode 100644 x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.test.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.test.tsx new file mode 100644 index 00000000000000..e8e0e583a7c6da --- /dev/null +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.test.tsx @@ -0,0 +1,36 @@ +/* + * 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 { getBreadcrumbs } from './get_breadcrumbs'; + +jest.mock('../../../kibana_services', () => {}); +jest.mock('../../maps_router', () => {}); + +const getHasUnsavedChanges = () => { + return false; +}; + +test('should get breadcrumbs "Maps / mymap"', () => { + const breadcrumbs = getBreadcrumbs({ title: 'mymap', getHasUnsavedChanges }); + expect(breadcrumbs.length).toBe(2); + expect(breadcrumbs[0].text).toBe('Maps'); + expect(breadcrumbs[1].text).toBe('mymap'); +}); + +test('should get breadcrumbs "Dashboard / Maps / mymap" with originatingApp', () => { + const breadcrumbs = getBreadcrumbs({ + title: 'mymap', + getHasUnsavedChanges, + originatingApp: 'dashboardId', + getAppNameFromId: (appId) => { + return 'Dashboard'; + }, + }); + expect(breadcrumbs.length).toBe(3); + expect(breadcrumbs[0].text).toBe('Dashboard'); + expect(breadcrumbs[1].text).toBe('Maps'); + expect(breadcrumbs[2].text).toBe('mymap'); +}); diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx new file mode 100644 index 00000000000000..1ccf890597edc1 --- /dev/null +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx @@ -0,0 +1,59 @@ +/* + * 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 { getNavigateToApp } from '../../../kibana_services'; +// @ts-expect-error +import { goToSpecifiedPath } from '../../maps_router'; + +export const unsavedChangesWarning = i18n.translate( + 'xpack.maps.breadCrumbs.unsavedChangesWarning', + { + defaultMessage: 'Your map has unsaved changes. Are you sure you want to leave?', + } +); + +export function getBreadcrumbs({ + title, + getHasUnsavedChanges, + originatingApp, + getAppNameFromId, +}: { + title: string; + getHasUnsavedChanges: () => boolean; + originatingApp?: string; + getAppNameFromId?: (id: string) => string; +}) { + const breadcrumbs = []; + if (originatingApp && getAppNameFromId) { + breadcrumbs.push({ + onClick: () => { + getNavigateToApp()(originatingApp); + }, + text: getAppNameFromId(originatingApp), + }); + } + + breadcrumbs.push({ + text: i18n.translate('xpack.maps.mapController.mapsBreadcrumbLabel', { + defaultMessage: 'Maps', + }), + onClick: () => { + if (getHasUnsavedChanges()) { + const navigateAway = window.confirm(unsavedChangesWarning); + if (navigateAway) { + goToSpecifiedPath('/'); + } + } else { + goToSpecifiedPath('/'); + } + }, + }); + + breadcrumbs.push({ text: title }); + + return breadcrumbs; +} diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js index 58f0bf16e93f22..485b0ed7682fad 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js @@ -5,7 +5,6 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import 'mapbox-gl/dist/mapbox-gl.css'; import _ from 'lodash'; import { DEFAULT_IS_LAYER_TOC_OPEN } from '../../../reducers/ui'; @@ -29,13 +28,9 @@ import { AppStateManager } from '../../state_syncing/app_state_manager'; import { startAppStateSyncing } from '../../state_syncing/app_sync'; import { esFilters } from '../../../../../../../src/plugins/data/public'; import { MapContainer } from '../../../connected_components/map_container'; -import { goToSpecifiedPath } from '../../maps_router'; import { getIndexPatternsFromIds } from '../../../index_pattern_util'; import { getTopNavConfig } from './top_nav_config'; - -const unsavedChangesWarning = i18n.translate('xpack.maps.breadCrumbs.unsavedChangesWarning', { - defaultMessage: 'Your map has unsaved changes. Are you sure you want to leave?', -}); +import { getBreadcrumbs, unsavedChangesWarning } from './get_breadcrumbs'; export class MapsAppView extends React.Component { _globalSyncUnsubscribe = null; @@ -104,7 +99,7 @@ export class MapsAppView extends React.Component { getCoreChrome().setBreadcrumbs([]); } - _hasUnsavedChanges() { + _hasUnsavedChanges = () => { const savedLayerList = this.props.savedMap.getLayerList(); return !savedLayerList ? !_.isEqual(this.props.layerListConfigOnly, this.state.initialLayerListConfig) @@ -114,27 +109,16 @@ export class MapsAppView extends React.Component { // Need to perform the same process for layerListConfigOnly to compare apples to apples // and avoid undefined properties in layerListConfigOnly triggering unsaved changes. !_.isEqual(JSON.parse(JSON.stringify(this.props.layerListConfigOnly)), savedLayerList); - } + }; _setBreadcrumbs = () => { - getCoreChrome().setBreadcrumbs([ - { - text: i18n.translate('xpack.maps.mapController.mapsBreadcrumbLabel', { - defaultMessage: 'Maps', - }), - onClick: () => { - if (this._hasUnsavedChanges()) { - const navigateAway = window.confirm(unsavedChangesWarning); - if (navigateAway) { - goToSpecifiedPath('/'); - } - } else { - goToSpecifiedPath('/'); - } - }, - }, - { text: this.props.savedMap.title }, - ]); + const breadcrumbs = getBreadcrumbs({ + title: this.props.savedMap.title, + getHasUnsavedChanges: this._hasUnsavedChanges, + originatingApp: this.state.originatingApp, + getAppNameFromId: this.props.stateTransfer.getAppNameFromId, + }); + getCoreChrome().setBreadcrumbs(breadcrumbs); }; _updateFromGlobalState = ({ changes, state: globalState }) => { From 9511285bbd95f9f074dc28077c22505208272cf9 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 25 Aug 2020 13:27:27 -0700 Subject: [PATCH 016/216] [src/dev/build] report file count of archives when building (#75900) Co-authored-by: spalger Co-authored-by: Elastic Machine --- src/dev/build/lib/fs.ts | 22 ++++++- src/dev/build/tasks/create_archives_task.ts | 71 +++++++++++---------- 2 files changed, 56 insertions(+), 37 deletions(-) diff --git a/src/dev/build/lib/fs.ts b/src/dev/build/lib/fs.ts index d86901c41e4366..a91113ab2d1c41 100644 --- a/src/dev/build/lib/fs.ts +++ b/src/dev/build/lib/fs.ts @@ -273,7 +273,16 @@ export async function compressTar({ archive.pipe(output); - return archive.directory(source, name).finalize(); + let fileCount = 0; + archive.on('entry', (entry) => { + if (entry.stats?.isFile()) { + fileCount += 1; + } + }); + + await archive.directory(source, name).finalize(); + + return fileCount; } interface CompressZipOptions { @@ -294,5 +303,14 @@ export async function compressZip({ archive.pipe(output); - return archive.directory(source, name).finalize(); + let fileCount = 0; + archive.on('entry', (entry) => { + if (entry.stats?.isFile()) { + fileCount += 1; + } + }); + + await archive.directory(source, name).finalize(); + + return fileCount; } diff --git a/src/dev/build/tasks/create_archives_task.ts b/src/dev/build/tasks/create_archives_task.ts index 3ffb1afef7469c..0083881e9f7487 100644 --- a/src/dev/build/tasks/create_archives_task.ts +++ b/src/dev/build/tasks/create_archives_task.ts @@ -21,7 +21,7 @@ import Path from 'path'; import Fs from 'fs'; import { promisify } from 'util'; -import { CiStatsReporter } from '@kbn/dev-utils'; +import { CiStatsReporter, CiStatsMetrics } from '@kbn/dev-utils'; import { mkdirp, compressTar, compressZip, Task } from '../lib'; @@ -47,17 +47,16 @@ export const CreateArchives: Task = { archives.push({ format: 'zip', path: destination, - }); - - await compressZip({ - source, - destination, - archiverOptions: { - zlib: { - level: 9, + fileCount: await compressZip({ + source, + destination, + archiverOptions: { + zlib: { + level: 9, + }, }, - }, - createRootDirectory: true, + createRootDirectory: true, + }), }); break; @@ -65,18 +64,17 @@ export const CreateArchives: Task = { archives.push({ format: 'tar', path: destination, - }); - - await compressTar({ - source, - destination, - archiverOptions: { - gzip: true, - gzipOptions: { - level: 9, + fileCount: await compressTar({ + source, + destination, + archiverOptions: { + gzip: true, + gzipOptions: { + level: 9, + }, }, - }, - createRootDirectory: true, + createRootDirectory: true, + }), }); break; @@ -85,19 +83,22 @@ export const CreateArchives: Task = { } } - const reporter = CiStatsReporter.fromEnv(log); - if (reporter.isEnabled()) { - await reporter.metrics( - await Promise.all( - archives.map(async ({ format, path }) => { - return { - group: `${build.isOss() ? 'oss ' : ''}distributable size`, - id: format, - value: (await asyncStat(path)).size, - }; - }) - ) - ); + const metrics: CiStatsMetrics = []; + for (const { format, path, fileCount } of archives) { + metrics.push({ + group: `${build.isOss() ? 'oss ' : ''}distributable size`, + id: format, + value: (await asyncStat(path)).size, + }); + + metrics.push({ + group: `${build.isOss() ? 'oss ' : ''}distributable file count`, + id: 'total', + value: fileCount, + }); } + log.debug('archive metrics:', metrics); + + await CiStatsReporter.fromEnv(log).metrics(metrics); }, }; From 947a93900d05e8837ac26eae706c96254f48c86d Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 25 Aug 2020 15:02:38 -0600 Subject: [PATCH 017/216] [Maps] fix IVectorLayer.getStyle typing (#75829) * [Maps] fix IVectorLayer.getStyle typing * update typing in VectorLayer type definition * fix unit tests * review feedback --- .../public/classes/layers/vector_layer/vector_layer.d.ts | 2 -- .../classes/styles/vector/properties/__tests__/test_util.ts | 4 ++-- .../styles/vector/properties/dynamic_style_property.tsx | 5 +++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts index ad4479d3a324b2..fa614ae87b2905 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts @@ -32,7 +32,6 @@ export interface IVectorLayer extends ILayer { getJoins(): IJoin[]; getValidJoins(): IJoin[]; getSource(): IVectorSource; - getStyle(): IVectorStyle; getFeatureById(id: string | number): Feature | null; getPropertiesForTooltip(properties: GeoJsonProperties): Promise; hasJoins(): boolean; @@ -79,7 +78,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { _setMbPointsProperties(mbMap: unknown, mvtSourceLayer?: string): void; _setMbLinePolygonProperties(mbMap: unknown, mvtSourceLayer?: string): void; getSource(): IVectorSource; - getStyle(): IVectorStyle; getFeatureById(id: string | number): Feature | null; getPropertiesForTooltip(properties: GeoJsonProperties): Promise; hasJoins(): boolean; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts index 3f6edc81e30ef4..a2dfdc94d8058c 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts @@ -5,7 +5,7 @@ */ // eslint-disable-next-line max-classes-per-file -import { FIELD_ORIGIN } from '../../../../../../common/constants'; +import { FIELD_ORIGIN, LAYER_STYLE_TYPE } from '../../../../../../common/constants'; import { StyleMeta } from '../../style_meta'; import { CategoryFieldMeta, @@ -44,7 +44,7 @@ export class MockStyle implements IStyle { } getType() { - return 'mockStyle'; + return LAYER_STYLE_TYPE.VECTOR; } getStyleMeta(): StyleMeta { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx index 47659e055936e5..826acd41e27a99 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx @@ -27,6 +27,7 @@ import { import { IField } from '../../../fields/field'; import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; import { IJoin } from '../../../joins/join'; +import { IVectorStyle } from '../vector_style'; export interface IDynamicStyleProperty extends IStyleProperty { getFieldMetaOptions(): FieldMetaOptions; @@ -88,7 +89,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty } getRangeFieldMeta() { - const style = this._layer.getStyle(); + const style = this._layer.getStyle() as IVectorStyle; const styleMeta = style.getStyleMeta(); const fieldName = this.getFieldName(); const rangeFieldMetaFromLocalFeatures = styleMeta.getRangeFieldMetaDescriptor(fieldName); @@ -113,7 +114,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty } getCategoryFieldMeta() { - const style = this._layer.getStyle(); + const style = this._layer.getStyle() as IVectorStyle; const styleMeta = style.getStyleMeta(); const fieldName = this.getFieldName(); const categoryFieldMetaFromLocalFeatures = styleMeta.getCategoryFieldMetaDescriptor(fieldName); From fef89334b573b5a4fca89969ff6e2dca9de95a43 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Tue, 25 Aug 2020 16:43:28 -0500 Subject: [PATCH 018/216] [Enterprise Search] Move views into separate folder from components (#75906) * Move views into separate folder from components * Fix paths in tests * More error_state to views --- .../public/applications/workplace_search/index.test.tsx | 4 ++-- .../public/applications/workplace_search/index.tsx | 8 ++++---- .../error_state/error_state.test.tsx | 0 .../{components => views}/error_state/error_state.tsx | 2 +- .../{components => views}/error_state/index.ts | 0 .../{components => views}/overview/__mocks__/index.ts | 0 .../overview/__mocks__/overview_logic.mock.ts | 0 .../{components => views}/overview/index.ts | 0 .../overview/onboarding_card.test.tsx | 0 .../{components => views}/overview/onboarding_card.tsx | 0 .../overview/onboarding_steps.test.tsx | 0 .../{components => views}/overview/onboarding_steps.tsx | 4 ++-- .../overview/organization_stats.test.tsx | 0 .../{components => views}/overview/organization_stats.tsx | 2 +- .../{components => views}/overview/overview.test.tsx | 4 ++-- .../{components => views}/overview/overview.tsx | 6 +++--- .../{components => views}/overview/overview_logic.test.ts | 0 .../{components => views}/overview/overview_logic.ts | 0 .../{components => views}/overview/recent_activity.scss | 0 .../overview/recent_activity.test.tsx | 0 .../{components => views}/overview/recent_activity.tsx | 2 +- .../overview/statistic_card.test.tsx | 0 .../{components => views}/overview/statistic_card.tsx | 0 .../{components => views}/setup_guide/index.ts | 0 .../setup_guide/setup_guide.test.tsx | 0 .../{components => views}/setup_guide/setup_guide.tsx | 0 26 files changed, 16 insertions(+), 16 deletions(-) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/error_state/error_state.test.tsx (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/error_state/error_state.tsx (93%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/error_state/index.ts (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/__mocks__/index.ts (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/__mocks__/overview_logic.mock.ts (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/index.ts (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/onboarding_card.test.tsx (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/onboarding_card.tsx (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/onboarding_steps.test.tsx (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/onboarding_steps.tsx (97%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/organization_stats.test.tsx (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/organization_stats.tsx (97%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/overview.test.tsx (93%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/overview.tsx (93%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/overview_logic.test.ts (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/overview_logic.ts (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/recent_activity.scss (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/recent_activity.test.tsx (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/recent_activity.tsx (98%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/statistic_card.test.tsx (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/overview/statistic_card.tsx (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/setup_guide/index.ts (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/setup_guide/setup_guide.test.tsx (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{components => views}/setup_guide/setup_guide.tsx (100%) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 654f4dce0ebf3c..a0d9352ee9f82a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -12,8 +12,8 @@ import { Redirect } from 'react-router-dom'; import { shallow } from 'enzyme'; import { useValues } from 'kea'; -import { Overview } from './components/overview'; -import { ErrorState } from './components/error_state'; +import { Overview } from './views/overview'; +import { ErrorState } from './views/error_state'; import { WorkplaceSearch } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index b261c83e30ddeb..8582a003c6fa88 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -16,11 +16,11 @@ import { WorkplaceSearchNav } from './components/layout/nav'; import { SETUP_GUIDE_PATH } from './routes'; -import { SetupGuide } from './components/setup_guide'; -import { ErrorState } from './components/error_state'; -import { Overview } from './components/overview'; +import { SetupGuide } from './views/setup_guide'; +import { ErrorState } from './views/error_state'; +import { Overview } from './views/overview'; -export const WorkplaceSearch: React.FC = (props) => { +export const WorkplaceSearch: React.FC = () => { const { config } = useContext(KibanaContext) as IKibanaContext; const { errorConnecting } = useValues(HttpLogic) as IHttpLogicValues; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx similarity index 93% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx index 53f3a7a274429e..9ad649c292fb7a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx @@ -13,7 +13,7 @@ import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; import { ErrorStatePrompt } from '../../../shared/error_state'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; -import { ViewContentHeader } from '../shared/view_content_header'; +import { ViewContentHeader } from '../../components/shared/view_content_header'; export const ErrorState: React.FC = () => { return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/index.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/index.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/index.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/index.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/index.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/index.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_card.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_card.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx similarity index 97% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx index d0f5893bdb88a6..fa4decccb34b10 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx @@ -21,12 +21,12 @@ import { EuiButtonEmptyProps, EuiLinkProps, } from '@elastic/eui'; -import sharedSourcesIcon from '../shared/assets/share_circle.svg'; +import sharedSourcesIcon from '../../components/shared/assets/share_circle.svg'; import { sendTelemetry } from '../../../shared/telemetry'; import { KibanaContext, IKibanaContext } from '../../../index'; import { ORG_SOURCES_PATH, USERS_PATH, ORG_SETTINGS_PATH } from '../../routes'; -import { ContentSection } from '../shared/content_section'; +import { ContentSection } from '../../components/shared/content_section'; import { OverviewLogic, IOverviewValues } from './overview_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx similarity index 97% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx index 4c5efce9baf129..53549cfcdbce7e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx @@ -11,7 +11,7 @@ import { useValues } from 'kea'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { ContentSection } from '../shared/content_section'; +import { ContentSection } from '../../components/shared/content_section'; import { ORG_SOURCES_PATH, USERS_PATH } from '../../routes'; import { OverviewLogic, IOverviewValues } from './overview_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx similarity index 93% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx index fee966a56923d0..e4531ff03587b0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx @@ -11,8 +11,8 @@ import { mockLogicActions, setMockValues } from './__mocks__'; import React from 'react'; import { shallow, mount } from 'enzyme'; -import { Loading } from '../shared/loading'; -import { ViewContentHeader } from '../shared/view_content_header'; +import { Loading } from '../../components/shared/loading'; +import { ViewContentHeader } from '../../components/shared/view_content_header'; import { OnboardingSteps } from './onboarding_steps'; import { OrganizationStats } from './organization_stats'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx similarity index 93% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx index 6aa3e1e608bfe8..134fc9389694dd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx @@ -16,9 +16,9 @@ import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/t import { OverviewLogic, IOverviewActions, IOverviewValues } from './overview_logic'; -import { Loading } from '../shared/loading'; -import { ProductButton } from '../shared/product_button'; -import { ViewContentHeader } from '../shared/view_content_header'; +import { Loading } from '../../components/shared/loading'; +import { ProductButton } from '../../components/shared/product_button'; +import { ViewContentHeader } from '../../components/shared/view_content_header'; import { OnboardingSteps } from './onboarding_steps'; import { OrganizationStats } from './organization_stats'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.scss similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.scss rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.scss diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx similarity index 98% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx index 0f4f6c65d083c5..ada89c33be7e29 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx @@ -12,7 +12,7 @@ import { useValues } from 'kea'; import { EuiEmptyPrompt, EuiLink, EuiPanel, EuiSpacer, EuiLinkProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ContentSection } from '../shared/content_section'; +import { ContentSection } from '../../components/shared/content_section'; import { sendTelemetry } from '../../../shared/telemetry'; import { KibanaContext, IKibanaContext } from '../../../index'; import { getSourcePath } from '../../routes'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/statistic_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/statistic_card.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/statistic_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/statistic_card.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/setup_guide/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/index.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/setup_guide/index.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/setup_guide/setup_guide.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/setup_guide/setup_guide.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/setup_guide/setup_guide.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx From 1fee8f16ef8c6399bfa9d00a7c59cdb92be12361 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 26 Aug 2020 00:00:24 +0200 Subject: [PATCH 019/216] [Lens] fix dimension popover design on mobile (#75866) --- .../indexpattern_datasource/dimension_panel/field_select.tsx | 2 +- .../indexpattern_datasource/dimension_panel/popover_editor.scss | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index b2a59788b50f9a..e4dfa698137439 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -181,7 +181,7 @@ export function FieldSelect({ }} renderOption={(option, searchValue) => { return ( - + Date: Tue, 25 Aug 2020 18:13:41 -0400 Subject: [PATCH 020/216] [Security Solution][Detections] Disables add exception for ML and threshold rules (#75802) --- .../components/alerts_table/default_config.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 5bab2e3c789705..ca17d331c67e58 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -9,6 +9,8 @@ import ApolloClient from 'apollo-client'; import { Dispatch } from 'redux'; import { EuiText } from '@elastic/eui'; +import { RuleType } from '../../../../common/detection_engine/types'; +import { isMlRule } from '../../../../common/machine_learning/helpers'; import { RowRendererId } from '../../../../common/types/timeline'; import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; @@ -39,6 +41,7 @@ import { import { Ecs, TimelineNonEcsData } from '../../../graphql/types'; import { AddExceptionModalBaseProps } from '../../../common/components/exceptions/add_exception_modal'; import { getMappedNonEcsValue } from '../../../common/components/exceptions/helpers'; +import { isThresholdRule } from '../../../../common/detection_engine/utils'; export const buildAlertStatusFilter = (status: Status): Filter[] => [ { @@ -193,6 +196,7 @@ export const requiredFieldsForActions = [ 'signal.rule.query', 'signal.rule.to', 'signal.rule.id', + 'signal.rule.type', 'signal.original_event.kind', 'signal.original_event.module', @@ -317,6 +321,15 @@ export const getAlertActions = ({ return module === 'endpoint' && kind === 'alert'; }; + const exceptionsAreAllowed = () => { + const ruleTypes = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.type', + }); + const [ruleType] = ruleTypes as RuleType[]; + return !isMlRule(ruleType) && !isThresholdRule(ruleType); + }; + return [ { ...getInvestigateInResolverAction({ dispatch, timelineId }), @@ -386,7 +399,7 @@ export const getAlertActions = ({ } }, id: 'addException', - isActionDisabled: () => !canUserCRUD || !hasIndexWrite, + isActionDisabled: () => !canUserCRUD || !hasIndexWrite || !exceptionsAreAllowed(), dataTestSubj: 'add-exception-menu-item', ariaLabel: 'Add Exception', content: {i18n.ACTION_ADD_EXCEPTION}, From ba9a60738425a2948f6e074408a0e5d22d07c721 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 25 Aug 2020 19:48:18 -0600 Subject: [PATCH 021/216] Optimizes the index queries to not block the NodeJS event loop (#75716) ## Summary Before this PR you can see event loop block times of: ```ts formatIndexFields: 7986.884ms ``` After this PR you will see event loop block times of: ```ts formatIndexFields: 85.012ms ``` within the file: ```ts x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts ``` For the GraphQL query of `SourceQuery`/`IndexFields` This also fixes the issue of `unknown` being returned to the front end by removing code that is no longer functioning as it was intended. Ensure during testing of this PR that blank/default and non exist indexes within `securitySolution:defaultIndex` still work as expected. Before, notice the `unknown` instead of the `filebeat-*`: Screen Shot 2020-08-20 at 4 55 52 PM After: Screen Shot 2020-08-20 at 4 56 03 PM An explanation of how to see the block times for before and after --- For perf testing you first add timed testing to the file: ```ts x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts ``` Before this PR, around lines 42: ```ts console.time('formatIndexFields'); // <--- start timer const fields = formatIndexFields( responsesIndexFields, Object.keys(indexesAliasIndices) as IndexAlias[] ); console.timeEnd('formatIndexFields'); // <--- outputs the end timer return fields; ``` After this PR, around lines 42: ```ts console.time('formatIndexFields'); // <--- start timer const fields = await formatIndexFields(responsesIndexFields, indices); console.timeEnd('formatIndexFields'); // <--- outputs the end timer return fields; ``` And then reload the security solutions application web page here: ``` http://localhost:5601/app/security/timelines/default ``` Be sure to load it _twice_ for testing as NodeJS will sometimes report better numbers the second time as it does optimizations after the first time it encounters some code paths. You will begin to see numbers similar to this before this PR: ```ts formatIndexFields: 2553.279ms ``` This indicates that it is blocking the event loop for ~2.5 seconds befofe this fix. If you add additional indexes to your `securitySolution:defaultIndex` indexes that have additional fields then this amount will increase exponentially. For developers using our test servers I created two other indexes called delme-1 and delme-2 with additional mappings you can add like below ```ts apm-*-transaction*, auditbeat-*, endgame-*, filebeat-*, logs-*, packetbeat-*, winlogbeat-*, delme-1, delme-2 ``` Screen Shot 2020-08-21 at 8 21 50 PM Then you are going to see times approaching 8 seconds of blocking the event loop like so: ```ts formatIndexFields: 7986.884ms ``` After this fix on the first pass unoptimized it will report ```ts formatIndexFields: 373.082ms ``` Then after it optimizes the code paths on a second page load it will report ```ts formatIndexFields: 84.304ms ``` ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../elasticsearch_adapter.test.ts | 564 +++++++++++++++++- .../lib/index_fields/elasticsearch_adapter.ts | 203 ++++--- .../server/utils/beat_schema/index.test.ts | 22 +- .../server/utils/beat_schema/index.ts | 15 - .../server/utils/beat_schema/type.ts | 2 - 5 files changed, 692 insertions(+), 114 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.test.ts b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.test.ts index 20bc1387a3c4e7..e8883111c95f64 100644 --- a/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.test.ts @@ -6,16 +6,21 @@ import { sortBy } from 'lodash/fp'; -import { formatIndexFields } from './elasticsearch_adapter'; +import { + formatIndexFields, + formatFirstFields, + formatSecondFields, + createFieldItem, +} from './elasticsearch_adapter'; import { mockAuditbeatIndexField, mockFilebeatIndexField, mockPacketbeatIndexField } from './mock'; describe('Index Fields', () => { describe('formatIndexFields', () => { - test('Test Basic functionality', async () => { + test('Basic functionality', async () => { expect( sortBy( 'name', - formatIndexFields( + await formatIndexFields( [mockAuditbeatIndexField, mockFilebeatIndexField, mockPacketbeatIndexField], ['auditbeat', 'filebeat', 'packetbeat'] ) @@ -130,4 +135,557 @@ describe('Index Fields', () => { ); }); }); + + describe('formatFirstFields', () => { + test('Basic functionality', async () => { + const fields = await formatFirstFields( + [mockAuditbeatIndexField, mockFilebeatIndexField, mockPacketbeatIndexField], + ['auditbeat', 'filebeat', 'packetbeat'] + ); + expect(fields).toEqual([ + { + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + footnote: '', + group: 1, + level: 'core', + name: '_id', + required: true, + type: 'string', + searchable: true, + aggregatable: false, + readFromDocValues: true, + category: '_id', + indexes: ['auditbeat'], + }, + { + description: + 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', + example: 'auditbeat-8.0.0-2019.02.19-000001', + footnote: '', + group: 1, + level: 'core', + name: '_index', + required: true, + type: 'string', + searchable: true, + aggregatable: true, + readFromDocValues: true, + category: '_index', + indexes: ['auditbeat'], + }, + { + description: + 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + name: '@timestamp', + type: 'date', + searchable: true, + aggregatable: true, + category: 'base', + indexes: ['auditbeat'], + }, + { + description: + 'Ephemeral identifier of this agent (if one exists).\n\nThis id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + name: 'agent.ephemeral_id', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat'], + }, + { + description: + 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', + example: 'foo', + name: 'agent.name', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat'], + }, + { + description: + 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', + example: 'filebeat', + name: 'agent.type', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat'], + }, + { + description: 'Version of the agent.', + example: '6.0.0-rc2', + name: 'agent.version', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat'], + }, + { + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + footnote: '', + group: 1, + level: 'core', + name: '_id', + required: true, + type: 'string', + searchable: true, + aggregatable: false, + readFromDocValues: true, + category: '_id', + indexes: ['filebeat'], + }, + { + description: + 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', + example: 'auditbeat-8.0.0-2019.02.19-000001', + footnote: '', + group: 1, + level: 'core', + name: '_index', + required: true, + type: 'string', + searchable: true, + aggregatable: true, + readFromDocValues: true, + category: '_index', + indexes: ['filebeat'], + }, + { + description: + 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + name: '@timestamp', + type: 'date', + searchable: true, + aggregatable: true, + category: 'base', + indexes: ['filebeat'], + }, + { + name: 'agent.hostname', + searchable: true, + type: 'string', + aggregatable: true, + category: 'agent', + indexes: ['filebeat'], + }, + { + description: + 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', + example: 'foo', + name: 'agent.name', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['filebeat'], + }, + { + description: 'Version of the agent.', + example: '6.0.0-rc2', + name: 'agent.version', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['filebeat'], + }, + { + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + footnote: '', + group: 1, + level: 'core', + name: '_id', + required: true, + type: 'string', + searchable: true, + aggregatable: false, + readFromDocValues: true, + category: '_id', + indexes: ['packetbeat'], + }, + { + description: + 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', + example: 'auditbeat-8.0.0-2019.02.19-000001', + footnote: '', + group: 1, + level: 'core', + name: '_index', + required: true, + type: 'string', + searchable: true, + aggregatable: true, + readFromDocValues: true, + category: '_index', + indexes: ['packetbeat'], + }, + { + description: + 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + name: '@timestamp', + type: 'date', + searchable: true, + aggregatable: true, + category: 'base', + indexes: ['packetbeat'], + }, + { + description: + 'Unique identifier of this agent (if one exists).\n\nExample: For Beats this would be beat.id.', + example: '8a4f500d', + name: 'agent.id', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['packetbeat'], + }, + { + description: + 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', + example: 'filebeat', + name: 'agent.type', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['packetbeat'], + }, + ]); + }); + }); + + describe('formatSecondFields', () => { + test('Basic functionality', async () => { + const fields = await formatSecondFields([ + { + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + name: '_id', + type: 'string', + searchable: true, + aggregatable: false, + category: '_id', + indexes: ['auditbeat'], + }, + { + description: + 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', + example: 'auditbeat-8.0.0-2019.02.19-000001', + name: '_index', + type: 'string', + searchable: true, + aggregatable: true, + category: '_index', + indexes: ['auditbeat'], + }, + { + description: + 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + name: '@timestamp', + type: 'date', + searchable: true, + aggregatable: true, + category: 'base', + indexes: ['auditbeat'], + }, + { + description: + 'Ephemeral identifier of this agent (if one exists).\n\nThis id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + name: 'agent.ephemeral_id', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat'], + }, + { + description: + 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', + example: 'foo', + name: 'agent.name', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat'], + }, + { + description: + 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', + example: 'filebeat', + name: 'agent.type', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat'], + }, + { + description: 'Version of the agent.', + example: '6.0.0-rc2', + name: 'agent.version', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat'], + }, + { + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + name: '_id', + type: 'string', + searchable: true, + aggregatable: false, + category: '_id', + indexes: ['filebeat'], + }, + { + description: + 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', + example: 'auditbeat-8.0.0-2019.02.19-000001', + name: '_index', + type: 'string', + searchable: true, + aggregatable: true, + category: '_index', + indexes: ['filebeat'], + }, + { + description: + 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + name: '@timestamp', + type: 'date', + searchable: true, + aggregatable: true, + category: 'base', + indexes: ['filebeat'], + }, + { + name: 'agent.hostname', + searchable: true, + type: 'string', + aggregatable: true, + category: 'agent', + indexes: ['filebeat'], + }, + { + description: + 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', + example: 'foo', + name: 'agent.name', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['filebeat'], + }, + { + description: 'Version of the agent.', + example: '6.0.0-rc2', + name: 'agent.version', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['filebeat'], + }, + { + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + name: '_id', + type: 'string', + searchable: true, + aggregatable: false, + category: '_id', + indexes: ['packetbeat'], + }, + { + description: + 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', + example: 'auditbeat-8.0.0-2019.02.19-000001', + name: '_index', + type: 'string', + searchable: true, + aggregatable: true, + category: '_index', + indexes: ['packetbeat'], + }, + { + description: + 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + name: '@timestamp', + type: 'date', + searchable: true, + aggregatable: true, + category: 'base', + indexes: ['packetbeat'], + }, + { + description: + 'Unique identifier of this agent (if one exists).\n\nExample: For Beats this would be beat.id.', + example: '8a4f500d', + name: 'agent.id', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['packetbeat'], + }, + { + description: + 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', + example: 'filebeat', + name: 'agent.type', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['packetbeat'], + }, + ]); + expect(fields).toEqual([ + { + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + name: '_id', + type: 'string', + searchable: true, + aggregatable: false, + category: '_id', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + }, + { + description: + 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', + example: 'auditbeat-8.0.0-2019.02.19-000001', + name: '_index', + type: 'string', + searchable: true, + aggregatable: true, + category: '_index', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + }, + { + description: + 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + name: '@timestamp', + type: 'date', + searchable: true, + aggregatable: true, + category: 'base', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + }, + { + description: + 'Ephemeral identifier of this agent (if one exists).\n\nThis id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + name: 'agent.ephemeral_id', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat'], + }, + { + description: + 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', + example: 'foo', + name: 'agent.name', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat', 'filebeat'], + }, + { + description: + 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', + example: 'filebeat', + name: 'agent.type', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat', 'packetbeat'], + }, + { + description: 'Version of the agent.', + example: '6.0.0-rc2', + name: 'agent.version', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['auditbeat', 'filebeat'], + }, + { + name: 'agent.hostname', + searchable: true, + type: 'string', + aggregatable: true, + category: 'agent', + indexes: ['filebeat'], + }, + { + description: + 'Unique identifier of this agent (if one exists).\n\nExample: For Beats this would be beat.id.', + example: '8a4f500d', + name: 'agent.id', + type: 'string', + searchable: true, + aggregatable: true, + category: 'agent', + indexes: ['packetbeat'], + }, + ]); + }); + }); + + describe('createFieldItem', () => { + test('Basic functionality', () => { + const item = createFieldItem( + ['auditbeat'], + { + name: '_id', + type: 'string', + searchable: true, + aggregatable: false, + }, + 0 + ); + expect(item).toEqual({ + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + footnote: '', + group: 1, + level: 'core', + name: '_id', + required: true, + type: 'string', + searchable: true, + aggregatable: false, + category: '_id', + indexes: ['auditbeat'], + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts index bb0a4b9e2ba9b4..777b1cf3bb80d7 100644 --- a/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts @@ -4,45 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty, get } from 'lodash/fp'; +import { isEmpty } from 'lodash/fp'; import { IndexField } from '../../graphql/types'; -import { - baseCategoryFields, - getDocumentation, - getIndexAlias, - hasDocumentation, - IndexAlias, -} from '../../utils/beat_schema'; +import { baseCategoryFields, getDocumentation, hasDocumentation } from '../../utils/beat_schema'; import { FrameworkAdapter, FrameworkRequest } from '../framework'; import { FieldsAdapter, IndexFieldDescriptor } from './types'; export class ElasticsearchIndexFieldAdapter implements FieldsAdapter { constructor(private readonly framework: FrameworkAdapter) {} - public async getIndexFields(request: FrameworkRequest, indices: string[]): Promise { const indexPatternsService = this.framework.getIndexPatternsService(request); - const indexesAliasIndices = indices.reduce>((accumulator, indice) => { - const key = getIndexAlias(indices, indice); - - if (get(key, accumulator)) { - accumulator[key] = [...accumulator[key], indice]; - } else { - accumulator[key] = [indice]; - } - return accumulator; - }, {}); - const responsesIndexFields: IndexFieldDescriptor[][] = await Promise.all( - Object.values(indexesAliasIndices).map((indicesByGroup) => - indexPatternsService.getFieldsForWildcard({ - pattern: indicesByGroup, - }) - ) - ); - return formatIndexFields( - responsesIndexFields, - Object.keys(indexesAliasIndices) as IndexAlias[] + const responsesIndexFields = await Promise.all( + indices.map((index) => { + return indexPatternsService.getFieldsForWildcard({ + pattern: index, + }); + }) ); + return formatIndexFields(responsesIndexFields, indices); } } @@ -63,51 +43,128 @@ const missingFields = [ }, ]; -export const formatIndexFields = ( +/** + * Creates a single field item. + * + * This is a mutatious HOT CODE PATH function that will have array sizes up to 4.7 megs + * in size at a time calling this function repeatedly. This function should be as optimized as possible + * and should avoid any and all creation of new arrays, iterating over the arrays or performing + * any n^2 operations. + * @param indexesAlias The index alias + * @param index The index its self + * @param indexesAliasIdx The index within the alias + */ +export const createFieldItem = ( + indexesAlias: string[], + index: IndexFieldDescriptor, + indexesAliasIdx: number +): IndexField => { + const alias = indexesAlias[indexesAliasIdx]; + const splitName = index.name.split('.'); + const category = baseCategoryFields.includes(splitName[0]) ? 'base' : splitName[0]; + return { + ...(hasDocumentation(alias, index.name) ? getDocumentation(alias, index.name) : {}), + ...index, + category, + indexes: [alias], + }; +}; + +/** + * This is a mutatious HOT CODE PATH function that will have array sizes up to 4.7 megs + * in size at a time when being called. This function should be as optimized as possible + * and should avoid any and all creation of new arrays, iterating over the arrays or performing + * any n^2 operations. The `.push`, and `forEach` operations are expected within this function + * to speed up performance. + * + * This intentionally waits for the next tick on the event loop to process as the large 4.7 megs + * has already consumed a lot of the event loop processing up to this function and we want to give + * I/O opportunity to occur by scheduling this on the next loop. + * @param responsesIndexFields The response index fields to loop over + * @param indexesAlias The index aliases such as filebeat-* + */ +export const formatFirstFields = async ( responsesIndexFields: IndexFieldDescriptor[][], - indexesAlias: IndexAlias[] -): IndexField[] => - responsesIndexFields - .reduce( - (accumulator: IndexField[], indexFields: IndexFieldDescriptor[], indexesAliasIdx: number) => [ - ...accumulator, - ...[...missingFields, ...indexFields].reduce( - (itemAccumulator: IndexField[], index: IndexFieldDescriptor) => { - const alias: IndexAlias = indexesAlias[indexesAliasIdx]; - const splitName = index.name.split('.'); - const category = baseCategoryFields.includes(splitName[0]) ? 'base' : splitName[0]; - return [ - ...itemAccumulator, - { - ...(hasDocumentation(alias, index.name) ? getDocumentation(alias, index.name) : {}), - ...index, - category, - indexes: [alias], - } as IndexField, - ]; + indexesAlias: string[] +): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + resolve( + responsesIndexFields.reduce( + ( + accumulator: IndexField[], + indexFields: IndexFieldDescriptor[], + indexesAliasIdx: number + ) => { + missingFields.forEach((index) => { + const item = createFieldItem(indexesAlias, index, indexesAliasIdx); + accumulator.push(item); + }); + indexFields.forEach((index) => { + const item = createFieldItem(indexesAlias, index, indexesAliasIdx); + accumulator.push(item); + }); + return accumulator; }, [] - ), - ], - [] - ) - .reduce((accumulator: IndexField[], indexfield: IndexField) => { - const alreadyExistingIndexField = accumulator.findIndex( - (acc) => acc.name === indexfield.name + ) ); - if (alreadyExistingIndexField > -1) { - const existingIndexField = accumulator[alreadyExistingIndexField]; - return [ - ...accumulator.slice(0, alreadyExistingIndexField), - { - ...existingIndexField, - description: isEmpty(existingIndexField.description) - ? indexfield.description - : existingIndexField.description, - indexes: Array.from(new Set([...existingIndexField.indexes, ...indexfield.indexes])), - }, - ...accumulator.slice(alreadyExistingIndexField + 1), - ]; - } - return [...accumulator, indexfield]; - }, []); + }); + }); +}; + +/** + * This is a mutatious HOT CODE PATH function that will have array sizes up to 4.7 megs + * in size at a time when being called. This function should be as optimized as possible + * and should avoid any and all creation of new arrays, iterating over the arrays or performing + * any n^2 operations. The `.push`, and `forEach` operations are expected within this function + * to speed up performance. The "indexFieldNameHash" side effect hash avoids additional expensive n^2 + * look ups. + * + * This intentionally waits for the next tick on the event loop to process as the large 4.7 megs + * has already consumed a lot of the event loop processing up to this function and we want to give + * I/O opportunity to occur by scheduling this on the next loop. + * @param fields The index fields to create the secondary fields for + */ +export const formatSecondFields = async (fields: IndexField[]): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + const indexFieldNameHash: Record = {}; + const reduced = fields.reduce((accumulator: IndexField[], indexfield: IndexField) => { + const alreadyExistingIndexField = indexFieldNameHash[indexfield.name]; + if (alreadyExistingIndexField != null) { + const existingIndexField = accumulator[alreadyExistingIndexField]; + if (isEmpty(accumulator[alreadyExistingIndexField].description)) { + accumulator[alreadyExistingIndexField].description = indexfield.description; + } + accumulator[alreadyExistingIndexField].indexes = Array.from( + new Set([...existingIndexField.indexes, ...indexfield.indexes]) + ); + return accumulator; + } + accumulator.push(indexfield); + indexFieldNameHash[indexfield.name] = accumulator.length - 1; + return accumulator; + }, []); + resolve(reduced); + }); + }); +}; + +/** + * Formats the index fields into a format the UI wants. + * + * NOTE: This will have array sizes up to 4.7 megs in size at a time when being called. + * This function should be as optimized as possible and should avoid any and all creation + * of new arrays, iterating over the arrays or performing any n^2 operations. + * @param responsesIndexFields The response index fields to format + * @param indexesAlias The index alias + */ +export const formatIndexFields = async ( + responsesIndexFields: IndexFieldDescriptor[][], + indexesAlias: string[] +): Promise => { + const fields = await formatFirstFields(responsesIndexFields, indexesAlias); + const secondFields = await formatSecondFields(fields); + return secondFields; +}; diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts index 5f002aa7fad7b7..29944edf382f43 100644 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts +++ b/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts @@ -6,7 +6,7 @@ import { cloneDeep, isArray } from 'lodash/fp'; -import { convertSchemaToAssociativeArray, getIndexSchemaDoc, getIndexAlias } from '.'; +import { convertSchemaToAssociativeArray, getIndexSchemaDoc } from '.'; import { auditbeatSchema, filebeatSchema, packetbeatSchema } from './8.0.0'; import { Schema } from './type'; @@ -394,24 +394,4 @@ describe('Schema Beat', () => { ]); }); }); - - describe('getIndexAlias', () => { - test('getIndexAlias handles values with leading wildcard', () => { - const leadingWildcardIndex = '*-auditbeat-*'; - const result = getIndexAlias([leadingWildcardIndex], leadingWildcardIndex); - expect(result).toBe(leadingWildcardIndex); - }); - - test('getIndexAlias no match returns "unknown" string', () => { - const index = 'auditbeat-*'; - const result = getIndexAlias([index], 'hello'); - expect(result).toBe('unknown'); - }); - - test('empty index should not cause an error to return although it will cause an invalid regular expression to occur', () => { - const index = ''; - const result = getIndexAlias([index], 'hello'); - expect(result).toBe('unknown'); - }); - }); }); diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts index 6ec15d328714d3..58627a199a1814 100644 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts +++ b/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts @@ -76,21 +76,6 @@ const convertFieldsToAssociativeArray = ( }, {}) : {}; -export const getIndexAlias = (defaultIndex: string[], indexName: string): string => { - try { - const found = defaultIndex.find((index) => `\\${indexName}`.match(`\\${index}`) != null); - if (found != null) { - return found; - } else { - return 'unknown'; - } - } catch (error) { - // if we encounter an error because the index contains invalid regular expressions then we should return an unknown - // rather than blow up with a toaster error upstream - return 'unknown'; - } -}; - export const getIndexSchemaDoc = memoize((index: string) => { if (index.match('auditbeat') != null) { return { diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/type.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/type.ts index 2b7be8f4b7539e..722589ce7e2bb4 100644 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/type.ts +++ b/x-pack/plugins/security_solution/server/utils/beat_schema/type.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export type IndexAlias = 'auditbeat' | 'filebeat' | 'packetbeat' | 'ecs' | 'winlogbeat' | 'unknown'; - /* * BEAT Interface * From eecf4aa71f27b0fc22113f73ddf745a4fcd59cb5 Mon Sep 17 00:00:00 2001 From: Justin Ibarra Date: Tue, 25 Aug 2020 23:25:07 -0500 Subject: [PATCH 022/216] [Detection Rules] Add 7.9.1 rules (#75939) * increase lookback (`from`) and bump versions --- .../command_and_control_certutil_network_connection.json | 3 ++- .../credential_access_credential_dumping_msbuild.json | 3 ++- .../prepackaged_rules/credential_access_tcpdump_activity.json | 3 ++- ...on_adding_the_hidden_file_attribute_with_via_attribexe.json | 3 ++- ...efense_evasion_attempt_to_disable_iptables_or_firewall.json | 3 ++- .../defense_evasion_attempt_to_disable_syslog_service.json | 3 ++- ...evasion_base16_or_base32_encoding_or_decoding_activity.json | 3 ++- .../defense_evasion_base64_encoding_or_decoding_activity.json | 3 ++- .../defense_evasion_clearing_windows_event_logs.json | 3 ++- .../defense_evasion_delete_volume_usn_journal_with_fsutil.json | 3 ++- .../defense_evasion_deleting_backup_catalogs_with_wbadmin.json | 3 ++- .../defense_evasion_deletion_of_bash_command_line_history.json | 3 ++- .../defense_evasion_disable_selinux_attempt.json | 3 ++- ...ense_evasion_disable_windows_firewall_rules_with_netsh.json | 3 ++- ...efense_evasion_encoding_or_decoding_files_via_certutil.json | 3 ++- ...efense_evasion_execution_msbuild_started_by_office_app.json | 3 ++- .../defense_evasion_execution_msbuild_started_by_script.json | 3 ++- ...se_evasion_execution_msbuild_started_by_system_process.json | 3 ++- .../defense_evasion_execution_msbuild_started_renamed.json | 3 ++- ...fense_evasion_execution_msbuild_started_unusal_process.json | 3 ++- .../defense_evasion_file_deletion_via_shred.json | 3 ++- .../defense_evasion_file_mod_writable_dir.json | 3 ++- .../defense_evasion_hex_encoding_or_decoding_activity.json | 3 ++- .../prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json | 3 ++- .../defense_evasion_kernel_module_removal.json | 3 ++- ...defense_evasion_misc_lolbin_connecting_to_the_internet.json | 3 ++- .../defense_evasion_modification_of_boot_config.json | 3 ++- ...fense_evasion_volume_shadow_copy_deletion_via_vssadmin.json | 3 ++- .../defense_evasion_volume_shadow_copy_deletion_via_wmic.json | 3 ++- .../prepackaged_rules/discovery_kernel_module_enumeration.json | 3 ++- .../discovery_net_command_system_account.json | 3 ++- .../discovery_virtual_machine_fingerprinting.json | 3 ++- .../rules/prepackaged_rules/discovery_whoami_commmand.json | 3 ++- .../execution_command_prompt_connecting_to_the_internet.json | 3 ++- .../execution_command_shell_started_by_powershell.json | 3 ++- .../execution_command_shell_started_by_svchost.json | 3 ++- ...tml_help_executable_program_connecting_to_the_internet.json | 3 ++- .../prepackaged_rules/execution_local_service_commands.json | 3 ++- .../execution_msbuild_making_network_connections.json | 3 ++- .../execution_mshta_making_network_connections.json | 3 ++- .../rules/prepackaged_rules/execution_msxsl_network.json | 3 ++- .../rules/prepackaged_rules/execution_perl_tty_shell.json | 3 ++- .../execution_psexec_lateral_movement_command.json | 3 ++- .../rules/prepackaged_rules/execution_python_tty_shell.json | 3 ++- ...ion_register_server_program_connecting_to_the_internet.json | 3 ++- .../execution_script_executing_powershell.json | 3 ++- .../execution_suspicious_ms_office_child_process.json | 3 ++- .../execution_suspicious_ms_outlook_child_process.json | 3 ++- .../prepackaged_rules/execution_suspicious_pdf_reader.json | 3 ++- .../execution_unusual_network_connection_via_rundll32.json | 3 ++- .../execution_unusual_process_network_connection.json | 3 ++- .../prepackaged_rules/execution_via_net_com_assemblies.json | 3 ++- .../lateral_movement_direct_outbound_smb_connection.json | 3 ++- .../lateral_movement_telnet_network_activity_external.json | 3 ++- .../lateral_movement_telnet_network_activity_internal.json | 3 ++- .../rules/prepackaged_rules/linux_hping_activity.json | 3 ++- .../rules/prepackaged_rules/linux_iodine_activity.json | 3 ++- .../rules/prepackaged_rules/linux_mknod_activity.json | 3 ++- .../prepackaged_rules/linux_netcat_network_connection.json | 3 ++- .../rules/prepackaged_rules/linux_nmap_activity.json | 3 ++- .../rules/prepackaged_rules/linux_nping_activity.json | 3 ++- .../linux_process_started_in_temp_directory.json | 3 ++- .../rules/prepackaged_rules/linux_socat_activity.json | 3 ++- .../rules/prepackaged_rules/linux_strace_activity.json | 3 ++- .../persistence_adobe_hijack_persistence.json | 3 ++- .../prepackaged_rules/persistence_kernel_module_activity.json | 3 ++- .../persistence_local_scheduled_task_commands.json | 3 ++- .../persistence_shell_activity_by_web_server.json | 3 ++- .../persistence_system_shells_via_services.json | 3 ++- .../prepackaged_rules/persistence_user_account_creation.json | 3 ++- .../privilege_escalation_setgid_bit_set_via_chmod.json | 3 ++- .../privilege_escalation_setuid_bit_set_via_chmod.json | 3 ++- .../privilege_escalation_sudoers_file_mod.json | 3 ++- .../privilege_escalation_uac_bypass_event_viewer.json | 3 ++- .../privilege_escalation_unusual_parentchild_relationship.json | 3 ++- 75 files changed, 150 insertions(+), 75 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json index 25274928aa2b7a..a8be0fe97524e6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies certutil.exe making a network connection. Adversaries could abuse certutil.exe to download a certificate, or malware, from a remote URL.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json index 6be1f037f967e1..f2032b5bef2180 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json @@ -6,6 +6,7 @@ "false_positives": [ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json index d5b069f7b81e79..306a38f5d2a28f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Some normal use of this command may originate from server or network administrators engaged in network troubleshooting." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -54,5 +55,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json index b22b74ebc53bc2..c80f24a21d958d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Adversaries can add the 'hidden' attribute to files to hide them from the user in an attempt to evade detection.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -51,5 +52,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json index e2ba81da917b3e..4d4f10bbaa5997 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Adversaries may attempt to disable the iptables or firewall service in an attempt to affect how a host is allowed to receive or send network traffic.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json index 4f4a9aacd79aae..3c34b04a77a500 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Adversaries may attempt to disable the syslog service in an attempt to an attempt to disrupt event logging and evade detection by security controls.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json index 5bcc4a00ccd820..3cdfac92572b19 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Automated tools such as Jenkins may encode or decode files as part of their normal behavior. These events can be filtered by the process executable or username values." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -54,5 +55,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json index a17fd6d2702ddc..2d26d867b8718e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Automated tools such as Jenkins may encode or decode files as part of their normal behavior. These events can be filtered by the process executable or username values." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -54,5 +55,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json index cf09bc512916fa..60ce575148f4c1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies attempts to clear Windows event log stores. This is often done by attackers in an attempt to evade detection or destroy forensic evidence on a system.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json index 0c82444dd9397b..50213b9f1a42c4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies use of the fsutil.exe to delete the volume USNJRNL. This technique is used by attackers to eliminate evidence of files created during post-exploitation activities.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json index c76c5f20fa88bd..026735f413eabd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies use of the wbadmin.exe to delete the backup catalog. Ransomware and other malware may do this to prevent system recovery.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json index b38ed94e132e1f..85d8bdcb2582f4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Adversaries may attempt to clear the bash command line history in an attempt to evade detection or forensic investigations.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json index 229a03de396008..d107c0b2620919 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies potential attempts to disable Security-Enhanced Linux (SELinux), which is a Linux kernel security feature to support access control policies. Adversaries may disable security tools to avoid possible detection of their tools and activities.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json index 4800e87c180e2a..6fbf9ca800f795 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies use of the netsh.exe to disable or weaken the local firewall. Attackers will use this command line tool to disable the firewall during troubleshooting or to enable network mobility.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json index 075dd13d9819b6..0d47aab2c64bde 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies the use of certutil.exe to encode or decode data. CertUtil is a native Windows component which is part of Certificate Services. CertUtil is often abused by attackers to encode or decode base64 data for stealthier command and control or exfiltration.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json index 133863f8e2148f..df7fc85b63d4ae 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json @@ -6,6 +6,7 @@ "false_positives": [ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual. It is quite unusual for this program to be started by an Office application like Word or Excel." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -57,5 +58,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json index 85d348bb14be02..aa4674f75bcd09 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json @@ -6,6 +6,7 @@ "false_positives": [ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -54,5 +55,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json index 38482c0a70fc94..da7d91933bd2a0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json @@ -6,6 +6,7 @@ "false_positives": [ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -54,5 +55,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json index 7db683caf2bb22..8e4f7366a7657f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json @@ -6,6 +6,7 @@ "false_positives": [ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json index 1c4666955dde08..4f353a6ff9e6f1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json @@ -6,6 +6,7 @@ "false_positives": [ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual. If a build system triggers this rule it can be exempted by process, user or host name." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -42,5 +43,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json index c375ea7b19b376..5b02f63a1c7f70 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Malware or other files dropped or created on a system by an adversary may leave traces behind as to what was done within a network and how. Adversaries may remove these files over the course of an intrusion to keep their footprint low or remove them at the end as part of the post-intrusion cleanup process.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json index 22090e1a241e7b..8ee2d4fda7bf84 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json @@ -6,6 +6,7 @@ "false_positives": [ "Certain programs or applications may modify files or change ownership in writable directories. These can be exempted by username." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json index 00491937e9aae3..f5345b2276e8a7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Automated tools such as Jenkins may encode or decode files as part of their normal behavior. These events can be filtered by the process executable or username values." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -54,5 +55,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json index 16a398011fc536..e66968a50709e8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json @@ -6,6 +6,7 @@ "false_positives": [ "Certain tools may create hidden temporary files or directories upon installation or as part of their normal behavior. These events can be filtered by the process arguments, username, or process name values." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -55,5 +56,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json index 11781cb7195999..ad751a10314378 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json @@ -6,6 +6,7 @@ "false_positives": [ "There is usually no reason to remove modules, but some buggy modules require it. These can be exempted by username. Note that some Linux distributions are not built to support the removal of modules at all." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -57,5 +58,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json index 7d931725fa6ebb..5b5f69a0aef744 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Binaries signed with trusted digital certificates can execute on Windows systems protected by digital signature validation. Adversaries may use these binaries to 'live off the land' and execute malicious files that could bypass application allowlists and signature validation.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -51,5 +52,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json index 1bffe7a1cfc248..6025fc5ca6452d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies use of bcdedit.exe to delete boot configuration data. This tactic is sometimes used as by malware or an attacker as a destructive technique.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json index f3cc5c2eec8a32..8a504281b03f76 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies use of vssadmin.exe for shadow copy deletion on endpoints. This commonly occurs in tandem with ransomware or other destructive attacks.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json index 334276142ca428..2ae938bb34104d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies use of wmic.exe for shadow copy deletion on endpoints. This commonly occurs in tandem with ransomware or other destructive attacks.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json index 0e4bea426c5913..af9c4b5409964d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json @@ -6,6 +6,7 @@ "false_positives": [ "Security tools and device drivers may run these programs in order to enumerate kernel modules. Use of these programs by ordinary users is uncommon. These can be exempted by process name or username." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json index 6ac2bbf355961b..f1a214b7cd436e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies the SYSTEM account using the Net utility. The Net utility is a component of the Windows operating system. It is used in command line operations for control of users, groups, services, and network connections.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json index e73aa5f4566a7a..d913a92e2ee0e3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json @@ -6,6 +6,7 @@ "false_positives": [ "Certain tools or automated software may enumerate hardware information. These tools can be exempted via user name or process arguments to eliminate potential noise." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json index 00171867871390..a8b34362d95793 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json @@ -6,6 +6,7 @@ "false_positives": [ "Security testing tools and frameworks may run this command. Some normal use of this command may originate from automation tools and frameworks." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json index 0ba6480fe42a10..46208f3753fa14 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json @@ -6,6 +6,7 @@ "false_positives": [ "Administrators may use the command prompt for regular administrative tasks. It's important to baseline your environment for network connections being made from the command prompt to determine any abnormal use of this tool." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -54,5 +55,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json index 2d3edb0f5f6cce..c619d8f764bc42 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies a suspicious parent child process relationship with cmd.exe descending from PowerShell.exe.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -51,5 +52,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json index 3a4b4915f3c8b7..140212e4148eb8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies a suspicious parent child process relationship with cmd.exe descending from svchost.exe", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json index a2eb76b9831f0b..963c6b2e53ed64 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Compiled HTML files (.chm) are commonly distributed as part of the Microsoft HTML Help system. Adversaries may conceal malicious code in a CHM file and deliver it to a victim for execution. CHM content is loaded by the HTML Help executable program (hh.exe).", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -51,5 +52,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json index e43ab9de86ef76..7b20cefdc67f0a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies use of sc.exe to create, modify, or start services on remote hosts. This could be indicative of adversary lateral movement but will be noisy if commonly done by admins.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json index 9d480259d49de5..629efa90a71ea9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies MsBuild.exe making outbound network connections. This may indicate adversarial activity as MsBuild is often leveraged by adversaries to execute code and evade detection.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json index cdef5f16e5cd76..7af823070889f4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies mshta.exe making a network connection. This may indicate adversarial activity as mshta.exe is often leveraged by adversaries to execute malicious scripts and evade detection.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json index d501bda08c3a54..1dc75575636fbf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies msxsl.exe making a network connection. This may indicate adversarial activity as msxsl.exe is often leveraged by adversaries to execute malicious scripts and evade detection.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json index e82b42869e44d8..9b6ee099116f39 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies when a terminal (tty) is spawned via Perl. Attackers may upgrade a simple reverse shell to a fully interactive tty after obtaining initial access to a host.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json index e4c84fd3c3b83d..f647d8d00e0844 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json @@ -6,6 +6,7 @@ "false_positives": [ "PsExec is a dual-use tool that can be used for benign or malicious activity. It's important to baseline your environment to determine the amount of noise to expect from this tool." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -54,5 +55,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json index 3aa9ac20bba9e8..d9c26a9c26cc97 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies when a terminal (tty) is spawned via Python. Attackers may upgrade a simple reverse shell to a fully interactive tty after obtaining initial access to a host.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json index 0a1ba97bd01ea4..b3b6a2b0c7fab2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json @@ -6,6 +6,7 @@ "false_positives": [ "Security testing may produce events like this. Activity of this kind performed by non-engineers and ordinary users is unusual." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -54,5 +55,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json index 7305247192f57c..6d7f11f01fae02 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies a PowerShell process launched by either cscript.exe or wscript.exe. Observing Windows scripting processes executing a PowerShell script, may be indicative of malicious activity.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json index 7ff8eb9424d5fc..005a0c38c8a8b9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies suspicious child processes of frequently targeted Microsoft Office applications (Word, PowerPoint, Excel). These child processes are often launched during exploitation of Office applications or from documents with malicious macros.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json index e923407765f8fe..74e21c7d174792 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies suspicious child processes of Microsoft Outlook. These child processes are often associated with spear phishing activity.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json index 24a744ce308322..adf1a76bfb9011 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies suspicious child processes of PDF reader applications. These child processes are often launched via exploitation of PDF applications or social engineering.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json index 529f2199e46dc6..11041593506557 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies unusual instances of rundll32.exe making outbound network connections. This may indicate adversarial activity and may identify malicious DLLs.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json index 69a25b3b24bace..854ecc40d76abc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies network activity from unexpected system applications. This may indicate adversarial activity as these applications are often leveraged by adversaries to execute code and evade detection.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json index cae5d1b7e0f1fd..d9dcbfe25a4c2d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "RegSvcs.exe and RegAsm.exe are Windows command line utilities that are used to register .NET Component Object Model (COM) assemblies. Adversaries can use RegSvcs.exe and RegAsm.exe to proxy execution of code through a trusted Windows utility.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -51,5 +52,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json index 8a68b26abad200..e4014b22a6c091 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies unexpected processes making network connections over port 445. Windows File Sharing is typically implemented over Server Message Block (SMB), which communicates between hosts using port 445. When legitimate, these network connections are established by the kernel. Processes making 445/tcp connections may be port scanners, exploits, or suspicious user-level processes moving laterally.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json index 2ea75dbd758cb8..e4804329c0f307 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json @@ -6,6 +6,7 @@ "false_positives": [ "Telnet can be used for both benign or malicious purposes. Telnet is included by default in some Linux distributions, so its presence is not inherently suspicious. The use of Telnet to manage devices remotely has declined in recent years in favor of more secure protocols such as SSH. Telnet usage by non-automated tools or frameworks may be suspicious." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json index 4379759608aba3..30312987d166c3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json @@ -6,6 +6,7 @@ "false_positives": [ "Telnet can be used for both benign or malicious purposes. Telnet is included by default in some Linux distributions, so its presence is not inherently suspicious. The use of Telnet to manage devices remotely has declined in recent years in favor of more secure protocols such as SSH. Telnet usage by non-automated tools or frameworks may be suspicious." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json index 24104439cd0ec0..3a5c4d9e69d496 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Normal use of hping is uncommon apart from security testing and research. Use by non-security engineers is very uncommon." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -25,5 +26,5 @@ "Linux" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json index 73bf20a5a175e7..63c82c5662df66 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Normal use of Iodine is uncommon apart from security testing and research. Use by non-security engineers is very uncommon." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -25,5 +26,5 @@ "Linux" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json index 1895caf4dea812..37d5468c773bf2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Mknod is a Linux system program. Some normal use of this program, at varying levels of frequency, may originate from scripts, automation tools, and frameworks. Usage by web servers is more likely to be suspicious." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -25,5 +26,5 @@ "Linux" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json index ac46bcbdbc083d..bce10f640691b7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json @@ -6,6 +6,7 @@ "false_positives": [ "Netcat is a dual-use tool that can be used for benign or malicious activity. Netcat is included in some Linux distributions so its presence is not necessarily suspicious. Some normal use of this program, while uncommon, may originate from scripts, automation tools, and frameworks." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -27,5 +28,5 @@ "Linux" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json index 2825dc28ad18f6..5d9e338425bdae 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Security testing tools and frameworks may run `Nmap` in the course of security auditing. Some normal use of this command may originate from security engineers and network or server administrators. Use of nmap by ordinary users is uncommon." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -25,5 +26,5 @@ "Linux" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json index 234a09e9607b9b..bd019c9a80c4c5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Some normal use of this command may originate from security engineers and network or server administrators, but this is usually not routine or unannounced. Use of `Nping` by non-engineers or ordinary users is uncommon." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -25,5 +26,5 @@ "Linux" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json index 759622804444e3..f0bbc892d7d9c4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json @@ -6,6 +6,7 @@ "false_positives": [ "Build systems, like Jenkins, may start processes in the `/tmp` directory. These can be exempted by name or by username." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -22,5 +23,5 @@ "Linux" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json index cd38aff3f21642..fac03d31b57bfe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Socat is a dual-use tool that can be used for benign or malicious activity. Some normal use of this program, at varying levels of frequency, may originate from scripts, automation tools, and frameworks. Usage by web servers is more likely to be suspicious." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -25,5 +26,5 @@ "Linux" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json index 7fcb9f915c5608..c1b782d612ccbc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Strace is a dual-use tool that can be used for benign or malicious activity. Some normal use of this command may originate from developers or SREs engaged in debugging or system call tracing." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -25,5 +26,5 @@ "Linux" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json index 3392a1bff23b8c..a4c62b98fb0609 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Detects writing executable files that will be automatically launched by Adobe on launch.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json index e76379d171bf7c..e3dedeef07eb56 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json @@ -6,6 +6,7 @@ "false_positives": [ "Security tools and device drivers may run these programs in order to load legitimate kernel modules. Use of these programs by ordinary users is uncommon." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -42,5 +43,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json index b9e7f941ee5dff..8b81789f6aa8f9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json @@ -6,6 +6,7 @@ "false_positives": [ "Legitimate scheduled tasks may be created during installation of new software." ], + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -39,5 +40,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json index 0cf6fcdb3875a6..2aaf0012acabf5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json @@ -6,6 +6,7 @@ "false_positives": [ "Network monitoring or management products may have a web server component that runs shell commands as part of normal behavior." ], + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -42,5 +43,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json index 59715dae441f49..32d78480325e62 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Windows services typically run as SYSTEM and can be used as a privilege escalation opportunity. Malware or penetration testers may run a shell as a service to gain SYSTEM permissions.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json index 7465751d5cd49b..3f2e00f0976de8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies attempts to create new local users. This is sometimes done by attackers to increase access to a system or domain.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json index 9550eea6ca6aa7..bb0856c0452d53 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "An adversary may add the setgid bit to a file or directory in order to run a file with the privileges of the owning group. An adversary can take advantage of this to either do a shell escape or exploit a vulnerability in an application with the setgid bit to get code running in a different user\u2019s context. Additionally, adversaries can use this mechanism on their own malware to make sure they're able to execute in elevated contexts in the future.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -52,5 +53,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json index 343426953add6d..4cf60d2c9d0def 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "An adversary may add the setuid bit to a file or directory in order to run a file with the privileges of the owning user. An adversary can take advantage of this to either do a shell escape or exploit a vulnerability in an application with the setuid bit to get code running in a different user\u2019s context. Additionally, adversaries can use this mechanism on their own malware to make sure they're able to execute in elevated contexts in the future.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -52,5 +53,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json index 44b50c74bafe62..73a804fcbda8f1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "A sudoers file specifies the commands that users or groups can run and from which terminals. Adversaries can take advantage of these configurations to execute commands as other users or spawn processes with higher privileges.", + "from": "now-9m", "index": [ "auditbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json index 50692dae3856fa..740ff47e5abe5d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies User Account Control (UAC) bypass via eventvwr.exe. Attackers bypass UAC to stealthily execute code with elevated permissions.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json index 8f938c0ceee6d2..c6c5cbce2c095e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json @@ -3,6 +3,7 @@ "Elastic" ], "description": "Identifies Windows programs run from unexpected parent processes. This could indicate masquerading or other strange activity on a system.", + "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" @@ -36,5 +37,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } From ddf99b64db371f22f6752adc50648fcf2ff413fb Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 26 Aug 2020 09:09:40 +0200 Subject: [PATCH 023/216] [Lens] Fix rollup related bugs (#75314) Co-authored-by: Marta Bondyra --- .../datapanel.test.tsx | 19 +- .../indexpattern_datasource/datapanel.tsx | 8 +- .../dimension_panel/dimension_panel.test.tsx | 2 + .../fields_accordion.tsx | 12 +- .../indexpattern.test.ts | 2 + .../indexpattern_suggestions.test.tsx | 7 + .../layerpanel.test.tsx | 3 + .../indexpattern_datasource/loader.test.ts | 15 +- .../public/indexpattern_datasource/loader.ts | 8 + .../public/indexpattern_datasource/mocks.ts | 2 + .../definitions/date_histogram.test.tsx | 43 +- .../operations/definitions/date_histogram.tsx | 33 +- .../operations/definitions/index.ts | 2 +- .../operations/definitions/metrics.tsx | 2 +- .../operations/definitions/terms.test.tsx | 5 +- .../operations/definitions/terms.tsx | 2 +- .../operations/operations.test.ts | 1 + .../state_helpers.test.ts | 1 + .../indexpattern_datasource/to_expression.ts | 6 +- .../public/indexpattern_datasource/types.ts | 1 + x-pack/test/functional/apps/lens/index.ts | 3 + x-pack/test/functional/apps/lens/rollup.ts | 75 + .../es_archives/lens/rollup/config/data.json | 65 + .../lens/rollup/config/mappings.json | 1294 +++++++++++++++++ .../es_archives/lens/rollup/data/data.json | 59 + .../lens/rollup/data/mappings.json | 129 ++ 26 files changed, 1765 insertions(+), 34 deletions(-) create mode 100644 x-pack/test/functional/apps/lens/rollup.ts create mode 100644 x-pack/test/functional/es_archives/lens/rollup/config/data.json create mode 100644 x-pack/test/functional/es_archives/lens/rollup/config/mappings.json create mode 100644 x-pack/test/functional/es_archives/lens/rollup/data/data.json create mode 100644 x-pack/test/functional/es_archives/lens/rollup/data/mappings.json diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 8291b673cd17a8..f17bf172b0fb18 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -84,6 +84,7 @@ const initialState: IndexPatternPrivateState = { id: '1', title: 'idx1', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', @@ -134,6 +135,7 @@ const initialState: IndexPatternPrivateState = { id: '2', title: 'idx2', timeFieldName: 'timestamp', + hasRestrictions: true, fields: [ { name: 'timestamp', @@ -191,6 +193,7 @@ const initialState: IndexPatternPrivateState = { id: '3', title: 'idx3', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', @@ -322,8 +325,20 @@ describe('IndexPattern Data Panel', () => { isFirstExistenceFetch: false, currentIndexPatternId: 'a', indexPatterns: { - a: { id: 'a', title: 'aaa', timeFieldName: 'atime', fields: [] }, - b: { id: 'b', title: 'bbb', timeFieldName: 'btime', fields: [] }, + a: { + id: 'a', + title: 'aaa', + timeFieldName: 'atime', + fields: [], + hasRestrictions: false, + }, + b: { + id: 'b', + title: 'bbb', + timeFieldName: 'btime', + fields: [], + hasRestrictions: false, + }, }, layers: { 1: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 0777b9b9d8e573..f7adf91e307da4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -126,6 +126,7 @@ export function IndexPatternDataPanel({ title: indexPatterns[id].title, timeFieldName: indexPatterns[id].timeFieldName, fields: indexPatterns[id].fields, + hasRestrictions: indexPatterns[id].hasRestrictions, })); const dslQuery = buildSafeEsQuery( @@ -422,6 +423,8 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ ] ); + const fieldInfoUnavailable = existenceFetchFailed || currentIndexPattern.hasRestrictions; + return ( - {!existenceFetchFailed && ( + {!fieldInfoUnavailable && ( { foo: { id: 'foo', title: 'Foo pattern', + hasRestrictions: false, fields: [ { aggregatable: true, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx index af2ed97ad81254..30a92c21ff6610 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx @@ -47,6 +47,7 @@ export interface FieldsAccordionProps { renderCallout: JSX.Element; exists: boolean; showExistenceFetchError?: boolean; + hideDetails?: boolean; } export const InnerFieldsAccordion = function InnerFieldsAccordion({ @@ -61,13 +62,20 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({ fieldProps, renderCallout, exists, + hideDetails, showExistenceFetchError, }: FieldsAccordionProps) { const renderField = useCallback( (field: IndexPatternField) => ( - + ), - [fieldProps, exists] + [fieldProps, exists, hideDetails] ); return ( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 0ba7b7df97853c..900cd02622aaf3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -21,6 +21,7 @@ const expectedIndexPatterns = { id: '1', title: 'my-fake-index-pattern', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', @@ -70,6 +71,7 @@ const expectedIndexPatterns = { id: '2', title: 'my-fake-restricted-pattern', timeFieldName: 'timestamp', + hasRestrictions: true, fields: [ { name: 'timestamp', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index 5489dcffc52c4e..663d7c18bb370d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -20,6 +20,7 @@ const expectedIndexPatterns = { id: '1', title: 'my-fake-index-pattern', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', @@ -68,6 +69,7 @@ const expectedIndexPatterns = { 2: { id: '2', title: 'my-fake-restricted-pattern', + hasRestrictions: true, timeFieldName: 'timestamp', fields: [ { @@ -322,6 +324,7 @@ describe('IndexPattern Data Source suggestions', () => { 1: { id: '1', title: 'no timefield', + hasRestrictions: false, fields: [ { name: 'bytes', @@ -532,6 +535,7 @@ describe('IndexPattern Data Source suggestions', () => { 1: { id: '1', title: 'no timefield', + hasRestrictions: false, fields: [ { name: 'bytes', @@ -1350,6 +1354,7 @@ describe('IndexPattern Data Source suggestions', () => { 1: { id: '1', title: 'my-fake-index-pattern', + hasRestrictions: false, fields: [ { name: 'field1', @@ -1493,6 +1498,7 @@ describe('IndexPattern Data Source suggestions', () => { 1: { id: '1', title: 'my-fake-index-pattern', + hasRestrictions: false, fields: [ { name: 'field1', @@ -1555,6 +1561,7 @@ describe('IndexPattern Data Source suggestions', () => { 1: { id: '1', title: 'my-fake-index-pattern', + hasRestrictions: false, fields: [ { name: 'field1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx index 738cdd611a7ba3..92e35b257f24ad 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -62,6 +62,7 @@ const initialState: IndexPatternPrivateState = { id: '1', title: 'my-fake-index-pattern', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', @@ -103,6 +104,7 @@ const initialState: IndexPatternPrivateState = { '2': { id: '2', title: 'my-fake-restricted-pattern', + hasRestrictions: true, timeFieldName: 'timestamp', fields: [ { @@ -160,6 +162,7 @@ const initialState: IndexPatternPrivateState = { id: '3', title: 'my-compatible-pattern', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index d80bf779a5d17f..660be9514a92f3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -40,6 +40,7 @@ const indexPattern1 = ({ id: '1', title: 'my-fake-index-pattern', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', @@ -105,6 +106,7 @@ const indexPattern2 = ({ id: '2', title: 'my-fake-restricted-pattern', timeFieldName: 'timestamp', + hasRestrictions: true, fields: [ { name: 'timestamp', @@ -733,9 +735,9 @@ describe('loader', () => { dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, fetchJson, indexPatterns: [ - { id: '1', title: '1', fields: [] }, - { id: '2', title: '1', fields: [] }, - { id: '3', title: '1', fields: [] }, + { id: '1', title: '1', fields: [], hasRestrictions: false }, + { id: '2', title: '1', fields: [], hasRestrictions: false }, + { id: '3', title: '1', fields: [], hasRestrictions: false }, ], setState, dslQuery, @@ -783,9 +785,9 @@ describe('loader', () => { dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, fetchJson, indexPatterns: [ - { id: '1', title: '1', fields: [] }, - { id: '2', title: '1', fields: [] }, - { id: 'c', title: '1', fields: [] }, + { id: '1', title: '1', fields: [], hasRestrictions: false }, + { id: '2', title: '1', fields: [], hasRestrictions: false }, + { id: 'c', title: '1', fields: [], hasRestrictions: false }, ], setState, dslQuery, @@ -817,6 +819,7 @@ describe('loader', () => { { id: '1', title: '1', + hasRestrictions: false, fields: [{ name: 'field1' }, { name: 'field2' }] as IndexPatternField[], }, ], diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 24906790a9fc9c..585a1281cbf51d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -91,6 +91,7 @@ export async function loadIndexPatterns({ timeFieldName, fieldFormatMap, fields: newFields, + hasRestrictions: !!typeMeta?.aggs, }; return { @@ -334,6 +335,7 @@ export async function syncExistingFields({ title: string; fields: IndexPatternField[]; timeFieldName?: string | null; + hasRestrictions: boolean; }>; fetchJson: HttpSetup['post']; setState: SetState; @@ -343,6 +345,12 @@ export async function syncExistingFields({ showNoDataPopover: () => void; }) { const existenceRequests = indexPatterns.map((pattern) => { + if (pattern.hasRestrictions) { + return { + indexPatternTitle: pattern.title, + existingFieldNames: pattern.fields.map((field) => field.name), + }; + } const body: Record = { dslQuery, fromDate: dateRange.fromDate, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts index 869eee67d381dc..31e6240993d365 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts @@ -11,6 +11,7 @@ export const createMockedIndexPattern = (): IndexPattern => ({ id: '1', title: 'my-fake-index-pattern', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', @@ -70,6 +71,7 @@ export const createMockedRestrictedIndexPattern = () => ({ id: '2', title: 'my-fake-restricted-pattern', timeFieldName: 'timestamp', + hasRestrictions: true, fields: [ { name: 'timestamp', 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 48a6079c58ac0a..ac6bf63c371104 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 @@ -55,6 +55,7 @@ describe('date_histogram', () => { id: '1', title: 'Mock Indexpattern', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', @@ -69,6 +70,7 @@ describe('date_histogram', () => { 2: { id: '2', title: 'Mock Indexpattern 2', + hasRestrictions: false, fields: [ { name: 'other_timestamp', @@ -229,13 +231,50 @@ describe('date_histogram', () => { it('should reflect params correctly', () => { const esAggsConfig = dateHistogramOperation.toEsAggsConfig( state.layers.first.columns.col1 as DateHistogramIndexPatternColumn, - 'col1' + 'col1', + state.indexPatterns['1'] ); expect(esAggsConfig).toEqual( expect.objectContaining({ params: expect.objectContaining({ interval: '42w', field: 'timestamp', + useNormalizedEsInterval: true, + }), + }) + ); + }); + + it('should not use normalized es interval for rollups', () => { + const esAggsConfig = dateHistogramOperation.toEsAggsConfig( + state.layers.first.columns.col1 as DateHistogramIndexPatternColumn, + 'col1', + { + ...state.indexPatterns['1'], + fields: [ + { + name: 'timestamp', + displayName: 'timestamp', + aggregatable: true, + searchable: true, + type: 'date', + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + time_zone: 'UTC', + calendar_interval: '42w', + }, + }, + }, + ], + } + ); + expect(esAggsConfig).toEqual( + expect.objectContaining({ + params: expect.objectContaining({ + interval: '42w', + field: 'timestamp', + useNormalizedEsInterval: false, }), }) ); @@ -300,6 +339,7 @@ describe('date_histogram', () => { { title: '', id: '', + hasRestrictions: true, fields: [ { name: 'dateField', @@ -343,6 +383,7 @@ describe('date_histogram', () => { { title: '', id: '', + hasRestrictions: false, fields: [ { name: 'dateField', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index 2236bc576e2b66..57454291d43c56 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -119,21 +119,24 @@ export const dateHistogramOperation: OperationDefinition ({ - id: columnId, - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: { - field: column.sourceField, - time_zone: column.params.timeZone, - useNormalizedEsInterval: true, - interval: column.params.interval, - drop_partials: false, - min_doc_count: 0, - extended_bounds: {}, - }, - }), + toEsAggsConfig: (column, columnId, indexPattern) => { + const usedField = indexPattern.fields.find((field) => field.name === column.sourceField); + return { + id: columnId, + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: column.sourceField, + time_zone: column.params.timeZone, + useNormalizedEsInterval: !usedField || !usedField.aggregationRestrictions?.date_histogram, + interval: column.params.interval, + drop_partials: false, + min_doc_count: 0, + extended_bounds: {}, + }, + }; + }, paramEditor: ({ state, setState, currentColumn: currentColumn, layerId, dateRange, data }) => { const field = currentColumn && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index ef12fca690f0c3..8ef53c1e0b4258 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -84,7 +84,7 @@ interface BaseOperationDefinitionProps { * Function turning a column into an agg config passed to the `esaggs` function * together with the agg configs returned from other columns. */ - toEsAggsConfig: (column: C, columnId: string) => unknown; + toEsAggsConfig: (column: C, columnId: string, indexPattern: IndexPattern) => unknown; /** * Returns true if the `column` can also be used on `newIndexPattern`. * If this function returns false, the column is removed when switching index pattern diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index e6c8a5f6ac8520..4c37d95f6b0509 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -68,7 +68,7 @@ function buildMetricOperation>({ sourceField: field.name, }; }, - toEsAggsConfig: (column, columnId) => ({ + toEsAggsConfig: (column, columnId, _indexPattern) => ({ id: columnId, enabled: true, type: column.operationType, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx index 05bb2ef6738888..2972ed2d0231b6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx @@ -13,7 +13,7 @@ import { dataPluginMock } from '../../../../../../../src/plugins/data/public/moc import { createMockedIndexPattern } from '../../mocks'; import { TermsIndexPatternColumn } from './terms'; import { termsOperation } from './index'; -import { IndexPatternPrivateState } from '../../types'; +import { IndexPatternPrivateState, IndexPattern } from '../../types'; const defaultProps = { storage: {} as IStorageWrapper, @@ -69,7 +69,8 @@ describe('terms', () => { it('should reflect params correctly', () => { const esAggsConfig = termsOperation.toEsAggsConfig( state.layers.first.columns.col1 as TermsIndexPatternColumn, - 'col1' + 'col1', + {} as IndexPattern ); expect(esAggsConfig).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx index ac1ff9da2fea08..c1b19fd5549e75 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx @@ -95,7 +95,7 @@ export const termsOperation: OperationDefinition = { }, }; }, - toEsAggsConfig: (column, columnId) => ({ + toEsAggsConfig: (column, columnId, _indexPattern) => ({ id: columnId, enabled: true, type: 'terms', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 3fce2562f528e0..4ac3fc89500f91 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -16,6 +16,7 @@ const expectedIndexPatterns = { id: '1', title: 'my-fake-index-pattern', timeFieldName: 'timestamp', + hasRestrictions: false, fields: [ { name: 'timestamp', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts index d7fd0d3661c86e..7b6eb11efc4943 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts @@ -570,6 +570,7 @@ describe('state_helpers', () => { const indexPattern: IndexPattern = { id: 'test', title: '', + hasRestrictions: true, fields: [ { name: 'fieldA', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index 9473a1523b8ca7..1b87c48dc7193d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -21,7 +21,11 @@ function getExpressionForLayer( } function getEsAggsConfig(column: C, columnId: string) { - return operationDefinitionMap[column.operationType].toEsAggsConfig(column, columnId); + return operationDefinitionMap[column.operationType].toEsAggsConfig( + column, + columnId, + indexPattern + ); } const columnEntries = columnOrder.map((colId) => [colId, columns[colId]] as const); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index 95cc47e68f8a15..c101f1354b703d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -19,6 +19,7 @@ export interface IndexPattern { params: unknown; } >; + hasRestrictions: boolean; } export interface IndexPatternField { diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index f2dcf28c017433..d1ecf8fa0973a3 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -31,6 +31,9 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./dashboard')); loadTestFile(require.resolve('./persistent_context')); loadTestFile(require.resolve('./lens_reporting')); + + // has to be last one in the suite because it overrides saved objects + loadTestFile(require.resolve('./rollup')); }); }); } diff --git a/x-pack/test/functional/apps/lens/rollup.ts b/x-pack/test/functional/apps/lens/rollup.ts new file mode 100644 index 00000000000000..f6882c8aed2144 --- /dev/null +++ b/x-pack/test/functional/apps/lens/rollup.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['visualize', 'lens']); + const find = getService('find'); + const listingTable = getService('listingTable'); + const esArchiver = getService('esArchiver'); + + describe('lens rollup tests', () => { + before(async () => { + await esArchiver.loadIfNeeded('lens/rollup/data'); + await esArchiver.loadIfNeeded('lens/rollup/config'); + }); + + after(async () => { + await esArchiver.unload('lens/rollup/data'); + await esArchiver.unload('lens/rollup/config'); + }); + + it('should allow creation of lens xy chart', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'sum', + field: 'bytes', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'geo.src', + }); + expect(await find.allByCssSelector('.echLegendItem')).to.have.length(2); + + await PageObjects.lens.save('Afancilenstest'); + + // Ensure the visualization shows up in the visualize list, and takes + // us back to the visualization as we configured it. + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('Afancilenstest'); + await PageObjects.lens.clickVisualizeListItemTitle('Afancilenstest'); + await PageObjects.lens.goToTimeRange(); + + expect(await PageObjects.lens.getTitle()).to.eql('Afancilenstest'); + + // .echLegendItem__title is the only viable way of getting the xy chart's + // legend item(s), so we're using a class selector here. + expect(await find.allByCssSelector('.echLegendItem')).to.have.length(2); + }); + + it('should allow seamless transition to and from table view', async () => { + await PageObjects.lens.switchToVisualization('lnsMetric'); + await PageObjects.lens.assertMetric('Sum of bytes', '16,788'); + await PageObjects.lens.switchToVisualization('lnsDatatable'); + expect(await PageObjects.lens.getDatatableHeaderText()).to.eql('Sum of bytes'); + expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('16,788'); + }); + }); +} diff --git a/x-pack/test/functional/es_archives/lens/rollup/config/data.json b/x-pack/test/functional/es_archives/lens/rollup/config/data.json new file mode 100644 index 00000000000000..268b98542fe115 --- /dev/null +++ b/x-pack/test/functional/es_archives/lens/rollup/config/data.json @@ -0,0 +1,65 @@ +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "space": "6.6.0" + }, + "references": [], + "space": { + "_reserved": true, + "description": "This is the default space!", + "disabledFeatures": [], + "name": "Default" + }, + "type": "space" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:lens-rolled-up-data", + "index": ".kibana_1", + "source": { + "index-pattern" : { + "title" : "lens_rolled_up_data", + "timeFieldName" : "@timestamp", + "fields" : "[{\"count\":0,\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_score\",\"type\":\"number\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"@timestamp\",\"type\":\"date\",\"esTypes\":[\"date\"],\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"count\":0,\"name\":\"bytes\",\"type\":\"number\",\"esTypes\":[\"float\"],\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"count\":0,\"name\":\"geo.src\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "type" : "rollup", + "typeMeta" : "{\"params\":{\"rollup_index\":\"lens_rolled_up_data\"},\"aggs\":{\"date_histogram\":{\"@timestamp\":{\"agg\":\"date_histogram\",\"fixed_interval\":\"60m\",\"time_zone\":\"UTC\"}},\"sum\":{\"bytes\":{\"agg\":\"sum\"}},\"max\":{\"bytes\":{\"agg\":\"max\"}},\"terms\":{\"geo.src\":{\"agg\":\"terms\"}}}}" + }, + "type" : "index-pattern", + "references" : [ ], + "migrationVersion" : { + "index-pattern" : "7.6.0" + }, + "updated_at" : "2020-08-19T08:39:09.998Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "config:8.0.0", + "index": ".kibana_1", + "source": { + "config": { + "accessibility:disableAnimations": true, + "buildNum": 9007199254740991, + "dateFormat:tz": "UTC", + "defaultIndex": "logstash-*" + }, + "references": [], + "type": "config", + "updated_at": "2019-09-04T18:47:24.761Z" + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/lens/rollup/config/mappings.json b/x-pack/test/functional/es_archives/lens/rollup/config/mappings.json new file mode 100644 index 00000000000000..f2a29f022ff5e4 --- /dev/null +++ b/x-pack/test/functional/es_archives/lens/rollup/config/mappings.json @@ -0,0 +1,1294 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "apm-telemetry": "07ee1939fa4302c62ddc052ec03fed90", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "config": "87aca8fdb053154f11383fce3dbf3edf", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", + "inventory-view": "84b320fd67209906333ffce261128462", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "21c3ea0763beb1ecb0162529706b88c5", + "map": "23d7aa4a720d4938ccde3983f87bd58d", + "maps-telemetry": "a4229f8b16a6820c6d724b7e0c1f729d", + "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "server": "ec97f1c5da1a19609a60874e5af1100c", + "siem-ui-timeline": "1f6f0860ad7bc0dba3e42467ca40470d", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "e1c8bc94e443aefd9458932cc0697a4d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "description": { + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "interval": { + "type": "keyword" + }, + "scheduledTaskId": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "dotnet": { + "null_value": 0, + "type": "long" + }, + "go": { + "null_value": 0, + "type": "long" + }, + "java": { + "null_value": 0, + "type": "long" + }, + "js-base": { + "null_value": 0, + "type": "long" + }, + "nodejs": { + "null_value": 0, + "type": "long" + }, + "python": { + "null_value": 0, + "type": "long" + }, + "ruby": { + "null_value": 0, + "type": "long" + }, + "rum-js": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "defaultIndex": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "gis-map": { + "properties": { + "bounds": { + "strategy": "recursive", + "type": "geo_shape" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "inventory-view": { + "properties": { + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "metric": { + "properties": { + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "time": { + "type": "integer" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "bounds": { + "type": "geo_shape" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "properties": { + "attributesPerMap": { + "properties": { + "dataSourcesCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + }, + "emsVectorLayersCount": { + "dynamic": "true", + "type": "object" + }, + "layerTypesCount": { + "dynamic": "true", + "type": "object" + }, + "layersCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + } + } + }, + "mapsTotalCount": { + "type": "long" + }, + "timeCaptured": { + "type": "date" + } + } + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "dynamic": "true", + "properties": { + "indexName": { + "type": "keyword" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/lens/rollup/data/data.json b/x-pack/test/functional/es_archives/lens/rollup/data/data.json new file mode 100644 index 00000000000000..36dc10c05f0b97 --- /dev/null +++ b/x-pack/test/functional/es_archives/lens/rollup/data/data.json @@ -0,0 +1,59 @@ +{ + "type": "doc", + "value": { + "index": "lens_rolled_up_data", + "id": "lens_rolled_up_data$vuSq1a9Ph2Nq-2yfGpE34g", + "source": { + "@timestamp.date_histogram.time_zone": "UTC", + "@timestamp.date_histogram.timestamp": 1442710800000, + "geo.src.terms.value": "CN", + "bytes.max.value": 5678.0, + "_rollup.version": 2, + "bytes.sum.value": 5678.0, + "@timestamp.date_histogram.interval": "60m", + "geo.src.terms._count": 1, + "@timestamp.date_histogram._count": 1, + "_rollup.id": "lens_rolled_up_data" + } + } +} + +{ + "type": "doc", + "value": { + "index": "lens_rolled_up_data", + "id": "lens_rolled_up_data$QFyUWoecErSYPMrIb6CgZA", + "source": { + "@timestamp.date_histogram.time_zone": "UTC", + "@timestamp.date_histogram.timestamp": 1442710800000, + "geo.src.terms.value": "US", + "bytes.max.value": 1234.0, + "_rollup.version": 2, + "bytes.sum.value": 1234.0, + "@timestamp.date_histogram.interval": "60m", + "geo.src.terms._count": 1, + "@timestamp.date_histogram._count": 1, + "_rollup.id": "lens_rolled_up_data" + } + } +} + +{ + "type": "doc", + "value": { + "index": "lens_rolled_up_data", + "id": "lens_rolled_up_data$cKCjv1OPjYiyv5WPPblohw", + "source": { + "@timestamp.date_histogram.time_zone": "UTC", + "@timestamp.date_histogram.timestamp": 1442714400000, + "geo.src.terms.value": "CN", + "bytes.max.value": 9876.0, + "_rollup.version": 2, + "bytes.sum.value": 9876.0, + "@timestamp.date_histogram.interval": "60m", + "geo.src.terms._count": 1, + "@timestamp.date_histogram._count": 1, + "_rollup.id": "lens_rolled_up_data" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/lens/rollup/data/mappings.json b/x-pack/test/functional/es_archives/lens/rollup/data/mappings.json new file mode 100644 index 00000000000000..0e47a632bbf3f3 --- /dev/null +++ b/x-pack/test/functional/es_archives/lens/rollup/data/mappings.json @@ -0,0 +1,129 @@ +{ + "type": "index", + "value": { + "index": "lens_rolled_up_data", + "mappings": { + "_meta": { + "_rollup": { + "lens_rolled_up_data": { + "cron": "0 * * * * ?", + "rollup_index": "lens_rolled_up_data", + "groups": { + "date_histogram": { + "fixed_interval": "60m", + "field": "@timestamp", + "time_zone": "UTC" + }, + "terms": { + "fields": ["geo.src", "ip"] + } + }, + "id": "lens_rolled_up_data", + "metrics": [ + { + "field": "bytes", + "metrics": ["sum", "max"] + } + ], + "index_pattern": "lens_raw", + "timeout": "20s", + "page_size": 1000 + } + }, + "rollup-version": "8.0.0" + }, + "dynamic_templates": [ + { + "strings": { + "match_mapping_type": "string", + "mapping": { + "type": "keyword" + } + } + }, + { + "date_histograms": { + "path_match": "*.date_histogram.timestamp", + "mapping": { + "type": "date" + } + } + } + ], + "properties": { + "@timestamp": { + "properties": { + "date_histogram": { + "properties": { + "_count": { + "type": "long" + }, + "interval": { + "type": "keyword" + }, + "time_zone": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + } + } + } + } + }, + "_rollup": { + "properties": { + "id": { + "type": "keyword" + }, + "version": { + "type": "long" + } + } + }, + "bytes": { + "properties": { + "max": { + "properties": { + "value": { + "type": "float" + } + } + }, + "sum": { + "properties": { + "value": { + "type": "float" + } + } + } + } + }, + "geo": { + "properties": { + "src": { + "properties": { + "terms": { + "properties": { + "_count": { + "type": "long" + }, + "value": { + "type": "keyword" + } + } + } + } + } + } + } + } + }, + "settings": { + "index": { + "number_of_shards": "1", + "number_of_replicas": "0" + } + } + } +} From 686cde88afa4c303ff92906ae6c100d130967812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Wed, 26 Aug 2020 10:38:54 +0200 Subject: [PATCH 024/216] [Logs UI] View log details for anomaly log examples (#75425) Co-authored-by: Elastic Machine --- .../logging/log_entry_flyout/index.tsx | 2 +- .../log_entry_flyout/log_entry_flyout.tsx | 36 ++-- .../logs/log_entry_rate/page_providers.tsx | 23 ++- .../log_entry_rate/page_results_content.tsx | 183 +++++++++++------- .../sections/anomalies/log_entry_example.tsx | 27 ++- .../pages/logs/stream/page_logs_content.tsx | 21 +- 6 files changed, 184 insertions(+), 108 deletions(-) diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/index.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/index.tsx index 521fbf209870c0..f11d6cdb8d26d8 100644 --- a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/index.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/index.tsx @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { LogEntryFlyout } from './log_entry_flyout'; +export * from './log_entry_flyout'; diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx index 57f27ee76184b4..76ffada510e51c 100644 --- a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx @@ -26,12 +26,10 @@ import { InfraLoadingPanel } from '../../loading'; import { LogEntryActionsMenu } from './log_entry_actions_menu'; import { LogEntriesItem, LogEntriesItemField } from '../../../../common/http_api'; -interface Props { +export interface LogEntryFlyoutProps { flyoutItem: LogEntriesItem | null; setFlyoutVisibility: (visible: boolean) => void; - setFilter: (filter: string) => void; - setTarget: (timeKey: TimeKey, flyoutItemId: string) => void; - + setFilter: (filter: string, flyoutItemId: string, timeKey?: TimeKey) => void; loading: boolean; } @@ -40,27 +38,27 @@ export const LogEntryFlyout = ({ loading, setFlyoutVisibility, setFilter, - setTarget, -}: Props) => { +}: LogEntryFlyoutProps) => { const createFilterHandler = useCallback( (field: LogEntriesItemField) => () => { + if (!flyoutItem) { + return; + } + const filter = `${field.field}:"${field.value}"`; - setFilter(filter); + const timestampMoment = moment(flyoutItem.key.time); + let target; - if (flyoutItem && flyoutItem.key) { - const timestampMoment = moment(flyoutItem.key.time); - if (timestampMoment.isValid()) { - setTarget( - { - time: timestampMoment.valueOf(), - tiebreaker: flyoutItem.key.tiebreaker, - }, - flyoutItem.id - ); - } + if (timestampMoment.isValid()) { + target = { + time: timestampMoment.valueOf(), + tiebreaker: flyoutItem.key.tiebreaker, + }; } + + setFilter(filter, flyoutItem.id, target); }, - [flyoutItem, setFilter, setTarget] + [flyoutItem, setFilter] ); const closeFlyout = useCallback(() => setFlyoutVisibility(false), [setFlyoutVisibility]); diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx index e986fa37c2b2c8..4ad654614237dc 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx @@ -10,6 +10,7 @@ import { LogEntryCategoriesModuleProvider } from '../../../containers/logs/log_a import { LogEntryRateModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_rate'; import { useLogSourceContext } from '../../../containers/logs/log_source'; import { useActiveKibanaSpace } from '../../../hooks/use_kibana_space'; +import { LogFlyout } from '../../../containers/logs/log_flyout'; export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) => { const { sourceId, sourceConfiguration } = useLogSourceContext(); @@ -23,20 +24,22 @@ export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) } return ( - - + - {children} - - + + {children} + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx index 65cc4a6c4a704b..de72ac5c5a574e 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx @@ -7,7 +7,9 @@ import datemath from '@elastic/datemath'; import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel, EuiSuperDatePicker } from '@elastic/eui'; import moment from 'moment'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { encode, RisonValue } from 'rison-node'; +import { stringify } from 'query-string'; +import React, { useCallback, useEffect, useMemo, useState, useContext } from 'react'; import { euiStyled, useTrackPageview } from '../../../../../observability/public'; import { TimeRange } from '../../../../common/http_api/shared/time_range'; import { bucketSpan } from '../../../../common/log_analysis'; @@ -29,6 +31,9 @@ import { StringTimeRange, useLogAnalysisResultsUrlState, } from './use_log_entry_rate_results_url_state'; +import { LogEntryFlyout, LogEntryFlyoutProps } from '../../../components/logging/log_entry_flyout'; +import { LogFlyout } from '../../../containers/logs/log_flyout'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; export const SORT_DEFAULTS = { direction: 'desc' as const, @@ -42,6 +47,7 @@ export const PAGINATION_DEFAULTS = { export const LogEntryRateResultsContent: React.FunctionComponent = () => { useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results' }); useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results', delay: 15000 }); + const navigateToApp = useKibana().services.application?.navigateToApp; const { sourceId } = useLogSourceContext(); @@ -79,6 +85,30 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { lastChangedTime: Date.now(), })); + const linkToLogStream = useCallback( + (filter, id, timeKey) => { + const params = { + logPosition: encode({ + end: moment(queryTimeRange.value.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + position: timeKey as RisonValue, + start: moment(queryTimeRange.value.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + streamLive: false, + }), + flyoutOptions: encode({ + surroundingLogsId: id, + }), + logFilter: encode({ + expression: filter, + kind: 'kuery', + }), + }; + + // eslint-disable-next-line no-unused-expressions + navigateToApp?.('logs', { path: `/stream?${stringify(params)}` }); + }, + [queryTimeRange, navigateToApp] + ); + const bucketDuration = useMemo( () => getBucketDuration(queryTimeRange.value.startTime, queryTimeRange.value.endTime), [queryTimeRange.value.endTime, queryTimeRange.value.startTime] @@ -115,6 +145,10 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { filteredDatasets: selectedDatasets, }); + const { flyoutVisible, setFlyoutVisibility, flyoutItem, isLoading: isFlyoutLoading } = useContext( + LogFlyout.Context + ); + const handleQueryTimeRangeChange = useCallback( ({ start: startTime, end: endTime }: { start: string; end: string }) => { setQueryTimeRange({ @@ -198,75 +232,86 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { ); return ( - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - + + + + + + + + + + {flyoutVisible ? ( + + ) : null} + ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx index fece2522de574f..a543f95bf4ffb2 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useCallback, useState } from 'react'; +import React, { useMemo, useCallback, useState, useContext } from 'react'; import moment from 'moment'; import { encode } from 'rison-node'; import { i18n } from '@kbn/i18n'; @@ -37,6 +37,7 @@ import { } from '../../../../../utils/source_configuration'; import { localizedDate } from '../../../../../../common/formatters/datetime'; import { LogEntryAnomaly } from '../../../../../../common/http_api'; +import { LogFlyout } from '../../../../../containers/logs/log_flyout'; export const exampleMessageScale = 'medium' as const; export const exampleTimestampFormat = 'time' as const; @@ -45,6 +46,13 @@ const MENU_LABEL = i18n.translate('xpack.infra.logAnomalies.logEntryExamplesMenu defaultMessage: 'View actions for log entry', }); +const VIEW_DETAILS_LABEL = i18n.translate( + 'xpack.infra.logs.analysis.logEntryExamplesViewDetailsLabel', + { + defaultMessage: 'View details', + } +); + const VIEW_IN_STREAM_LABEL = i18n.translate( 'xpack.infra.logs.analysis.logEntryExamplesViewInStreamLabel', { @@ -80,6 +88,8 @@ export const LogEntryExampleMessage: React.FunctionComponent = ({ const setItemIsHovered = useCallback(() => setIsHovered(true), []); const setItemIsNotHovered = useCallback(() => setIsHovered(false), []); + const { setFlyoutVisibility, setFlyoutId } = useContext(LogFlyout.Context); + // handle special cases for the dataset value const humanFriendlyDataset = getFriendlyNameForPartitionId(dataset); @@ -116,6 +126,13 @@ export const LogEntryExampleMessage: React.FunctionComponent = ({ } return [ + { + label: VIEW_DETAILS_LABEL, + onClick: () => { + setFlyoutId(id); + setFlyoutVisibility(true); + }, + }, { label: VIEW_IN_STREAM_LABEL, onClick: viewInStreamLinkProps.onClick, @@ -127,7 +144,13 @@ export const LogEntryExampleMessage: React.FunctionComponent = ({ href: viewAnomalyInMachineLearningLinkProps.href, }, ]; - }, [viewInStreamLinkProps, viewAnomalyInMachineLearningLinkProps]); + }, [ + id, + setFlyoutId, + setFlyoutVisibility, + viewInStreamLinkProps, + viewAnomalyInMachineLearningLinkProps, + ]); return ( { const [, { setContextEntry }] = useContext(ViewLogInContext.Context); + const setFilter = useCallback( + (filter, flyoutItemId, timeKey) => { + applyLogFilterQuery(filter); + if (timeKey) { + jumpToTargetPosition(timeKey); + } + setSurroundingLogsId(flyoutItemId); + stopLiveStreaming(); + }, + [applyLogFilterQuery, jumpToTargetPosition, setSurroundingLogsId, stopLiveStreaming] + ); + return ( <> @@ -65,12 +77,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { {flyoutVisible ? ( { - jumpToTargetPosition(timeKey); - setSurroundingLogsId(flyoutItemId); - stopLiveStreaming(); - }} + setFilter={setFilter} setFlyoutVisibility={setFlyoutVisibility} flyoutItem={flyoutItem} loading={isLoading} From 4efaba3298f79ceda4586fcb891d84987fc95980 Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Wed, 26 Aug 2020 11:48:27 +0300 Subject: [PATCH 025/216] Reset chrome fields while switching an app (#73064) * Reset chrome help extension while switching an app * Reset other chrome fields * Set docTitle in saved objects app * Add unit tests Co-authored-by: Elastic Machine --- src/core/public/chrome/chrome_service.test.ts | 53 +++++++++++++++++++ src/core/public/chrome/chrome_service.tsx | 8 +++ .../management_section/mount_section.tsx | 7 +++ 3 files changed, 68 insertions(+) diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 8dc81dceaccd61..0150554a609065 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -405,6 +405,59 @@ describe('start', () => { `); }); }); + + describe('erase chrome fields', () => { + it('while switching an app', async () => { + const startDeps = defaultStartDeps([new FakeApp('alpha')]); + const { navigateToApp } = startDeps.application; + const { chrome, service } = await start({ startDeps }); + + const helpExtensionPromise = chrome.getHelpExtension$().pipe(toArray()).toPromise(); + const breadcrumbsPromise = chrome.getBreadcrumbs$().pipe(toArray()).toPromise(); + const badgePromise = chrome.getBadge$().pipe(toArray()).toPromise(); + const docTitleResetSpy = jest.spyOn(chrome.docTitle, 'reset'); + + const promises = Promise.all([helpExtensionPromise, breadcrumbsPromise, badgePromise]); + + chrome.setHelpExtension({ appName: 'App name' }); + chrome.setBreadcrumbs([{ text: 'App breadcrumb' }]); + chrome.setBadge({ text: 'App badge', tooltip: 'App tooltip' }); + + navigateToApp('alpha'); + + service.stop(); + + expect(docTitleResetSpy).toBeCalledTimes(1); + await expect(promises).resolves.toMatchInlineSnapshot(` + Array [ + Array [ + undefined, + Object { + "appName": "App name", + }, + undefined, + ], + Array [ + Array [], + Array [ + Object { + "text": "App breadcrumb", + }, + ], + Array [], + ], + Array [ + undefined, + Object { + "text": "App badge", + "tooltip": "App tooltip", + }, + undefined, + ], + ] + `); + }); + }); }); describe('stop', () => { diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index d29120e6ee9ace..ef9a682d609ec6 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -157,6 +157,14 @@ export class ChromeService { const recentlyAccessed = await this.recentlyAccessed.start({ http }); const docTitle = this.docTitle.start({ document: window.document }); + // erase chrome fields from a previous app while switching to a next app + application.currentAppId$.subscribe(() => { + helpExtension$.next(undefined); + breadcrumbs$.next([]); + badge$.next(undefined); + docTitle.reset(); + }); + const setIsNavDrawerLocked = (isLocked: boolean) => { isNavDrawerLocked$.next(isLocked); localStorage.setItem(IS_LOCKED_KEY, `${isLocked}`); diff --git a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx index 9cfe99fd3bbf83..4339c2fa13c0f8 100644 --- a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx +++ b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx @@ -21,6 +21,7 @@ import React, { lazy, Suspense } from 'react'; import ReactDOM from 'react-dom'; import { Router, Switch, Route } from 'react-router-dom'; import { I18nProvider } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { EuiLoadingSpinner } from '@elastic/eui'; import { CoreSetup } from 'src/core/public'; import { ManagementAppMountParams } from '../../../management/public'; @@ -36,6 +37,10 @@ interface MountParams { let allowedObjectTypes: string[] | undefined; +const title = i18n.translate('savedObjectsManagement.objects.savedObjectsTitle', { + defaultMessage: 'Saved Objects', +}); + const SavedObjectsEditionPage = lazy(() => import('./saved_objects_edition_page')); const SavedObjectsTablePage = lazy(() => import('./saved_objects_table_page')); export const mountManagementSection = async ({ @@ -49,6 +54,8 @@ export const mountManagementSection = async ({ allowedObjectTypes = await getAllowedTypes(coreStart.http); } + coreStart.chrome.docTitle.change(title); + const capabilities = coreStart.application.capabilities; const RedirectToHomeIfUnauthorized: React.FunctionComponent = ({ children }) => { From 789b67fb5f6e6293dba812bfef3f08333f4a798e Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 26 Aug 2020 10:59:44 +0200 Subject: [PATCH 026/216] [APM] Improvements for breakdown data gaps (#75534) Closes #69704, #73387, #43780. --- .../shared/charts/CustomPlot/StaticPlot.js | 43 +- .../__snapshots__/CustomPlot.test.js.snap | 372 +++--- .../ErroneousTransactionsRateChart/index.tsx | 2 +- .../plugins/apm/public/utils/testHelpers.tsx | 15 +- x-pack/plugins/apm/server/index.ts | 2 + .../plugins/apm/server/lib/helpers/metrics.ts | 10 +- .../java/gc/fetch_and_transform_gc_metrics.ts | 8 +- .../metrics/fetch_and_transform_metrics.ts | 8 +- .../lib/transaction_groups/get_error_rate.ts | 9 +- .../lib/transactions/breakdown/index.ts | 8 +- .../expectation/error_rate.json | 1010 ++++++++++++++++- 11 files changed, 1242 insertions(+), 245 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js index d489970b55f29d..e49899da85e0d0 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js @@ -71,11 +71,29 @@ class StaticPlot extends PureComponent { const data = serie.data.map((value) => { return 'y' in value && isValidCoordinateValue(value.y) ? value - : { - ...value, - y: undefined, - }; + : { ...value, y: undefined }; }); + + // make sure individual markers are displayed in cases + // where there are gaps + + const markersForGaps = serie.data.map((value, index) => { + const prevHasData = getNull(serie.data[index - 1] ?? {}); + const nextHasData = getNull(serie.data[index + 1] ?? {}); + const thisHasData = getNull(value); + + const isGap = !prevHasData && !nextHasData && thisHasData; + + if (!isGap) { + return { + ...value, + y: undefined, + }; + } + + return value; + }); + return [ , + , ]; } @@ -132,7 +165,7 @@ class StaticPlot extends PureComponent { curve={'curveMonotoneX'} data={serie.data} color={serie.color} - size={0.5} + size={1} /> ); default: diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap index 8101b01a83b08b..f413610ebd9844 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap @@ -460,7 +460,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -477,7 +477,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -494,7 +494,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -511,7 +511,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -528,7 +528,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -545,7 +545,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -562,7 +562,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -579,7 +579,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -596,7 +596,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -613,7 +613,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -630,7 +630,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -647,7 +647,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -664,7 +664,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -681,7 +681,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -698,7 +698,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -715,7 +715,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -732,7 +732,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -749,7 +749,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -766,7 +766,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -783,7 +783,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -800,7 +800,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -817,7 +817,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -834,7 +834,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -851,7 +851,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -868,7 +868,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -885,7 +885,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -902,7 +902,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -919,7 +919,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -936,7 +936,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -953,7 +953,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -970,7 +970,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -1013,7 +1013,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1030,7 +1030,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1047,7 +1047,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1064,7 +1064,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1081,7 +1081,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1098,7 +1098,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1115,7 +1115,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1132,7 +1132,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1149,7 +1149,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1166,7 +1166,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1183,7 +1183,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1200,7 +1200,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1217,7 +1217,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1234,7 +1234,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1251,7 +1251,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1268,7 +1268,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1285,7 +1285,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1302,7 +1302,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1319,7 +1319,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1336,7 +1336,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1353,7 +1353,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1370,7 +1370,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1387,7 +1387,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1404,7 +1404,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1421,7 +1421,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1438,7 +1438,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1455,7 +1455,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1472,7 +1472,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1489,7 +1489,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1506,7 +1506,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1523,7 +1523,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -1566,7 +1566,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1583,7 +1583,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1600,7 +1600,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1617,7 +1617,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1634,7 +1634,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1651,7 +1651,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1668,7 +1668,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1685,7 +1685,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1702,7 +1702,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1719,7 +1719,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1736,7 +1736,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1753,7 +1753,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1770,7 +1770,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1787,7 +1787,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1804,7 +1804,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1821,7 +1821,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1838,7 +1838,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1855,7 +1855,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1872,7 +1872,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1889,7 +1889,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1906,7 +1906,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1923,7 +1923,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1940,7 +1940,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1957,7 +1957,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1974,7 +1974,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -1991,7 +1991,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -2008,7 +2008,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -2025,7 +2025,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -2042,7 +2042,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -2059,7 +2059,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -2076,7 +2076,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -3396,7 +3396,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3413,7 +3413,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3430,7 +3430,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3447,7 +3447,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3464,7 +3464,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3481,7 +3481,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3498,7 +3498,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3515,7 +3515,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3532,7 +3532,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3549,7 +3549,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3566,7 +3566,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3583,7 +3583,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3600,7 +3600,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3617,7 +3617,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3634,7 +3634,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3651,7 +3651,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3668,7 +3668,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3685,7 +3685,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3702,7 +3702,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3719,7 +3719,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3736,7 +3736,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3753,7 +3753,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3770,7 +3770,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3787,7 +3787,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3804,7 +3804,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3821,7 +3821,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3838,7 +3838,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3855,7 +3855,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3872,7 +3872,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3889,7 +3889,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3906,7 +3906,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#da8b45", @@ -3949,7 +3949,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -3966,7 +3966,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -3983,7 +3983,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4000,7 +4000,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4017,7 +4017,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4034,7 +4034,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4051,7 +4051,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4068,7 +4068,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4085,7 +4085,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4102,7 +4102,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4119,7 +4119,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4136,7 +4136,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4153,7 +4153,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4170,7 +4170,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4187,7 +4187,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4204,7 +4204,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4221,7 +4221,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4238,7 +4238,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4255,7 +4255,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4272,7 +4272,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4289,7 +4289,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4306,7 +4306,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4323,7 +4323,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4340,7 +4340,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4357,7 +4357,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4374,7 +4374,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4391,7 +4391,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4408,7 +4408,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4425,7 +4425,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4442,7 +4442,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4459,7 +4459,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#d6bf57", @@ -4502,7 +4502,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4519,7 +4519,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4536,7 +4536,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4553,7 +4553,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4570,7 +4570,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4587,7 +4587,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4604,7 +4604,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4621,7 +4621,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4638,7 +4638,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4655,7 +4655,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4672,7 +4672,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4689,7 +4689,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4706,7 +4706,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4723,7 +4723,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4740,7 +4740,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4757,7 +4757,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4774,7 +4774,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4791,7 +4791,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4808,7 +4808,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4825,7 +4825,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4842,7 +4842,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4859,7 +4859,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4876,7 +4876,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4893,7 +4893,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4910,7 +4910,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4927,7 +4927,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4944,7 +4944,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4961,7 +4961,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4978,7 +4978,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -4995,7 +4995,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", @@ -5012,7 +5012,7 @@ Array [ onContextMenu={[Function]} onMouseOut={[Function]} onMouseOver={[Function]} - r={0.5} + r={1} style={ Object { "fill": "#6092c0", diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx index 8214c081e6ce16..3b6d1684e08e1c 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx @@ -88,7 +88,7 @@ export function ErroneousTransactionsRateChart() { }, { data: errorRates, - type: 'line', + type: 'linemark', color: theme.euiColorVis7, hideLegend: true, title: i18n.translate('xpack.apm.errorRateChart.rateLabel', { diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index 217e6a30a33b4f..a750a9ea7af673 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -151,7 +151,20 @@ export async function inspectSearchParams( end: 1528977600000, apmEventClient: { search: spy } as any, internalClient: { search: spy } as any, - config: new Proxy({}, { get: () => 'myIndex' }) as APMConfig, + config: new Proxy( + {}, + { + get: (_, key) => { + switch (key) { + default: + return 'myIndex'; + + case 'xpack.apm.metricsInterval': + return 30; + } + }, + } + ) as APMConfig, uiFiltersES: [{ term: { 'my.custom.ui.filter': 'foo-bar' } }], indices: { /* eslint-disable @typescript-eslint/naming-convention */ diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index fa4b8b821f9f8a..29b2a77df348eb 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -31,6 +31,7 @@ export const config = { maxTraceItems: schema.number({ defaultValue: 1000 }), }), telemetryCollectionEnabled: schema.boolean({ defaultValue: true }), + metricsInterval: schema.number({ defaultValue: 30 }), }), }; @@ -68,6 +69,7 @@ export function mergeConfigs( 'xpack.apm.autocreateApmIndexPattern': apmConfig.autocreateApmIndexPattern, 'xpack.apm.telemetryCollectionEnabled': apmConfig.telemetryCollectionEnabled, + 'xpack.apm.metricsInterval': apmConfig.metricsInterval, }; } diff --git a/x-pack/plugins/apm/server/lib/helpers/metrics.ts b/x-pack/plugins/apm/server/lib/helpers/metrics.ts index c57769e9e15dae..9f5b5cdf475526 100644 --- a/x-pack/plugins/apm/server/lib/helpers/metrics.ts +++ b/x-pack/plugins/apm/server/lib/helpers/metrics.ts @@ -6,13 +6,17 @@ import { getBucketSize } from './get_bucket_size'; -export function getMetricsDateHistogramParams(start: number, end: number) { +export function getMetricsDateHistogramParams( + start: number, + end: number, + metricsInterval: number +) { const { bucketSize } = getBucketSize(start, end, 'auto'); return { field: '@timestamp', - // ensure minimum bucket size of 30s since this is the default resolution for metric data - fixed_interval: `${Math.max(bucketSize, 30)}s`, + // ensure minimum bucket size of configured interval since this is the default resolution for metric data + fixed_interval: `${Math.max(bucketSize, metricsInterval)}s`, min_doc_count: 0, extended_bounds: { min: start, max: end }, diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index e5c573ba1ec021..551384da2cca78 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -42,7 +42,7 @@ export async function fetchAndTransformGcMetrics({ chartBase: ChartBase; fieldName: typeof METRIC_JAVA_GC_COUNT | typeof METRIC_JAVA_GC_TIME; }) { - const { start, end, apmEventClient } = setup; + const { start, end, apmEventClient, config } = setup; const { bucketSize } = getBucketSize(start, end, 'auto'); @@ -75,7 +75,11 @@ export async function fetchAndTransformGcMetrics({ }, aggs: { over_time: { - date_histogram: getMetricsDateHistogramParams(start, end), + date_histogram: getMetricsDateHistogramParams( + start, + end, + config['xpack.apm.metricsInterval'] + ), aggs: { // get the max value max: { diff --git a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index f6e201b395c377..a42a10d6518a01 100644 --- a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -65,7 +65,7 @@ export async function fetchAndTransformMetrics({ aggs: T; additionalFilters?: Filter[]; }) { - const { start, end, apmEventClient } = setup; + const { start, end, apmEventClient, config } = setup; const projection = getMetricsProjection({ setup, @@ -83,7 +83,11 @@ export async function fetchAndTransformMetrics({ }, aggs: { timeseriesData: { - date_histogram: getMetricsDateHistogramParams(start, end), + date_histogram: getMetricsDateHistogramParams( + start, + end, + config['xpack.apm.metricsInterval'] + ), aggs, }, ...aggs, diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index d4e0bd1d54da1c..ec2d8144cf3ffc 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -12,12 +12,12 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; -import { getMetricsDateHistogramParams } from '../helpers/metrics'; import { Setup, SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; +import { getBucketSize } from '../helpers/get_bucket_size'; export async function getErrorRate({ serviceName, @@ -57,7 +57,12 @@ export async function getErrorRate({ query: { bool: { filter } }, aggs: { total_transactions: { - date_histogram: getMetricsDateHistogramParams(start, end), + date_histogram: { + field: '@timestamp', + fixed_interval: getBucketSize(start, end, 'auto').intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, aggs: { erroneous_transactions: { filter: { range: { [HTTP_RESPONSE_STATUS_CODE]: { gte: 400 } } }, diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts index 7248399d1f93f5..fbdddea32deb4f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -36,7 +36,7 @@ export async function getTransactionBreakdown({ transactionName?: string; transactionType: string; }) { - const { uiFiltersES, apmEventClient, start, end } = setup; + const { uiFiltersES, apmEventClient, start, end, config } = setup; const subAggs = { sum_all_self_times: { @@ -104,7 +104,11 @@ export async function getTransactionBreakdown({ aggs: { ...subAggs, by_date: { - date_histogram: getMetricsDateHistogramParams(start, end), + date_histogram: getMetricsDateHistogramParams( + start, + end, + config['xpack.apm.metricsInterval'] + ), aggs: subAggs, }, }, diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/error_rate.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/error_rate.json index 9ff45ebdbb21b7..e448729f44a98b 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/error_rate.json +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/error_rate.json @@ -1,42 +1,970 @@ { - "noHits":false, - "erroneousTransactionsRate":[ - { - "x":1593413100000, - "y":null - }, - { - "x":1593413130000, - "y":null - }, - { - "x":1593413160000, - "y":null - }, - { - "x":1593413190000, - "y":null - }, - { - "x":1593413220000, - "y":null - }, - { - "x":1593413250000, - "y":0 - }, - { - "x":1593413280000, - "y":0.14102564102564102 - }, - { - "x":1593413310000, - "y":0.14634146341463414 - }, - { - "x":1593413340000, - "y":null - } - ], - "average":0.09578903481342504 -} \ No newline at end of file + "noHits": false, + "erroneousTransactionsRate": [ + { + "x": 1593413100000, + "y": null + }, + { + "x": 1593413101000, + "y": null + }, + { + "x": 1593413102000, + "y": null + }, + { + "x": 1593413103000, + "y": null + }, + { + "x": 1593413104000, + "y": null + }, + { + "x": 1593413105000, + "y": null + }, + { + "x": 1593413106000, + "y": null + }, + { + "x": 1593413107000, + "y": null + }, + { + "x": 1593413108000, + "y": null + }, + { + "x": 1593413109000, + "y": null + }, + { + "x": 1593413110000, + "y": null + }, + { + "x": 1593413111000, + "y": null + }, + { + "x": 1593413112000, + "y": null + }, + { + "x": 1593413113000, + "y": null + }, + { + "x": 1593413114000, + "y": null + }, + { + "x": 1593413115000, + "y": null + }, + { + "x": 1593413116000, + "y": null + }, + { + "x": 1593413117000, + "y": null + }, + { + "x": 1593413118000, + "y": null + }, + { + "x": 1593413119000, + "y": null + }, + { + "x": 1593413120000, + "y": null + }, + { + "x": 1593413121000, + "y": null + }, + { + "x": 1593413122000, + "y": null + }, + { + "x": 1593413123000, + "y": null + }, + { + "x": 1593413124000, + "y": null + }, + { + "x": 1593413125000, + "y": null + }, + { + "x": 1593413126000, + "y": null + }, + { + "x": 1593413127000, + "y": null + }, + { + "x": 1593413128000, + "y": null + }, + { + "x": 1593413129000, + "y": null + }, + { + "x": 1593413130000, + "y": null + }, + { + "x": 1593413131000, + "y": null + }, + { + "x": 1593413132000, + "y": null + }, + { + "x": 1593413133000, + "y": null + }, + { + "x": 1593413134000, + "y": null + }, + { + "x": 1593413135000, + "y": null + }, + { + "x": 1593413136000, + "y": null + }, + { + "x": 1593413137000, + "y": null + }, + { + "x": 1593413138000, + "y": null + }, + { + "x": 1593413139000, + "y": null + }, + { + "x": 1593413140000, + "y": null + }, + { + "x": 1593413141000, + "y": null + }, + { + "x": 1593413142000, + "y": null + }, + { + "x": 1593413143000, + "y": null + }, + { + "x": 1593413144000, + "y": null + }, + { + "x": 1593413145000, + "y": null + }, + { + "x": 1593413146000, + "y": null + }, + { + "x": 1593413147000, + "y": null + }, + { + "x": 1593413148000, + "y": null + }, + { + "x": 1593413149000, + "y": null + }, + { + "x": 1593413150000, + "y": null + }, + { + "x": 1593413151000, + "y": null + }, + { + "x": 1593413152000, + "y": null + }, + { + "x": 1593413153000, + "y": null + }, + { + "x": 1593413154000, + "y": null + }, + { + "x": 1593413155000, + "y": null + }, + { + "x": 1593413156000, + "y": null + }, + { + "x": 1593413157000, + "y": null + }, + { + "x": 1593413158000, + "y": null + }, + { + "x": 1593413159000, + "y": null + }, + { + "x": 1593413160000, + "y": null + }, + { + "x": 1593413161000, + "y": null + }, + { + "x": 1593413162000, + "y": null + }, + { + "x": 1593413163000, + "y": null + }, + { + "x": 1593413164000, + "y": null + }, + { + "x": 1593413165000, + "y": null + }, + { + "x": 1593413166000, + "y": null + }, + { + "x": 1593413167000, + "y": null + }, + { + "x": 1593413168000, + "y": null + }, + { + "x": 1593413169000, + "y": null + }, + { + "x": 1593413170000, + "y": null + }, + { + "x": 1593413171000, + "y": null + }, + { + "x": 1593413172000, + "y": null + }, + { + "x": 1593413173000, + "y": null + }, + { + "x": 1593413174000, + "y": null + }, + { + "x": 1593413175000, + "y": null + }, + { + "x": 1593413176000, + "y": null + }, + { + "x": 1593413177000, + "y": null + }, + { + "x": 1593413178000, + "y": null + }, + { + "x": 1593413179000, + "y": null + }, + { + "x": 1593413180000, + "y": null + }, + { + "x": 1593413181000, + "y": null + }, + { + "x": 1593413182000, + "y": null + }, + { + "x": 1593413183000, + "y": null + }, + { + "x": 1593413184000, + "y": null + }, + { + "x": 1593413185000, + "y": null + }, + { + "x": 1593413186000, + "y": null + }, + { + "x": 1593413187000, + "y": null + }, + { + "x": 1593413188000, + "y": null + }, + { + "x": 1593413189000, + "y": null + }, + { + "x": 1593413190000, + "y": null + }, + { + "x": 1593413191000, + "y": null + }, + { + "x": 1593413192000, + "y": null + }, + { + "x": 1593413193000, + "y": null + }, + { + "x": 1593413194000, + "y": null + }, + { + "x": 1593413195000, + "y": null + }, + { + "x": 1593413196000, + "y": null + }, + { + "x": 1593413197000, + "y": null + }, + { + "x": 1593413198000, + "y": null + }, + { + "x": 1593413199000, + "y": null + }, + { + "x": 1593413200000, + "y": null + }, + { + "x": 1593413201000, + "y": null + }, + { + "x": 1593413202000, + "y": null + }, + { + "x": 1593413203000, + "y": null + }, + { + "x": 1593413204000, + "y": null + }, + { + "x": 1593413205000, + "y": null + }, + { + "x": 1593413206000, + "y": null + }, + { + "x": 1593413207000, + "y": null + }, + { + "x": 1593413208000, + "y": null + }, + { + "x": 1593413209000, + "y": null + }, + { + "x": 1593413210000, + "y": null + }, + { + "x": 1593413211000, + "y": null + }, + { + "x": 1593413212000, + "y": null + }, + { + "x": 1593413213000, + "y": null + }, + { + "x": 1593413214000, + "y": null + }, + { + "x": 1593413215000, + "y": null + }, + { + "x": 1593413216000, + "y": null + }, + { + "x": 1593413217000, + "y": null + }, + { + "x": 1593413218000, + "y": null + }, + { + "x": 1593413219000, + "y": null + }, + { + "x": 1593413220000, + "y": null + }, + { + "x": 1593413221000, + "y": null + }, + { + "x": 1593413222000, + "y": null + }, + { + "x": 1593413223000, + "y": null + }, + { + "x": 1593413224000, + "y": null + }, + { + "x": 1593413225000, + "y": null + }, + { + "x": 1593413226000, + "y": null + }, + { + "x": 1593413227000, + "y": null + }, + { + "x": 1593413228000, + "y": null + }, + { + "x": 1593413229000, + "y": null + }, + { + "x": 1593413230000, + "y": null + }, + { + "x": 1593413231000, + "y": null + }, + { + "x": 1593413232000, + "y": null + }, + { + "x": 1593413233000, + "y": null + }, + { + "x": 1593413234000, + "y": null + }, + { + "x": 1593413235000, + "y": null + }, + { + "x": 1593413236000, + "y": null + }, + { + "x": 1593413237000, + "y": null + }, + { + "x": 1593413238000, + "y": null + }, + { + "x": 1593413239000, + "y": null + }, + { + "x": 1593413240000, + "y": null + }, + { + "x": 1593413241000, + "y": null + }, + { + "x": 1593413242000, + "y": null + }, + { + "x": 1593413243000, + "y": null + }, + { + "x": 1593413244000, + "y": null + }, + { + "x": 1593413245000, + "y": null + }, + { + "x": 1593413246000, + "y": null + }, + { + "x": 1593413247000, + "y": null + }, + { + "x": 1593413248000, + "y": null + }, + { + "x": 1593413249000, + "y": null + }, + { + "x": 1593413250000, + "y": null + }, + { + "x": 1593413251000, + "y": null + }, + { + "x": 1593413252000, + "y": null + }, + { + "x": 1593413253000, + "y": null + }, + { + "x": 1593413254000, + "y": null + }, + { + "x": 1593413255000, + "y": null + }, + { + "x": 1593413256000, + "y": null + }, + { + "x": 1593413257000, + "y": null + }, + { + "x": 1593413258000, + "y": null + }, + { + "x": 1593413259000, + "y": null + }, + { + "x": 1593413260000, + "y": null + }, + { + "x": 1593413261000, + "y": null + }, + { + "x": 1593413262000, + "y": null + }, + { + "x": 1593413263000, + "y": null + }, + { + "x": 1593413264000, + "y": null + }, + { + "x": 1593413265000, + "y": null + }, + { + "x": 1593413266000, + "y": null + }, + { + "x": 1593413267000, + "y": null + }, + { + "x": 1593413268000, + "y": null + }, + { + "x": 1593413269000, + "y": null + }, + { + "x": 1593413270000, + "y": null + }, + { + "x": 1593413271000, + "y": null + }, + { + "x": 1593413272000, + "y": 0 + }, + { + "x": 1593413273000, + "y": 0 + }, + { + "x": 1593413274000, + "y": null + }, + { + "x": 1593413275000, + "y": null + }, + { + "x": 1593413276000, + "y": null + }, + { + "x": 1593413277000, + "y": 0 + }, + { + "x": 1593413278000, + "y": null + }, + { + "x": 1593413279000, + "y": null + }, + { + "x": 1593413280000, + "y": null + }, + { + "x": 1593413281000, + "y": 0 + }, + { + "x": 1593413282000, + "y": null + }, + { + "x": 1593413283000, + "y": null + }, + { + "x": 1593413284000, + "y": 0 + }, + { + "x": 1593413285000, + "y": 0 + }, + { + "x": 1593413286000, + "y": 0.125 + }, + { + "x": 1593413287000, + "y": 0.5 + }, + { + "x": 1593413288000, + "y": 0 + }, + { + "x": 1593413289000, + "y": 0.5 + }, + { + "x": 1593413290000, + "y": 0 + }, + { + "x": 1593413291000, + "y": 0 + }, + { + "x": 1593413292000, + "y": 0.5 + }, + { + "x": 1593413293000, + "y": 0 + }, + { + "x": 1593413294000, + "y": 0 + }, + { + "x": 1593413295000, + "y": 0 + }, + { + "x": 1593413296000, + "y": 0 + }, + { + "x": 1593413297000, + "y": 0 + }, + { + "x": 1593413298000, + "y": 0 + }, + { + "x": 1593413299000, + "y": 0.5 + }, + { + "x": 1593413300000, + "y": 0.3333333333333333 + }, + { + "x": 1593413301000, + "y": 0.14285714285714285 + }, + { + "x": 1593413302000, + "y": 0 + }, + { + "x": 1593413303000, + "y": 0 + }, + { + "x": 1593413304000, + "y": 0 + }, + { + "x": 1593413305000, + "y": 0.6666666666666666 + }, + { + "x": 1593413306000, + "y": 0 + }, + { + "x": 1593413307000, + "y": 0 + }, + { + "x": 1593413308000, + "y": 0.3333333333333333 + }, + { + "x": 1593413309000, + "y": 0.3333333333333333 + }, + { + "x": 1593413310000, + "y": 0.3333333333333333 + }, + { + "x": 1593413311000, + "y": 0.5 + }, + { + "x": 1593413312000, + "y": 0 + }, + { + "x": 1593413313000, + "y": 0 + }, + { + "x": 1593413314000, + "y": 0 + }, + { + "x": 1593413315000, + "y": 0.5 + }, + { + "x": 1593413316000, + "y": 0 + }, + { + "x": 1593413317000, + "y": 0 + }, + { + "x": 1593413318000, + "y": 0 + }, + { + "x": 1593413319000, + "y": 0 + }, + { + "x": 1593413320000, + "y": 0.3333333333333333 + }, + { + "x": 1593413321000, + "y": 0 + }, + { + "x": 1593413322000, + "y": 0.5 + }, + { + "x": 1593413323000, + "y": null + }, + { + "x": 1593413324000, + "y": null + }, + { + "x": 1593413325000, + "y": null + }, + { + "x": 1593413326000, + "y": null + }, + { + "x": 1593413327000, + "y": null + }, + { + "x": 1593413328000, + "y": null + }, + { + "x": 1593413329000, + "y": null + }, + { + "x": 1593413330000, + "y": null + }, + { + "x": 1593413331000, + "y": null + }, + { + "x": 1593413332000, + "y": null + }, + { + "x": 1593413333000, + "y": null + }, + { + "x": 1593413334000, + "y": null + }, + { + "x": 1593413335000, + "y": null + }, + { + "x": 1593413336000, + "y": null + }, + { + "x": 1593413337000, + "y": null + }, + { + "x": 1593413338000, + "y": null + }, + { + "x": 1593413339000, + "y": null + }, + { + "x": 1593413340000, + "y": null + } + ], + "average": 0.14188815060908083 +} From 86d7050822865eb96b2fe8faf9997a71b71aaedd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Wed, 26 Aug 2020 12:51:22 +0100 Subject: [PATCH 027/216] [Telemetry] Add Application Usage Schema (#75283) Co-authored-by: Elastic Machine --- .telemetryrc.json | 1 - .../src/tools/__fixture__/mock_schema.json | 16 + ...exed_interface_with_not_matching_schema.ts | 54 + .../__fixture__/parsed_working_collector.ts | 22 + .../extract_collectors.test.ts.snap | 54 + .../tools/check_collector__integrity.test.ts | 15 + .../src/tools/extract_collectors.test.ts | 2 +- .../src/tools/serializer.ts | 5 + .../kbn-telemetry-tools/src/tools/utils.ts | 37 +- ...exed_interface_with_not_matching_schema.ts | 48 + .../telemetry_collectors/working_collector.ts | 9 + .../collectors/application_usage/schema.ts | 99 ++ .../telemetry_application_usage_collector.ts | 168 +-- src/plugins/telemetry/schema/oss_plugins.json | 1208 +++++++++++++++++ 14 files changed, 1650 insertions(+), 88 deletions(-) create mode 100644 packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts create mode 100644 src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts diff --git a/.telemetryrc.json b/.telemetryrc.json index 2f57566159a700..818f9805628e17 100644 --- a/.telemetryrc.json +++ b/.telemetryrc.json @@ -7,7 +7,6 @@ "src/plugins/testbed/", "src/plugins/kibana_utils/", "src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts", - "src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts", "src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts", "src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts", "src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts" diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json b/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json index e87699825b4e1d..2e69d3625d7ff0 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json @@ -5,6 +5,22 @@ "flat": { "type": "keyword" }, + "my_index_signature_prop": { + "properties": { + "avg": { + "type": "number" + }, + "count": { + "type": "number" + }, + "max": { + "type": "number" + }, + "min": { + "type": "number" + } + } + }, "my_str": { "type": "text" }, diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts new file mode 100644 index 00000000000000..83866a2b6afec0 --- /dev/null +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts @@ -0,0 +1,54 @@ +/* + * 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 { SyntaxKind } from 'typescript'; +import { ParsedUsageCollection } from '../ts_parser'; + +export const parsedIndexedInterfaceWithNoMatchingSchema: ParsedUsageCollection = [ + 'src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts', + { + collectorName: 'indexed_interface_with_not_matching_schema', + schema: { + value: { + something: { + count_1: { + type: 'number', + }, + }, + }, + }, + fetch: { + typeName: 'Usage', + typeDescriptor: { + '': { + '@@INDEX@@': { + count_1: { + kind: SyntaxKind.NumberKeyword, + type: 'NumberKeyword', + }, + count_2: { + kind: SyntaxKind.NumberKeyword, + type: 'NumberKeyword', + }, + }, + }, + }, + }, + }, +]; diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts index 803bc7f13f59e1..b238c5aa346adb 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts @@ -32,6 +32,20 @@ export const parsedWorkingCollector: ParsedUsageCollection = [ my_str: { type: 'text', }, + my_index_signature_prop: { + avg: { + type: 'number', + }, + count: { + type: 'number', + }, + max: { + type: 'number', + }, + min: { + type: 'number', + }, + }, my_objects: { total: { type: 'number', @@ -60,6 +74,14 @@ export const parsedWorkingCollector: ParsedUsageCollection = [ kind: SyntaxKind.StringKeyword, type: 'StringKeyword', }, + my_index_signature_prop: { + '': { + '@@INDEX@@': { + kind: SyntaxKind.NumberKeyword, + type: 'NumberKeyword', + }, + }, + }, my_objects: { total: { kind: SyntaxKind.NumberKeyword, diff --git a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap index fc933b6c7fd355..bf1d5ffc1101ee 100644 --- a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap +++ b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap @@ -90,6 +90,38 @@ Array [ }, }, ], + Array [ + "src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts", + Object { + "collectorName": "indexed_interface_with_not_matching_schema", + "fetch": Object { + "typeDescriptor": Object { + "": Object { + "@@INDEX@@": Object { + "count_1": Object { + "kind": 140, + "type": "NumberKeyword", + }, + "count_2": Object { + "kind": 140, + "type": "NumberKeyword", + }, + }, + }, + }, + "typeName": "Usage", + }, + "schema": Object { + "value": Object { + "something": Object { + "count_1": Object { + "type": "long", + }, + }, + }, + }, + }, + ], Array [ "src/fixtures/telemetry_collectors/nested_collector.ts", Object { @@ -132,6 +164,14 @@ Array [ "type": "BooleanKeyword", }, }, + "my_index_signature_prop": Object { + "": Object { + "@@INDEX@@": Object { + "kind": 140, + "type": "NumberKeyword", + }, + }, + }, "my_objects": Object { "total": Object { "kind": 140, @@ -166,6 +206,20 @@ Array [ "type": "boolean", }, }, + "my_index_signature_prop": Object { + "avg": Object { + "type": "number", + }, + "count": Object { + "type": "number", + }, + "max": Object { + "type": "number", + }, + "min": Object { + "type": "number", + }, + }, "my_objects": Object { "total": Object { "type": "number", diff --git a/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts b/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts index dbdda3f38afd5b..a101210185a638 100644 --- a/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts +++ b/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts @@ -20,6 +20,7 @@ import { cloneDeep } from 'lodash'; import * as ts from 'typescript'; import { parsedWorkingCollector } from './__fixture__/parsed_working_collector'; +import { parsedIndexedInterfaceWithNoMatchingSchema } from './__fixture__/parsed_indexed_interface_with_not_matching_schema'; import { checkCompatibleTypeDescriptor, checkMatchingMapping } from './check_collector_integrity'; import * as path from 'path'; import { readFile } from 'fs'; @@ -82,6 +83,20 @@ describe('checkCompatibleTypeDescriptor', () => { expect(incompatibles).toHaveLength(0); }); + it('returns diff on indexed interface with no matching schema', () => { + const incompatibles = checkCompatibleTypeDescriptor([ + parsedIndexedInterfaceWithNoMatchingSchema, + ]); + expect(incompatibles).toHaveLength(1); + const { diff, message } = incompatibles[0]; + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(diff).toEqual({ '.@@INDEX@@.count_2.kind': 'number' }); + expect(message).toHaveLength(1); + expect(message).toEqual([ + 'incompatible Type key (Usage..@@INDEX@@.count_2): expected (undefined) got ("number").', + ]); + }); + describe('Interface Change', () => { it('returns diff on incompatible type descriptor with mapping', () => { const malformedParsedCollector = cloneDeep(parsedWorkingCollector); diff --git a/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts b/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts index 1b4ed21a1635cf..0517cb9034d0aa 100644 --- a/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts +++ b/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts @@ -34,7 +34,7 @@ describe('extractCollectors', () => { const programPaths = await getProgramPaths(configs[0]); const results = [...extractCollectors(programPaths, tsConfig)]; - expect(results).toHaveLength(6); + expect(results).toHaveLength(7); expect(results).toMatchSnapshot(); }); }); diff --git a/packages/kbn-telemetry-tools/src/tools/serializer.ts b/packages/kbn-telemetry-tools/src/tools/serializer.ts index bce5dd7f58643b..f945402ec5fc2a 100644 --- a/packages/kbn-telemetry-tools/src/tools/serializer.ts +++ b/packages/kbn-telemetry-tools/src/tools/serializer.ts @@ -84,6 +84,11 @@ export function getDescriptor(node: ts.Node, program: ts.Program): Descriptor | }, {} as any); } + // If it's defined as signature { [key: string]: OtherInterface } + if (ts.isIndexSignatureDeclaration(node) && node.type) { + return { '@@INDEX@@': getDescriptor(node.type, program) }; + } + if (ts.SyntaxKind.FirstNode === node.kind) { return getDescriptor((node as any).right, program); } diff --git a/packages/kbn-telemetry-tools/src/tools/utils.ts b/packages/kbn-telemetry-tools/src/tools/utils.ts index 212b06a4c98954..c1424785b22a54 100644 --- a/packages/kbn-telemetry-tools/src/tools/utils.ts +++ b/packages/kbn-telemetry-tools/src/tools/utils.ts @@ -98,6 +98,14 @@ export function getVariableValue(node: ts.Node): string | Record { return serializeObject(node); } + if (ts.isIdentifier(node)) { + const declaration = getIdentifierDeclaration(node); + if (ts.isVariableDeclaration(declaration) && declaration.initializer) { + return getVariableValue(declaration.initializer); + } + // TODO: If this is another imported value from another file, we'll need to go fetch it like in getPropertyValue + } + throw Error(`Unsuppored Node: cannot get value of node (${node.getText()}) of kind ${node.kind}`); } @@ -112,10 +120,11 @@ export function serializeObject(node: ts.Node) { if (typeof propertyName === 'undefined') { throw new Error(`Unable to get property name ${property.getText()}`); } + const cleanPropertyName = propertyName.replace(/["']/g, ''); if (ts.isPropertyAssignment(property)) { - value[propertyName] = getVariableValue(property.initializer); + value[cleanPropertyName] = getVariableValue(property.initializer); } else { - value[propertyName] = getVariableValue(property); + value[cleanPropertyName] = getVariableValue(property); } } @@ -222,9 +231,29 @@ export const flattenKeys = (obj: any, keyPath: any[] = []): any => { }; export function difference(actual: any, expected: any) { - function changes(obj: any, base: any) { + function changes(obj: { [key: string]: any }, base: { [key: string]: any }) { return transform(obj, function (result, value, key) { - if (key && !isEqual(value, base[key])) { + if (key && /@@INDEX@@/.test(`${key}`)) { + // The type definition is an Index Signature, fuzzy searching for similar keys + const regexp = new RegExp(`${key}`.replace(/@@INDEX@@/g, '(.+)?')); + const keysInBase = Object.keys(base) + .map((k) => { + const match = k.match(regexp); + return match && match[0]; + }) + .filter((s): s is string => !!s); + + if (keysInBase.length === 0) { + // Mark this key as wrong because we couldn't find any matching keys + result[key] = value; + } + + keysInBase.forEach((k) => { + if (!isEqual(value, base[k])) { + result[k] = isObject(value) && isObject(base[k]) ? changes(value, base[k]) : value; + } + }); + } else if (key && !isEqual(value, base[key])) { result[key] = isObject(value) && isObject(base[key]) ? changes(value, base[key]) : value; } }); diff --git a/src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts b/src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts new file mode 100644 index 00000000000000..0ec8d2e15c34a8 --- /dev/null +++ b/src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts @@ -0,0 +1,48 @@ +/* + * 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 { CollectorSet } from '../../plugins/usage_collection/server/collector'; +import { loggerMock } from '../../core/server/logging/logger.mock'; + +const { makeUsageCollector } = new CollectorSet({ + logger: loggerMock.create(), + maximumWaitTimeForAllCollectorsInS: 0, +}); + +interface Usage { + [key: string]: { + count_1?: number; + count_2?: number; + }; +} + +export const myCollector = makeUsageCollector({ + type: 'indexed_interface_with_not_matching_schema', + isReady: () => true, + fetch() { + if (Math.random()) { + return { something: { count_1: 1 } }; + } + return { something: { count_2: 2 } }; + }, + schema: { + something: { + count_1: { type: 'long' }, // Intentionally missing count_2 + }, + }, +}); diff --git a/src/fixtures/telemetry_collectors/working_collector.ts b/src/fixtures/telemetry_collectors/working_collector.ts index d58a89db97d740..bdf10b5e549193 100644 --- a/src/fixtures/telemetry_collectors/working_collector.ts +++ b/src/fixtures/telemetry_collectors/working_collector.ts @@ -35,6 +35,9 @@ interface Usage { my_objects: MyObject; my_array?: MyObject[]; my_str_array?: string[]; + my_index_signature_prop?: { + [key: string]: number; + }; } const SOME_NUMBER: number = 123; @@ -93,5 +96,11 @@ export const myCollector = makeUsageCollector({ type: { type: 'boolean' }, }, my_str_array: { type: 'keyword' }, + my_index_signature_prop: { + count: { type: 'number' }, + avg: { type: 'number' }, + max: { type: 'number' }, + min: { type: 'number' }, + }, }, }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts new file mode 100644 index 00000000000000..6efe8725535838 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -0,0 +1,99 @@ +/* + * 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 { MakeSchemaFrom } from 'src/plugins/usage_collection/server'; +import { ApplicationUsageTelemetryReport } from './telemetry_application_usage_collector'; + +const commonSchema: MakeSchemaFrom = { + clicks_total: { + type: 'long', + }, + clicks_7_days: { + type: 'long', + }, + clicks_30_days: { + type: 'long', + }, + clicks_90_days: { + type: 'long', + }, + minutes_on_screen_total: { + type: 'float', + }, + minutes_on_screen_7_days: { + type: 'float', + }, + minutes_on_screen_30_days: { + type: 'float', + }, + minutes_on_screen_90_days: { + type: 'float', + }, +}; + +// These keys obtained by searching for `/application\w*\.register\(/` and checking the value of the attr `id`. +// TODO: Find a way to update these keys automatically. +export const applicationUsageSchema = { + // OSS + dashboards: commonSchema, + dev_tools: commonSchema, + discover: commonSchema, + home: commonSchema, + kibana: commonSchema, // It's a forward app so we'll likely never report it + management: commonSchema, + short_url_redirect: commonSchema, // It's a forward app so we'll likely never report it + timelion: commonSchema, + visualize: commonSchema, + + // X-Pack + apm: commonSchema, + csm: commonSchema, + canvas: commonSchema, + dashboard_mode: commonSchema, // It's a forward app so we'll likely never report it + appSearch: commonSchema, + workplaceSearch: commonSchema, + graph: commonSchema, + logs: commonSchema, + metrics: commonSchema, + infra: commonSchema, // It's a forward app so we'll likely never report it + ingestManager: commonSchema, + lens: commonSchema, + maps: commonSchema, + ml: commonSchema, + monitoring: commonSchema, + 'observability-overview': commonSchema, + security_account: commonSchema, + security_access_agreement: commonSchema, + security_capture_url: commonSchema, // It's a forward app so we'll likely never report it + security_logged_out: commonSchema, + security_login: commonSchema, + security_logout: commonSchema, + security_overwritten_session: commonSchema, + securitySolution: commonSchema, // It's a forward app so we'll likely never report it + 'securitySolution:overview': commonSchema, + 'securitySolution:detections': commonSchema, + 'securitySolution:hosts': commonSchema, + 'securitySolution:network': commonSchema, + 'securitySolution:timelines': commonSchema, + 'securitySolution:case': commonSchema, + 'securitySolution:administration': commonSchema, + siem: commonSchema, + space_selector: commonSchema, + uptime: commonSchema, +}; diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts index 1f22ab01001010..69137681e0597f 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts @@ -26,6 +26,7 @@ import { ApplicationUsageTransactional, registerMappings, } from './saved_objects_types'; +import { applicationUsageSchema } from './schema'; /** * Roll indices every 24h @@ -40,7 +41,7 @@ export const ROLL_INDICES_START = 5 * 60 * 1000; export const SAVED_OBJECTS_TOTAL_TYPE = 'application_usage_totals'; export const SAVED_OBJECTS_TRANSACTIONAL_TYPE = 'application_usage_transactional'; -interface ApplicationUsageTelemetryReport { +export interface ApplicationUsageTelemetryReport { [appId: string]: { clicks_total: number; clicks_7_days: number; @@ -60,93 +61,96 @@ export function registerApplicationUsageCollector( ) { registerMappings(registerType); - const collector = usageCollection.makeUsageCollector({ - type: 'application_usage', - isReady: () => typeof getSavedObjectsClient() !== 'undefined', - fetch: async () => { - const savedObjectsClient = getSavedObjectsClient(); - if (typeof savedObjectsClient === 'undefined') { - return; - } - const [rawApplicationUsageTotals, rawApplicationUsageTransactional] = await Promise.all([ - findAll(savedObjectsClient, { type: SAVED_OBJECTS_TOTAL_TYPE }), - findAll(savedObjectsClient, { - type: SAVED_OBJECTS_TRANSACTIONAL_TYPE, - }), - ]); - - const applicationUsageFromTotals = rawApplicationUsageTotals.reduce( - (acc, { attributes: { appId, minutesOnScreen, numberOfClicks } }) => { - const existing = acc[appId] || { clicks_total: 0, minutes_on_screen_total: 0 }; - return { - ...acc, - [appId]: { - clicks_total: numberOfClicks + existing.clicks_total, + const collector = usageCollection.makeUsageCollector( + { + type: 'application_usage', + isReady: () => typeof getSavedObjectsClient() !== 'undefined', + schema: applicationUsageSchema, + fetch: async () => { + const savedObjectsClient = getSavedObjectsClient(); + if (typeof savedObjectsClient === 'undefined') { + return; + } + const [rawApplicationUsageTotals, rawApplicationUsageTransactional] = await Promise.all([ + findAll(savedObjectsClient, { type: SAVED_OBJECTS_TOTAL_TYPE }), + findAll(savedObjectsClient, { + type: SAVED_OBJECTS_TRANSACTIONAL_TYPE, + }), + ]); + + const applicationUsageFromTotals = rawApplicationUsageTotals.reduce( + (acc, { attributes: { appId, minutesOnScreen, numberOfClicks } }) => { + const existing = acc[appId] || { clicks_total: 0, minutes_on_screen_total: 0 }; + return { + ...acc, + [appId]: { + clicks_total: numberOfClicks + existing.clicks_total, + clicks_7_days: 0, + clicks_30_days: 0, + clicks_90_days: 0, + minutes_on_screen_total: minutesOnScreen + existing.minutes_on_screen_total, + minutes_on_screen_7_days: 0, + minutes_on_screen_30_days: 0, + minutes_on_screen_90_days: 0, + }, + }; + }, + {} as ApplicationUsageTelemetryReport + ); + const nowMinus7 = moment().subtract(7, 'days'); + const nowMinus30 = moment().subtract(30, 'days'); + const nowMinus90 = moment().subtract(90, 'days'); + + const applicationUsage = rawApplicationUsageTransactional.reduce( + (acc, { attributes: { appId, minutesOnScreen, numberOfClicks, timestamp } }) => { + const existing = acc[appId] || { + clicks_total: 0, clicks_7_days: 0, clicks_30_days: 0, clicks_90_days: 0, - minutes_on_screen_total: minutesOnScreen + existing.minutes_on_screen_total, + minutes_on_screen_total: 0, minutes_on_screen_7_days: 0, minutes_on_screen_30_days: 0, minutes_on_screen_90_days: 0, - }, - }; - }, - {} as ApplicationUsageTelemetryReport - ); - const nowMinus7 = moment().subtract(7, 'days'); - const nowMinus30 = moment().subtract(30, 'days'); - const nowMinus90 = moment().subtract(90, 'days'); - - const applicationUsage = rawApplicationUsageTransactional.reduce( - (acc, { attributes: { appId, minutesOnScreen, numberOfClicks, timestamp } }) => { - const existing = acc[appId] || { - clicks_total: 0, - clicks_7_days: 0, - clicks_30_days: 0, - clicks_90_days: 0, - minutes_on_screen_total: 0, - minutes_on_screen_7_days: 0, - minutes_on_screen_30_days: 0, - minutes_on_screen_90_days: 0, - }; - - const timeOfEntry = moment(timestamp as string); - const isInLast7Days = timeOfEntry.isSameOrAfter(nowMinus7); - const isInLast30Days = timeOfEntry.isSameOrAfter(nowMinus30); - const isInLast90Days = timeOfEntry.isSameOrAfter(nowMinus90); - - const last7Days = { - clicks_7_days: existing.clicks_7_days + numberOfClicks, - minutes_on_screen_7_days: existing.minutes_on_screen_7_days + minutesOnScreen, - }; - const last30Days = { - clicks_30_days: existing.clicks_30_days + numberOfClicks, - minutes_on_screen_30_days: existing.minutes_on_screen_30_days + minutesOnScreen, - }; - const last90Days = { - clicks_90_days: existing.clicks_90_days + numberOfClicks, - minutes_on_screen_90_days: existing.minutes_on_screen_90_days + minutesOnScreen, - }; - - return { - ...acc, - [appId]: { - ...existing, - clicks_total: existing.clicks_total + numberOfClicks, - minutes_on_screen_total: existing.minutes_on_screen_total + minutesOnScreen, - ...(isInLast7Days ? last7Days : {}), - ...(isInLast30Days ? last30Days : {}), - ...(isInLast90Days ? last90Days : {}), - }, - }; - }, - applicationUsageFromTotals - ); - - return applicationUsage; - }, - }); + }; + + const timeOfEntry = moment(timestamp as string); + const isInLast7Days = timeOfEntry.isSameOrAfter(nowMinus7); + const isInLast30Days = timeOfEntry.isSameOrAfter(nowMinus30); + const isInLast90Days = timeOfEntry.isSameOrAfter(nowMinus90); + + const last7Days = { + clicks_7_days: existing.clicks_7_days + numberOfClicks, + minutes_on_screen_7_days: existing.minutes_on_screen_7_days + minutesOnScreen, + }; + const last30Days = { + clicks_30_days: existing.clicks_30_days + numberOfClicks, + minutes_on_screen_30_days: existing.minutes_on_screen_30_days + minutesOnScreen, + }; + const last90Days = { + clicks_90_days: existing.clicks_90_days + numberOfClicks, + minutes_on_screen_90_days: existing.minutes_on_screen_90_days + minutesOnScreen, + }; + + return { + ...acc, + [appId]: { + ...existing, + clicks_total: existing.clicks_total + numberOfClicks, + minutes_on_screen_total: existing.minutes_on_screen_total + minutesOnScreen, + ...(isInLast7Days ? last7Days : {}), + ...(isInLast30Days ? last30Days : {}), + ...(isInLast90Days ? last90Days : {}), + }, + }; + }, + applicationUsageFromTotals + ); + + return applicationUsage; + }, + } + ); usageCollection.registerCollector(collector); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index c306446b9780d4..acd575badbe5b8 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -48,6 +48,1214 @@ } } }, + "application_usage": { + "properties": { + "dashboards": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "dev_tools": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "discover": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "home": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "kibana": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "management": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "short_url_redirect": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "timelion": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "visualize": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "apm": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "csm": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "canvas": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "dashboard_mode": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "appSearch": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "workplaceSearch": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "graph": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "logs": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "metrics": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "infra": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "ingestManager": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "lens": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "maps": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "ml": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "monitoring": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "observability-overview": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "security_account": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "security_access_agreement": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "security_capture_url": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "security_logged_out": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "security_login": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "security_logout": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "security_overwritten_session": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "securitySolution": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "securitySolution:overview": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "securitySolution:detections": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "securitySolution:hosts": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "securitySolution:network": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "securitySolution:timelines": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "securitySolution:case": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "securitySolution:administration": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "siem": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "space_selector": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, + "uptime": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + } + } + }, "csp": { "properties": { "strict": { From 63265b6f57e421c335945aa4e948bd3334876222 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 26 Aug 2020 08:50:52 -0400 Subject: [PATCH 028/216] Compute AAD to encrypty/decrypt SO only if needed (#75818) --- .../server/crypto/encrypted_saved_objects_service.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts index 99361107047c27..82d6bb9be15f67 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts @@ -198,12 +198,15 @@ export class EncryptedSavedObjectsService { if (typeDefinition === undefined) { return attributes; } + let encryptionAAD: string | undefined; - const encryptionAAD = this.getAAD(typeDefinition, descriptor, attributes); const encryptedAttributes: Record = {}; for (const attributeName of typeDefinition.attributesToEncrypt) { const attributeValue = attributes[attributeName]; if (attributeValue != null) { + if (!encryptionAAD) { + encryptionAAD = this.getAAD(typeDefinition, descriptor, attributes); + } try { encryptedAttributes[attributeName] = (yield [attributeValue, encryptionAAD])!; } catch (err) { @@ -376,8 +379,7 @@ export class EncryptedSavedObjectsService { if (typeDefinition === undefined) { return attributes; } - - const encryptionAAD = this.getAAD(typeDefinition, descriptor, attributes); + let encryptionAAD: string | undefined; const decryptedAttributes: Record = {}; for (const attributeName of typeDefinition.attributesToEncrypt) { const attributeValue = attributes[attributeName]; @@ -393,7 +395,9 @@ export class EncryptedSavedObjectsService { )}` ); } - + if (!encryptionAAD) { + encryptionAAD = this.getAAD(typeDefinition, descriptor, attributes); + } try { decryptedAttributes[attributeName] = (yield [attributeValue, encryptionAAD])!; } catch (err) { From 4042f82035f7dd776f54c3452be51c1fc7365786 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Wed, 26 Aug 2020 09:25:45 -0400 Subject: [PATCH 029/216] [Security Solution][Resolver] Support kuery filter (#74695) * Adding kql filter * Adding filter support for the backend and tests * Moving the filter to the body * switching events and alerts api to post * Removing unused import * Adding tests for events api results being in descending order * Switching frontend to use post for related events --- .../common/endpoint/generate_data.test.ts | 10 + .../common/endpoint/generate_data.ts | 32 +- .../common/endpoint/schema/resolver.ts | 10 + .../resolver/data_access_layer/factory.ts | 2 +- .../server/endpoint/routes/resolver.ts | 4 +- .../server/endpoint/routes/resolver/alerts.ts | 9 +- .../server/endpoint/routes/resolver/events.ts | 9 +- .../routes/resolver/queries/alerts.ts | 14 +- .../routes/resolver/queries/events.ts | 15 +- .../resolver/utils/alerts_query_handler.ts | 32 +- .../resolver/utils/events_query_handler.ts | 33 +- .../endpoint/routes/resolver/utils/fetch.ts | 70 ++-- .../routes/resolver/utils/pagination.test.ts | 14 + .../routes/resolver/utils/pagination.ts | 18 +- .../apis/resolver/alerts.ts | 159 ++++++++ .../apis/resolver/common.ts | 222 ++++++++++ .../apis/resolver/events.ts | 213 ++++++++++ .../apis/resolver/index.ts | 2 + .../apis/resolver/tree.ts | 386 +----------------- 19 files changed, 808 insertions(+), 446 deletions(-) create mode 100644 x-pack/test/security_solution_endpoint_api_int/apis/resolver/alerts.ts create mode 100644 x-pack/test/security_solution_endpoint_api_int/apis/resolver/common.ts create mode 100644 x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts 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 46fc002e76e7ff..be3a1e82356c8b 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 @@ -169,6 +169,7 @@ describe('data generator', () => { const childrenPerNode = 3; const generations = 3; const relatedAlerts = 4; + beforeEach(() => { tree = generator.generateTree({ alwaysGenMaxChildrenPerNode: true, @@ -182,6 +183,7 @@ describe('data generator', () => { { category: RelatedEventCategory.File, count: 2 }, { category: RelatedEventCategory.Network, count: 1 }, ], + relatedEventsOrdered: true, relatedAlerts, ancestryArraySize: ANCESTRY_LIMIT, }); @@ -212,6 +214,14 @@ describe('data generator', () => { } }; + it('creates related events in ascending order', () => { + // the order should not change since it should already be in ascending order + const relatedEventsAsc = _.cloneDeep(tree.origin.relatedEvents).sort( + (event1, event2) => event1['@timestamp'] - event2['@timestamp'] + ); + expect(tree.origin.relatedEvents).toStrictEqual(relatedEventsAsc); + }); + it('has ancestry array defined', () => { expect(tree.origin.lifecycle[0].process.Ext!.ancestry!.length).toBe(ANCESTRY_LIMIT); for (const event of tree.allEvents) { 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 7340b1c021eba4..0955f196df176a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -302,6 +302,12 @@ export interface TreeOptions { generations?: number; children?: number; relatedEvents?: RelatedEventInfo[] | number; + /** + * If true then the related events will be created with timestamps that preserve the + * generation order, meaning the first event will always have a timestamp number less + * than the next related event + */ + relatedEventsOrdered?: boolean; relatedAlerts?: number; percentWithRelated?: number; percentTerminated?: number; @@ -322,6 +328,7 @@ export function getTreeOptionsWithDef(options?: TreeOptions): TreeOptionDefaults generations: options?.generations ?? 2, children: options?.children ?? 2, relatedEvents: options?.relatedEvents ?? 5, + relatedEventsOrdered: options?.relatedEventsOrdered ?? false, relatedAlerts: options?.relatedAlerts ?? 3, percentWithRelated: options?.percentWithRelated ?? 30, percentTerminated: options?.percentTerminated ?? 100, @@ -809,7 +816,8 @@ export class EndpointDocGenerator { for (const relatedEvent of this.relatedEventsGenerator( node, opts.relatedEvents, - secBeforeEvent + secBeforeEvent, + opts.relatedEventsOrdered )) { eventList.push(relatedEvent); } @@ -877,6 +885,8 @@ export class EndpointDocGenerator { addRelatedAlerts(ancestor, numAlertsPerNode, processDuration, events); } } + timestamp = timestamp + 1000; + events.push( this.generateAlert( timestamp, @@ -961,7 +971,12 @@ export class EndpointDocGenerator { }); } if (this.randomN(100) < opts.percentWithRelated) { - yield* this.relatedEventsGenerator(child, opts.relatedEvents, processDuration); + yield* this.relatedEventsGenerator( + child, + opts.relatedEvents, + processDuration, + opts.relatedEventsOrdered + ); yield* this.relatedAlertsGenerator(child, opts.relatedAlerts, processDuration); } } @@ -973,13 +988,17 @@ export class EndpointDocGenerator { * @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 + * @param ordered - if true the events will have an increasing timestamp, otherwise their timestamp will be random but + * guaranteed to be greater than or equal to the originating event */ public *relatedEventsGenerator( node: Event, relatedEvents: RelatedEventInfo[] | number = 10, - processDuration: number = 6 * 3600 + processDuration: number = 6 * 3600, + ordered: boolean = false ) { let relatedEventsInfo: RelatedEventInfo[]; + let ts = node['@timestamp'] + 1; if (typeof relatedEvents === 'number') { relatedEventsInfo = [{ category: RelatedEventCategory.Random, count: relatedEvents }]; } else { @@ -995,7 +1014,12 @@ export class EndpointDocGenerator { eventInfo = OTHER_EVENT_CATEGORIES[event.category]; } - const ts = node['@timestamp'] + this.randomN(processDuration) * 1000; + if (ordered) { + ts += this.randomN(processDuration) * 1000; + } else { + ts = node['@timestamp'] + this.randomN(processDuration) * 1000; + } + yield this.generateEvent({ timestamp: ts, entityID: node.process.entity_id, diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts index f3e67f84b2880f..311aa0c04c9abf 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts @@ -33,6 +33,11 @@ export const validateEvents = { afterEvent: schema.maybe(schema.string()), legacyEndpointID: schema.maybe(schema.string({ minLength: 1 })), }), + body: schema.nullable( + schema.object({ + filter: schema.maybe(schema.string()), + }) + ), }; /** @@ -45,6 +50,11 @@ export const validateAlerts = { afterAlert: schema.maybe(schema.string()), legacyEndpointID: schema.maybe(schema.string({ minLength: 1 })), }), + body: schema.nullable( + schema.object({ + filter: schema.maybe(schema.string()), + }) + ), }; /** diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts index 016ebfa0faee40..dee53a624baff5 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts @@ -25,7 +25,7 @@ export function dataAccessLayerFactory( * Used to get non-process related events for a node. */ async relatedEvents(entityID: string): Promise { - return context.services.http.get(`/api/endpoint/resolver/${entityID}/events`, { + return context.services.http.post(`/api/endpoint/resolver/${entityID}/events`, { query: { events: 100 }, }); }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts index 5c92b23d594de7..3ec968e4a0e1a1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts @@ -24,7 +24,7 @@ import { handleEntities } from './resolver/entity'; export function registerResolverRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { const log = endpointAppContext.logFactory.get('resolver'); - router.get( + router.post( { path: '/api/endpoint/resolver/{id}/events', validate: validateEvents, @@ -33,7 +33,7 @@ export function registerResolverRoutes(router: IRouter, endpointAppContext: Endp handleEvents(log, endpointAppContext) ); - router.get( + router.post( { path: '/api/endpoint/resolver/{id}/alerts', validate: validateAlerts, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/alerts.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/alerts.ts index 830d92ef2efc0c..8e641194ab8994 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/alerts.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/alerts.ts @@ -14,11 +14,16 @@ import { EndpointAppContext } from '../../types'; export function handleAlerts( log: Logger, endpointAppContext: EndpointAppContext -): RequestHandler, TypeOf> { +): RequestHandler< + TypeOf, + TypeOf, + TypeOf +> { return async (context, req, res) => { const { params: { id }, query: { alerts, afterAlert, legacyEndpointID: endpointID }, + body, } = req; try { const client = context.core.elasticsearch.legacy.client; @@ -26,7 +31,7 @@ export function handleAlerts( const fetcher = new Fetcher(client, id, eventsIndexPattern, alertsIndexPattern, endpointID); return res.ok({ - body: await fetcher.alerts(alerts, afterAlert), + body: await fetcher.alerts(alerts, afterAlert, body?.filter), }); } catch (err) { log.warn(err); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts index 9e5c6be43f7289..80d21ae1182842 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts @@ -14,11 +14,16 @@ import { EndpointAppContext } from '../../types'; export function handleEvents( log: Logger, endpointAppContext: EndpointAppContext -): RequestHandler, TypeOf> { +): RequestHandler< + TypeOf, + TypeOf, + TypeOf +> { return async (context, req, res) => { const { params: { id }, query: { events, afterEvent, legacyEndpointID: endpointID }, + body, } = req; try { const client = context.core.elasticsearch.legacy.client; @@ -26,7 +31,7 @@ export function handleEvents( const fetcher = new Fetcher(client, id, eventsIndexPattern, alertsIndexPattern, endpointID); return res.ok({ - body: await fetcher.events(events, afterEvent), + body: await fetcher.events(events, afterEvent, body?.filter), }); } catch (err) { log.warn(err); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts index feb4a404b2359d..54c6cf432aa89f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { SearchResponse } from 'elasticsearch'; +import { esKuery } from '../../../../../../../../src/plugins/data/server'; import { ResolverEvent } from '../../../../../common/endpoint/types'; import { ResolverQuery } from './base'; import { PaginationBuilder } from '../utils/pagination'; @@ -13,12 +14,17 @@ import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/com * Builds a query for retrieving alerts for a node. */ export class AlertsQuery extends ResolverQuery { + private readonly kqlQuery: JsonObject[] = []; constructor( private readonly pagination: PaginationBuilder, indexPattern: string | string[], - endpointID?: string + endpointID?: string, + kql?: string ) { super(indexPattern, endpointID); + if (kql) { + this.kqlQuery.push(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kql))); + } } protected legacyQuery(endpointID: string, uniquePIDs: string[]): JsonObject { @@ -26,6 +32,7 @@ export class AlertsQuery extends ResolverQuery { query: { bool: { filter: [ + ...this.kqlQuery, { terms: { 'endgame.unique_pid': uniquePIDs }, }, @@ -38,7 +45,7 @@ export class AlertsQuery extends ResolverQuery { ], }, }, - ...this.pagination.buildQueryFields('endgame.serial_event_id'), + ...this.pagination.buildQueryFields('endgame.serial_event_id', 'asc'), }; } @@ -47,6 +54,7 @@ export class AlertsQuery extends ResolverQuery { query: { bool: { filter: [ + ...this.kqlQuery, { terms: { 'process.entity_id': entityIDs }, }, @@ -56,7 +64,7 @@ export class AlertsQuery extends ResolverQuery { ], }, }, - ...this.pagination.buildQueryFields('event.id'), + ...this.pagination.buildQueryFields('event.id', 'asc'), }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts index abc86826e77dd7..0969a3c360e4ad 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { SearchResponse } from 'elasticsearch'; +import { esKuery } from '../../../../../../../../src/plugins/data/server'; import { ResolverEvent } from '../../../../../common/endpoint/types'; import { ResolverQuery } from './base'; import { PaginationBuilder } from '../utils/pagination'; @@ -13,12 +14,18 @@ import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/com * Builds a query for retrieving related events for a node. */ export class EventsQuery extends ResolverQuery { + private readonly kqlQuery: JsonObject[] = []; + constructor( private readonly pagination: PaginationBuilder, indexPattern: string | string[], - endpointID?: string + endpointID?: string, + kql?: string ) { super(indexPattern, endpointID); + if (kql) { + this.kqlQuery.push(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kql))); + } } protected legacyQuery(endpointID: string, uniquePIDs: string[]): JsonObject { @@ -26,6 +33,7 @@ export class EventsQuery extends ResolverQuery { query: { bool: { filter: [ + ...this.kqlQuery, { terms: { 'endgame.unique_pid': uniquePIDs }, }, @@ -45,7 +53,7 @@ export class EventsQuery extends ResolverQuery { ], }, }, - ...this.pagination.buildQueryFields('endgame.serial_event_id'), + ...this.pagination.buildQueryFields('endgame.serial_event_id', 'desc'), }; } @@ -54,6 +62,7 @@ export class EventsQuery extends ResolverQuery { query: { bool: { filter: [ + ...this.kqlQuery, { terms: { 'process.entity_id': entityIDs }, }, @@ -70,7 +79,7 @@ export class EventsQuery extends ResolverQuery { ], }, }, - ...this.pagination.buildQueryFields('event.id'), + ...this.pagination.buildQueryFields('event.id', 'desc'), }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts index ae17cf4c3a562f..efffbc10473d42 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts @@ -13,23 +13,35 @@ import { PaginationBuilder } from './pagination'; import { QueryInfo } from '../queries/multi_searcher'; import { SingleQueryHandler } from './fetch'; +/** + * Parameters for RelatedAlertsQueryHandler + */ +export interface RelatedAlertsParams { + limit: number; + entityID: string; + indexPattern: string; + after?: string; + legacyEndpointID?: string; + filter?: string; +} + /** * Requests related alerts for the given node. */ export class RelatedAlertsQueryHandler implements SingleQueryHandler { private relatedAlerts: ResolverRelatedAlerts | undefined; private readonly query: AlertsQuery; - constructor( - private readonly limit: number, - private readonly entityID: string, - after: string | undefined, - indexPattern: string, - legacyEndpointID: string | undefined - ) { + private readonly limit: number; + private readonly entityID: string; + + constructor(options: RelatedAlertsParams) { + this.limit = options.limit; + this.entityID = options.entityID; this.query = new AlertsQuery( - PaginationBuilder.createBuilder(limit, after), - indexPattern, - legacyEndpointID + PaginationBuilder.createBuilder(this.limit, options.after), + options.indexPattern, + options.legacyEndpointID, + options.filter ); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts index 849dbc25fe4db9..8792f917fb4d6a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts @@ -13,23 +13,36 @@ import { PaginationBuilder } from './pagination'; import { QueryInfo } from '../queries/multi_searcher'; import { SingleQueryHandler } from './fetch'; +/** + * Parameters for the RelatedEventsQueryHandler + */ +export interface RelatedEventsParams { + limit: number; + entityID: string; + indexPattern: string; + after?: string; + legacyEndpointID?: string; + filter?: string; +} + /** * This retrieves the related events for the origin node of a resolver tree. */ export class RelatedEventsQueryHandler implements SingleQueryHandler { private relatedEvents: ResolverRelatedEvents | undefined; private readonly query: EventsQuery; - constructor( - private readonly limit: number, - private readonly entityID: string, - after: string | undefined, - indexPattern: string, - legacyEndpointID: string | undefined - ) { + private readonly limit: number; + private readonly entityID: string; + + constructor(options: RelatedEventsParams) { + this.limit = options.limit; + this.entityID = options.entityID; + this.query = new EventsQuery( - PaginationBuilder.createBuilder(limit, after), - indexPattern, - legacyEndpointID + PaginationBuilder.createBuilder(this.limit, options.after), + options.indexPattern, + options.legacyEndpointID, + options.filter ); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts index 43c10d552ab4e1..1b88f965909ebb 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts @@ -110,21 +110,21 @@ export class Fetcher { this.endpointID ); - const eventsHandler = new RelatedEventsQueryHandler( - options.events, - this.id, - options.afterEvent, - this.eventsIndexPattern, - this.endpointID - ); + const eventsHandler = new RelatedEventsQueryHandler({ + limit: options.events, + entityID: this.id, + after: options.afterEvent, + indexPattern: this.eventsIndexPattern, + legacyEndpointID: this.endpointID, + }); - const alertsHandler = new RelatedAlertsQueryHandler( - options.alerts, - this.id, - options.afterAlert, - this.alertsIndexPattern, - this.endpointID - ); + const alertsHandler = new RelatedAlertsQueryHandler({ + limit: options.alerts, + entityID: this.id, + after: options.afterAlert, + indexPattern: this.alertsIndexPattern, + legacyEndpointID: this.endpointID, + }); // we need to get the start events first because the API request defines how many nodes to return and we don't want // to count or limit ourselves based on the other lifecycle events (end, etc) @@ -228,17 +228,24 @@ export class Fetcher { /** * Retrieves the related events for the origin node. * - * @param limit the upper bound number of related events to return + * @param limit the upper bound number of related events to return. The limit is applied after the cursor is used to + * skip the previous results. * @param after a cursor to use as the starting point for retrieving related events + * @param filter a kql query for filtering the results */ - public async events(limit: number, after?: string): Promise { - const eventsHandler = new RelatedEventsQueryHandler( + public async events( + limit: number, + after?: string, + filter?: string + ): Promise { + const eventsHandler = new RelatedEventsQueryHandler({ limit, - this.id, + entityID: this.id, after, - this.eventsIndexPattern, - this.endpointID - ); + indexPattern: this.eventsIndexPattern, + legacyEndpointID: this.endpointID, + filter, + }); return eventsHandler.search(this.client); } @@ -246,17 +253,24 @@ export class Fetcher { /** * Retrieves the alerts for the origin node. * - * @param limit the upper bound number of alerts to return + * @param limit the upper bound number of alerts to return. The limit is applied after the cursor is used to + * skip the previous results. * @param after a cursor to use as the starting point for retrieving alerts + * @param filter a kql query string for filtering the results */ - public async alerts(limit: number, after?: string): Promise { - const alertsHandler = new RelatedAlertsQueryHandler( + public async alerts( + limit: number, + after?: string, + filter?: string + ): Promise { + const alertsHandler = new RelatedAlertsQueryHandler({ limit, - this.id, + entityID: this.id, after, - this.alertsIndexPattern, - this.endpointID - ); + indexPattern: this.alertsIndexPattern, + legacyEndpointID: this.endpointID, + filter, + }); return alertsHandler.search(this.client); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts index 4daa45aec2a740..8e567bfb59c650 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts @@ -42,5 +42,19 @@ describe('Pagination', () => { const fields = builder.buildQueryFields(''); expect(fields).not.toHaveProperty('search_after'); }); + + it('creates the sort field in ascending order', () => { + const builder = PaginationBuilder.createBuilder(100); + expect(builder.buildQueryFields('a').sort).toContainEqual({ '@timestamp': 'asc' }); + expect(builder.buildQueryFields('', 'asc').sort).toContainEqual({ '@timestamp': 'asc' }); + }); + + it('creates the sort field in descending order', () => { + const builder = PaginationBuilder.createBuilder(100); + expect(builder.buildQueryFields('a', 'desc').sort).toStrictEqual([ + { '@timestamp': 'desc' }, + { a: 'asc' }, + ]); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts index f6ff4451b5d8e3..4a6c65e55a6b66 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts @@ -16,6 +16,11 @@ interface PaginationCursor { eventID: string; } +/** + * The sort direction for the timestamp field + */ +export type TimeSortDirection = 'asc' | 'desc'; + /** * Defines the sorting fields for queries that leverage pagination */ @@ -158,10 +163,14 @@ export class PaginationBuilder { * Helper for creates an object for adding the pagination fields to a query * * @param tiebreaker a unique field to use as the tiebreaker for the search_after + * @param timeSort is the timestamp sort direction * @returns an object containing the pagination information */ - buildQueryFieldsAsInterface(tiebreaker: string): PaginationFields { - const sort: SortFields = [{ '@timestamp': 'asc' }, { [tiebreaker]: 'asc' }]; + buildQueryFieldsAsInterface( + tiebreaker: string, + timeSort: TimeSortDirection = 'asc' + ): PaginationFields { + const sort: SortFields = [{ '@timestamp': timeSort }, { [tiebreaker]: 'asc' }]; let searchAfter: SearchAfterFields | undefined; if (this.timestamp && this.eventID) { searchAfter = [this.timestamp, this.eventID]; @@ -174,11 +183,12 @@ export class PaginationBuilder { * Creates an object for adding the pagination fields to a query * * @param tiebreaker a unique field to use as the tiebreaker for the search_after + * @param timeSort is the timestamp sort direction * @returns an object containing the pagination information */ - buildQueryFields(tiebreaker: string): JsonObject { + buildQueryFields(tiebreaker: string, timeSort: TimeSortDirection = 'asc'): JsonObject { const fields: JsonObject = {}; - const pagination = this.buildQueryFieldsAsInterface(tiebreaker); + const pagination = this.buildQueryFieldsAsInterface(tiebreaker, timeSort); fields.sort = pagination.sort; fields.size = pagination.size; if (pagination.searchAfter) { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/alerts.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/alerts.ts new file mode 100644 index 00000000000000..82d844aae80161 --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/alerts.ts @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { eventId } from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { ResolverRelatedAlerts } from '../../../../plugins/security_solution/common/endpoint/types'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { + Tree, + RelatedEventCategory, +} from '../../../../plugins/security_solution/common/endpoint/generate_data'; +import { Options, GeneratedTrees } from '../../services/resolver'; +import { compareArrays } from './common'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const resolver = getService('resolverGenerator'); + + const relatedEventsToGen = [ + { category: RelatedEventCategory.Driver, count: 2 }, + { category: RelatedEventCategory.File, count: 1 }, + { category: RelatedEventCategory.Registry, count: 1 }, + ]; + const relatedAlerts = 4; + let resolverTrees: GeneratedTrees; + let tree: Tree; + const treeOptions: Options = { + ancestors: 5, + relatedEvents: relatedEventsToGen, + relatedAlerts, + children: 3, + generations: 2, + percentTerminated: 100, + percentWithRelated: 100, + numTrees: 1, + alwaysGenMaxChildrenPerNode: true, + ancestryArraySize: 2, + }; + + describe('related alerts route', () => { + before(async () => { + resolverTrees = await resolver.createTrees(treeOptions); + // we only requested a single alert so there's only 1 tree + tree = resolverTrees.trees[0]; + }); + after(async () => { + await resolver.deleteData(resolverTrees); + }); + + it('should not find any alerts', async () => { + const { body }: { body: ResolverRelatedAlerts } = await supertest + .post(`/api/endpoint/resolver/5555/alerts`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.nextAlert).to.eql(null); + expect(body.alerts).to.be.empty(); + }); + + it('should return details for the root node', async () => { + const { body }: { body: ResolverRelatedAlerts } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/alerts`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.alerts.length).to.eql(4); + compareArrays(tree.origin.relatedAlerts, body.alerts, true); + expect(body.nextAlert).to.eql(null); + }); + + it('should allow alerts to be filtered', async () => { + const filter = `not event.id:"${tree.origin.relatedAlerts[0].event.id}"`; + const { body }: { body: ResolverRelatedAlerts } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/alerts`) + .set('kbn-xsrf', 'xxx') + .send({ + filter, + }) + .expect(200); + expect(body.alerts.length).to.eql(3); + compareArrays(tree.origin.relatedAlerts, body.alerts); + expect(body.nextAlert).to.eql(null); + + // should not find the alert that we excluded in the filter + expect( + body.alerts.find((bodyAlert) => { + return eventId(bodyAlert) === tree.origin.relatedAlerts[0].event.id; + }) + ).to.not.be.ok(); + }); + + it('should return paginated results for the root node', async () => { + let { body }: { body: ResolverRelatedAlerts } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/alerts?alerts=2`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.alerts.length).to.eql(2); + compareArrays(tree.origin.relatedAlerts, body.alerts); + expect(body.nextAlert).not.to.eql(null); + + ({ body } = await supertest + .post( + `/api/endpoint/resolver/${tree.origin.id}/alerts?alerts=2&afterAlert=${body.nextAlert}` + ) + .set('kbn-xsrf', 'xxx') + .expect(200)); + expect(body.alerts.length).to.eql(2); + compareArrays(tree.origin.relatedAlerts, body.alerts); + expect(body.nextAlert).to.not.eql(null); + + ({ body } = await supertest + .post( + `/api/endpoint/resolver/${tree.origin.id}/alerts?alerts=2&afterAlert=${body.nextAlert}` + ) + .set('kbn-xsrf', 'xxx') + .expect(200)); + expect(body.alerts).to.be.empty(); + expect(body.nextAlert).to.eql(null); + }); + + it('should return the first page of information when the cursor is invalid', async () => { + const { body }: { body: ResolverRelatedAlerts } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/alerts?afterAlert=blah`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.alerts.length).to.eql(4); + compareArrays(tree.origin.relatedAlerts, body.alerts, true); + expect(body.nextAlert).to.eql(null); + }); + + it('should sort the alerts in ascending order', async () => { + const { body }: { body: ResolverRelatedAlerts } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/alerts`) + .set('kbn-xsrf', 'xxx') + .expect(200); + const sortedAsc = [...tree.origin.relatedAlerts].sort((event1, event2) => { + // this sorts the events by timestamp in ascending order + const diff = event1['@timestamp'] - event2['@timestamp']; + // if the timestamps are the same, fallback to the event.id sorted in + // ascending order + if (diff === 0) { + if (event1.event.id < event2.event.id) { + return -1; + } + if (event1.event.id > event2.event.id) { + return 1; + } + return 0; + } + return diff; + }); + + expect(body.alerts.length).to.eql(4); + for (let i = 0; i < body.alerts.length; i++) { + expect(eventId(body.alerts[i])).to.equal(sortedAsc[i].event.id); + } + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/common.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/common.ts new file mode 100644 index 00000000000000..92d14fb94a2d89 --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/common.ts @@ -0,0 +1,222 @@ +/* + * 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 _ from 'lodash'; +import expect from '@kbn/expect'; +import { + ResolverChildNode, + ResolverLifecycleNode, + ResolverEvent, + ResolverNodeStats, +} from '../../../../plugins/security_solution/common/endpoint/types'; +import { + parentEntityId, + eventId, +} from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { + Event, + Tree, + TreeNode, + RelatedEventInfo, + categoryMapping, +} from '../../../../plugins/security_solution/common/endpoint/generate_data'; + +/** + * Check that the given lifecycle is in the resolver tree's corresponding map + * + * @param node a lifecycle node containing the start and end events for a node + * @param nodeMap a map of entity_ids to nodes to look for the passed in `node` + */ +const expectLifecycleNodeInMap = (node: ResolverLifecycleNode, nodeMap: Map) => { + const genNode = nodeMap.get(node.entityID); + expect(genNode).to.be.ok(); + compareArrays(genNode!.lifecycle, node.lifecycle, true); +}; + +/** + * Verify that all the ancestor nodes are valid and optionally have parents. + * + * @param ancestors an array of ancestors + * @param tree the generated resolver tree as the source of truth + * @param verifyLastParent a boolean indicating whether to check the last ancestor. If the ancestors array intentionally + * does not contain all the ancestors, the last one will not have the parent + */ +export const verifyAncestry = ( + ancestors: ResolverLifecycleNode[], + tree: Tree, + verifyLastParent: boolean +) => { + // group the ancestors by their entity_id mapped to a lifecycle node + const groupedAncestors = _.groupBy(ancestors, (ancestor) => ancestor.entityID); + // group by parent entity_id + const groupedAncestorsParent = _.groupBy(ancestors, (ancestor) => + parentEntityId(ancestor.lifecycle[0]) + ); + // make sure there aren't any nodes with the same entity_id + expect(Object.keys(groupedAncestors).length).to.eql(ancestors.length); + // make sure there aren't any nodes with the same parent entity_id + expect(Object.keys(groupedAncestorsParent).length).to.eql(ancestors.length); + + // make sure each of the ancestors' lifecycle events are in the generated tree + for (const node of ancestors) { + expectLifecycleNodeInMap(node, tree.ancestry); + } + + // start at the origin which is always the first element of the array and make sure we have a connection + // using parent id between each of the nodes + let foundParents = 0; + let node = ancestors[0]; + for (let i = 0; i < ancestors.length; i++) { + const parentID = parentEntityId(node.lifecycle[0]); + if (parentID !== undefined) { + const nextNode = groupedAncestors[parentID]; + if (!nextNode) { + break; + } + // the grouped nodes should only have a single entry since each entity is unique + node = nextNode[0]; + } + foundParents++; + } + + if (verifyLastParent) { + expect(foundParents).to.eql(ancestors.length); + } else { + // if we only retrieved a portion of all the ancestors then the most distant grandparent's parent will not necessarily + // be in the results + expect(foundParents).to.eql(ancestors.length - 1); + } +}; + +/** + * Retrieves the most distant ancestor in the given array. + * + * @param ancestors an array of ancestor nodes + */ +export const retrieveDistantAncestor = (ancestors: ResolverLifecycleNode[]) => { + // group the ancestors by their entity_id mapped to a lifecycle node + const groupedAncestors = _.groupBy(ancestors, (ancestor) => ancestor.entityID); + let node = ancestors[0]; + for (let i = 0; i < ancestors.length; i++) { + const parentID = parentEntityId(node.lifecycle[0]); + if (parentID !== undefined) { + const nextNode = groupedAncestors[parentID]; + if (nextNode) { + node = nextNode[0]; + } else { + return node; + } + } + } + return node; +}; + +/** + * Verify that the children nodes are correct + * + * @param children the children nodes + * @param tree the generated resolver tree as the source of truth + * @param numberOfParents an optional number to compare that are a certain number of parents in the children array + * @param childrenPerParent an optional number to compare that there are a certain number of children for each parent + */ +export const verifyChildren = ( + children: ResolverChildNode[], + tree: Tree, + numberOfParents?: number, + childrenPerParent?: number +) => { + // group the children by their entity_id mapped to a child node + const groupedChildren = _.groupBy(children, (child) => child.entityID); + // make sure each child is unique + expect(Object.keys(groupedChildren).length).to.eql(children.length); + if (numberOfParents !== undefined) { + const groupParent = _.groupBy(children, (child) => parentEntityId(child.lifecycle[0])); + expect(Object.keys(groupParent).length).to.eql(numberOfParents); + if (childrenPerParent !== undefined) { + Object.values(groupParent).forEach((childNodes) => + expect(childNodes.length).to.be(childrenPerParent) + ); + } + } + + children.forEach((child) => { + expectLifecycleNodeInMap(child, tree.children); + }); +}; + +/** + * Compare an array of events returned from an API with an array of events generated + * + * @param expected an array to use as the source of truth + * @param toTest the array to test against the source of truth + * @param lengthCheck an optional flag to check that the arrays are the same length + */ +export const compareArrays = ( + expected: Event[], + toTest: ResolverEvent[], + lengthCheck: boolean = false +) => { + if (lengthCheck) { + expect(expected.length).to.eql(toTest.length); + } + + toTest.forEach((toTestEvent) => { + expect( + expected.find((arrEvent) => { + // we're only checking that the event ids are the same here. The reason we can't check the entire document + // is because ingest pipelines are used to add fields to the document when it is received by elasticsearch, + // therefore it will not be the same as the document created by the generator + return eventId(toTestEvent) === eventId(arrEvent); + }) + ).to.be.ok(); + }); +}; + +/** + * Verifies that the stats received from ES for a node reflect the categories of events that the generator created. + * + * @param relatedEvents the related events received for a particular node + * @param categories the related event info used when generating the resolver tree + */ +export const verifyStats = ( + stats: ResolverNodeStats | undefined, + categories: RelatedEventInfo[], + relatedAlerts: number +) => { + expect(stats).to.not.be(undefined); + let totalExpEvents = 0; + for (const cat of categories) { + const ecsCategories = categoryMapping[cat.category]; + if (Array.isArray(ecsCategories)) { + // if there are multiple ecs categories used to define a related event, the count for all of them should be the same + // and they should equal what is defined in the categories used to generate the related events + for (const ecsCat of ecsCategories) { + expect(stats?.events.byCategory[ecsCat]).to.be(cat.count); + } + } else { + expect(stats?.events.byCategory[ecsCategories]).to.be(cat.count); + } + + totalExpEvents += cat.count; + } + expect(stats?.events.total).to.be(totalExpEvents); + expect(stats?.totalAlerts); +}; + +/** + * A helper function for verifying the stats information an array of nodes. + * + * @param nodes an array of lifecycle nodes that should have a stats field defined + * @param categories the related event info used when generating the resolver tree + */ +export const verifyLifecycleStats = ( + nodes: ResolverLifecycleNode[], + categories: RelatedEventInfo[], + relatedAlerts: number +) => { + for (const node of nodes) { + verifyStats(node.stats, categories, relatedAlerts); + } +}; diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts new file mode 100644 index 00000000000000..c0e4e466c7b62c --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts @@ -0,0 +1,213 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { eventId } from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { ResolverRelatedEvents } from '../../../../plugins/security_solution/common/endpoint/types'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { + Tree, + RelatedEventCategory, +} from '../../../../plugins/security_solution/common/endpoint/generate_data'; +import { Options, GeneratedTrees } from '../../services/resolver'; +import { compareArrays } from './common'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const resolver = getService('resolverGenerator'); + const esArchiver = getService('esArchiver'); + + const relatedEventsToGen = [ + { category: RelatedEventCategory.Driver, count: 2 }, + { category: RelatedEventCategory.File, count: 1 }, + { category: RelatedEventCategory.Registry, count: 1 }, + ]; + const relatedAlerts = 4; + let resolverTrees: GeneratedTrees; + let tree: Tree; + const treeOptions: Options = { + ancestors: 5, + relatedEvents: relatedEventsToGen, + relatedEventsOrdered: true, + relatedAlerts, + children: 3, + generations: 2, + percentTerminated: 100, + percentWithRelated: 100, + numTrees: 1, + alwaysGenMaxChildrenPerNode: true, + ancestryArraySize: 2, + }; + + describe('related events route', () => { + before(async () => { + await esArchiver.load('endpoint/resolver/api_feature'); + resolverTrees = await resolver.createTrees(treeOptions); + // we only requested a single alert so there's only 1 tree + tree = resolverTrees.trees[0]; + }); + after(async () => { + await resolver.deleteData(resolverTrees); + await esArchiver.unload('endpoint/resolver/api_feature'); + }); + + describe('legacy events', () => { + const endpointID = '5a0c957f-b8e7-4538-965e-57e8bb86ad3a'; + const entityID = '94042'; + const cursor = 'eyJ0aW1lc3RhbXAiOjE1ODE0NTYyNTUwMDAsImV2ZW50SUQiOiI5NDA0MyJ9'; + + it('should return details for the root node', async () => { + const { body }: { body: ResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(1); + expect(body.entityID).to.eql(entityID); + expect(body.nextEvent).to.eql(null); + }); + + it('returns no values when there is no more data', async () => { + const { body }: { body: ResolverRelatedEvents } = await supertest + // after is set to the document id of the last event so there shouldn't be any more after it + .post( + `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=${cursor}` + ) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events).be.empty(); + expect(body.entityID).to.eql(entityID); + expect(body.nextEvent).to.eql(null); + }); + + it('should return the first page of information when the cursor is invalid', async () => { + const { body }: { body: ResolverRelatedEvents } = await supertest + .post( + `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=blah` + ) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.entityID).to.eql(entityID); + expect(body.nextEvent).to.eql(null); + }); + + it('should return no results for an invalid endpoint ID', async () => { + const { body }: { body: ResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=foo`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.nextEvent).to.eql(null); + expect(body.entityID).to.eql(entityID); + expect(body.events).to.be.empty(); + }); + + it('should error on invalid pagination values', async () => { + await supertest + .post(`/api/endpoint/resolver/${entityID}/events?events=0`) + .set('kbn-xsrf', 'xxx') + .expect(400); + await supertest + .post(`/api/endpoint/resolver/${entityID}/events?events=20000`) + .set('kbn-xsrf', 'xxx') + .expect(400); + await supertest + .post(`/api/endpoint/resolver/${entityID}/events?events=-1`) + .set('kbn-xsrf', 'xxx') + .expect(400); + }); + }); + + describe('endpoint events', () => { + it('should not find any events', async () => { + const { body }: { body: ResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/5555/events`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.nextEvent).to.eql(null); + expect(body.events).to.be.empty(); + }); + + it('should return details for the root node', async () => { + const { body }: { body: ResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(4); + compareArrays(tree.origin.relatedEvents, body.events, true); + expect(body.nextEvent).to.eql(null); + }); + + it('should allow for the events to be filtered', async () => { + const filter = `event.category:"${RelatedEventCategory.Driver}"`; + const { body }: { body: ResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events`) + .set('kbn-xsrf', 'xxx') + .send({ + filter, + }) + .expect(200); + expect(body.events.length).to.eql(2); + compareArrays(tree.origin.relatedEvents, body.events); + expect(body.nextEvent).to.eql(null); + for (const event of body.events) { + expect(event.event?.category).to.be(RelatedEventCategory.Driver); + } + }); + + it('should return paginated results for the root node', async () => { + let { body }: { body: ResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events?events=2`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(2); + compareArrays(tree.origin.relatedEvents, body.events); + expect(body.nextEvent).not.to.eql(null); + + ({ body } = await supertest + .post( + `/api/endpoint/resolver/${tree.origin.id}/events?events=2&afterEvent=${body.nextEvent}` + ) + .set('kbn-xsrf', 'xxx') + .expect(200)); + expect(body.events.length).to.eql(2); + compareArrays(tree.origin.relatedEvents, body.events); + expect(body.nextEvent).to.not.eql(null); + + ({ body } = await supertest + .post( + `/api/endpoint/resolver/${tree.origin.id}/events?events=2&afterEvent=${body.nextEvent}` + ) + .set('kbn-xsrf', 'xxx') + .expect(200)); + expect(body.events).to.be.empty(); + expect(body.nextEvent).to.eql(null); + }); + + it('should return the first page of information when the cursor is invalid', async () => { + const { body }: { body: ResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events?afterEvent=blah`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(4); + compareArrays(tree.origin.relatedEvents, body.events, true); + expect(body.nextEvent).to.eql(null); + }); + + it('should sort the events in descending order', async () => { + const { body }: { body: ResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(4); + // these events are created in the order they are defined in the array so the newest one is + // the last element in the array so let's reverse it + const relatedEvents = tree.origin.relatedEvents.reverse(); + for (let i = 0; i < body.events.length; i++) { + expect(body.events[i].event?.category).to.equal(relatedEvents[i].event.category); + expect(eventId(body.events[i])).to.equal(relatedEvents[i].event.id); + } + }); + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts index dc9a1fab9ec020..fc603af3619a4d 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts @@ -12,5 +12,7 @@ export default function (providerContext: FtrProviderContext) { loadTestFile(require.resolve('./entity_id')); loadTestFile(require.resolve('./children')); loadTestFile(require.resolve('./tree')); + loadTestFile(require.resolve('./alerts')); + loadTestFile(require.resolve('./events')); }); } diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts index f4836379ca273f..957d559087f5e1 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts @@ -3,232 +3,28 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import expect from '@kbn/expect'; import { - ResolverChildNode, - ResolverLifecycleNode, ResolverAncestry, - ResolverEvent, - ResolverRelatedEvents, ResolverChildren, ResolverTree, LegacyEndpointEvent, - ResolverNodeStats, - ResolverRelatedAlerts, } from '../../../../plugins/security_solution/common/endpoint/types'; -import { - parentEntityId, - eventId, -} from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { parentEntityId } from '../../../../plugins/security_solution/common/endpoint/models/event'; import { FtrProviderContext } from '../../ftr_provider_context'; import { - Event, Tree, - TreeNode, RelatedEventCategory, - RelatedEventInfo, - categoryMapping, } from '../../../../plugins/security_solution/common/endpoint/generate_data'; import { Options, GeneratedTrees } from '../../services/resolver'; - -/** - * Check that the given lifecycle is in the resolver tree's corresponding map - * - * @param node a lifecycle node containing the start and end events for a node - * @param nodeMap a map of entity_ids to nodes to look for the passed in `node` - */ -const expectLifecycleNodeInMap = (node: ResolverLifecycleNode, nodeMap: Map) => { - const genNode = nodeMap.get(node.entityID); - expect(genNode).to.be.ok(); - compareArrays(genNode!.lifecycle, node.lifecycle, true); -}; - -/** - * Verify that all the ancestor nodes are valid and optionally have parents. - * - * @param ancestors an array of ancestors - * @param tree the generated resolver tree as the source of truth - * @param verifyLastParent a boolean indicating whether to check the last ancestor. If the ancestors array intentionally - * does not contain all the ancestors, the last one will not have the parent - */ -const verifyAncestry = ( - ancestors: ResolverLifecycleNode[], - tree: Tree, - verifyLastParent: boolean -) => { - // group the ancestors by their entity_id mapped to a lifecycle node - const groupedAncestors = _.groupBy(ancestors, (ancestor) => ancestor.entityID); - // group by parent entity_id - const groupedAncestorsParent = _.groupBy(ancestors, (ancestor) => - parentEntityId(ancestor.lifecycle[0]) - ); - // make sure there aren't any nodes with the same entity_id - expect(Object.keys(groupedAncestors).length).to.eql(ancestors.length); - // make sure there aren't any nodes with the same parent entity_id - expect(Object.keys(groupedAncestorsParent).length).to.eql(ancestors.length); - - // make sure each of the ancestors' lifecycle events are in the generated tree - for (const node of ancestors) { - expectLifecycleNodeInMap(node, tree.ancestry); - } - - // start at the origin which is always the first element of the array and make sure we have a connection - // using parent id between each of the nodes - let foundParents = 0; - let node = ancestors[0]; - for (let i = 0; i < ancestors.length; i++) { - const parentID = parentEntityId(node.lifecycle[0]); - if (parentID !== undefined) { - const nextNode = groupedAncestors[parentID]; - if (!nextNode) { - break; - } - // the grouped nodes should only have a single entry since each entity is unique - node = nextNode[0]; - } - foundParents++; - } - - if (verifyLastParent) { - expect(foundParents).to.eql(ancestors.length); - } else { - // if we only retrieved a portion of all the ancestors then the most distant grandparent's parent will not necessarily - // be in the results - expect(foundParents).to.eql(ancestors.length - 1); - } -}; - -/** - * Retrieves the most distant ancestor in the given array. - * - * @param ancestors an array of ancestor nodes - */ -const retrieveDistantAncestor = (ancestors: ResolverLifecycleNode[]) => { - // group the ancestors by their entity_id mapped to a lifecycle node - const groupedAncestors = _.groupBy(ancestors, (ancestor) => ancestor.entityID); - let node = ancestors[0]; - for (let i = 0; i < ancestors.length; i++) { - const parentID = parentEntityId(node.lifecycle[0]); - if (parentID !== undefined) { - const nextNode = groupedAncestors[parentID]; - if (nextNode) { - node = nextNode[0]; - } else { - return node; - } - } - } - return node; -}; - -/** - * Verify that the children nodes are correct - * - * @param children the children nodes - * @param tree the generated resolver tree as the source of truth - * @param numberOfParents an optional number to compare that are a certain number of parents in the children array - * @param childrenPerParent an optional number to compare that there are a certain number of children for each parent - */ -const verifyChildren = ( - children: ResolverChildNode[], - tree: Tree, - numberOfParents?: number, - childrenPerParent?: number -) => { - // group the children by their entity_id mapped to a child node - const groupedChildren = _.groupBy(children, (child) => child.entityID); - // make sure each child is unique - expect(Object.keys(groupedChildren).length).to.eql(children.length); - if (numberOfParents !== undefined) { - const groupParent = _.groupBy(children, (child) => parentEntityId(child.lifecycle[0])); - expect(Object.keys(groupParent).length).to.eql(numberOfParents); - if (childrenPerParent !== undefined) { - Object.values(groupParent).forEach((childNodes) => - expect(childNodes.length).to.be(childrenPerParent) - ); - } - } - - children.forEach((child) => { - expectLifecycleNodeInMap(child, tree.children); - }); -}; - -/** - * Compare an array of events returned from an API with an array of events generated - * - * @param expected an array to use as the source of truth - * @param toTest the array to test against the source of truth - * @param lengthCheck an optional flag to check that the arrays are the same length - */ -const compareArrays = ( - expected: Event[], - toTest: ResolverEvent[], - lengthCheck: boolean = false -) => { - if (lengthCheck) { - expect(expected.length).to.eql(toTest.length); - } - - toTest.forEach((toTestEvent) => { - expect( - expected.find((arrEvent) => { - // we're only checking that the event ids are the same here. The reason we can't check the entire document - // is because ingest pipelines are used to add fields to the document when it is received by elasticsearch, - // therefore it will not be the same as the document created by the generator - return eventId(toTestEvent) === eventId(arrEvent); - }) - ).to.be.ok(); - }); -}; - -/** - * Verifies that the stats received from ES for a node reflect the categories of events that the generator created. - * - * @param relatedEvents the related events received for a particular node - * @param categories the related event info used when generating the resolver tree - */ -const verifyStats = ( - stats: ResolverNodeStats | undefined, - categories: RelatedEventInfo[], - relatedAlerts: number -) => { - expect(stats).to.not.be(undefined); - let totalExpEvents = 0; - for (const cat of categories) { - const ecsCategories = categoryMapping[cat.category]; - if (Array.isArray(ecsCategories)) { - // if there are multiple ecs categories used to define a related event, the count for all of them should be the same - // and they should equal what is defined in the categories used to generate the related events - for (const ecsCat of ecsCategories) { - expect(stats?.events.byCategory[ecsCat]).to.be(cat.count); - } - } else { - expect(stats?.events.byCategory[ecsCategories]).to.be(cat.count); - } - - totalExpEvents += cat.count; - } - expect(stats?.events.total).to.be(totalExpEvents); - expect(stats?.totalAlerts); -}; - -/** - * A helper function for verifying the stats information an array of nodes. - * - * @param nodes an array of lifecycle nodes that should have a stats field defined - * @param categories the related event info used when generating the resolver tree - */ -const verifyLifecycleStats = ( - nodes: ResolverLifecycleNode[], - categories: RelatedEventInfo[], - relatedAlerts: number -) => { - for (const node of nodes) { - verifyStats(node.stats, categories, relatedAlerts); - } -}; +import { + compareArrays, + verifyAncestry, + retrieveDistantAncestor, + verifyChildren, + verifyLifecycleStats, + verifyStats, +} from './common'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -269,170 +65,6 @@ export default function ({ getService }: FtrProviderContext) { await esArchiver.unload('endpoint/resolver/api_feature'); }); - describe('related alerts route', () => { - describe('endpoint events', () => { - it('should not find any alerts', async () => { - const { body }: { body: ResolverRelatedAlerts } = await supertest - .get(`/api/endpoint/resolver/5555/alerts`) - .expect(200); - expect(body.nextAlert).to.eql(null); - expect(body.alerts).to.be.empty(); - }); - - it('should return details for the root node', async () => { - const { body }: { body: ResolverRelatedAlerts } = await supertest - .get(`/api/endpoint/resolver/${tree.origin.id}/alerts`) - .expect(200); - expect(body.alerts.length).to.eql(4); - compareArrays(tree.origin.relatedAlerts, body.alerts, true); - expect(body.nextAlert).to.eql(null); - }); - - it('should return paginated results for the root node', async () => { - let { body }: { body: ResolverRelatedAlerts } = await supertest - .get(`/api/endpoint/resolver/${tree.origin.id}/alerts?alerts=2`) - .expect(200); - expect(body.alerts.length).to.eql(2); - compareArrays(tree.origin.relatedAlerts, body.alerts); - expect(body.nextAlert).not.to.eql(null); - - ({ body } = await supertest - .get( - `/api/endpoint/resolver/${tree.origin.id}/alerts?alerts=2&afterAlert=${body.nextAlert}` - ) - .expect(200)); - expect(body.alerts.length).to.eql(2); - compareArrays(tree.origin.relatedAlerts, body.alerts); - expect(body.nextAlert).to.not.eql(null); - - ({ body } = await supertest - .get( - `/api/endpoint/resolver/${tree.origin.id}/alerts?alerts=2&afterAlert=${body.nextAlert}` - ) - .expect(200)); - expect(body.alerts).to.be.empty(); - expect(body.nextAlert).to.eql(null); - }); - - it('should return the first page of information when the cursor is invalid', async () => { - const { body }: { body: ResolverRelatedAlerts } = await supertest - .get(`/api/endpoint/resolver/${tree.origin.id}/alerts?afterAlert=blah`) - .expect(200); - expect(body.alerts.length).to.eql(4); - compareArrays(tree.origin.relatedAlerts, body.alerts, true); - expect(body.nextAlert).to.eql(null); - }); - }); - }); - - describe('related events route', () => { - describe('legacy events', () => { - const endpointID = '5a0c957f-b8e7-4538-965e-57e8bb86ad3a'; - const entityID = '94042'; - const cursor = 'eyJ0aW1lc3RhbXAiOjE1ODE0NTYyNTUwMDAsImV2ZW50SUQiOiI5NDA0MyJ9'; - - it('should return details for the root node', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest - .get(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}`) - .expect(200); - expect(body.events.length).to.eql(1); - expect(body.entityID).to.eql(entityID); - expect(body.nextEvent).to.eql(null); - }); - - it('returns no values when there is no more data', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest - // after is set to the document id of the last event so there shouldn't be any more after it - .get( - `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=${cursor}` - ) - .expect(200); - expect(body.events).be.empty(); - expect(body.entityID).to.eql(entityID); - expect(body.nextEvent).to.eql(null); - }); - - it('should return the first page of information when the cursor is invalid', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest - .get( - `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=blah` - ) - .expect(200); - expect(body.entityID).to.eql(entityID); - expect(body.nextEvent).to.eql(null); - }); - - it('should return no results for an invalid endpoint ID', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest - .get(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=foo`) - .expect(200); - expect(body.nextEvent).to.eql(null); - expect(body.entityID).to.eql(entityID); - expect(body.events).to.be.empty(); - }); - - it('should error on invalid pagination values', async () => { - await supertest.get(`/api/endpoint/resolver/${entityID}/events?events=0`).expect(400); - await supertest.get(`/api/endpoint/resolver/${entityID}/events?events=20000`).expect(400); - await supertest.get(`/api/endpoint/resolver/${entityID}/events?events=-1`).expect(400); - }); - }); - - describe('endpoint events', () => { - it('should not find any events', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest - .get(`/api/endpoint/resolver/5555/events`) - .expect(200); - expect(body.nextEvent).to.eql(null); - expect(body.events).to.be.empty(); - }); - - it('should return details for the root node', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest - .get(`/api/endpoint/resolver/${tree.origin.id}/events`) - .expect(200); - expect(body.events.length).to.eql(4); - compareArrays(tree.origin.relatedEvents, body.events, true); - expect(body.nextEvent).to.eql(null); - }); - - it('should return paginated results for the root node', async () => { - let { body }: { body: ResolverRelatedEvents } = await supertest - .get(`/api/endpoint/resolver/${tree.origin.id}/events?events=2`) - .expect(200); - expect(body.events.length).to.eql(2); - compareArrays(tree.origin.relatedEvents, body.events); - expect(body.nextEvent).not.to.eql(null); - - ({ body } = await supertest - .get( - `/api/endpoint/resolver/${tree.origin.id}/events?events=2&afterEvent=${body.nextEvent}` - ) - .expect(200)); - expect(body.events.length).to.eql(2); - compareArrays(tree.origin.relatedEvents, body.events); - expect(body.nextEvent).to.not.eql(null); - - ({ body } = await supertest - .get( - `/api/endpoint/resolver/${tree.origin.id}/events?events=2&afterEvent=${body.nextEvent}` - ) - .expect(200)); - expect(body.events).to.be.empty(); - expect(body.nextEvent).to.eql(null); - }); - - it('should return the first page of information when the cursor is invalid', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest - .get(`/api/endpoint/resolver/${tree.origin.id}/events?afterEvent=blah`) - .expect(200); - expect(body.events.length).to.eql(4); - compareArrays(tree.origin.relatedEvents, body.events, true); - expect(body.nextEvent).to.eql(null); - }); - }); - }); - describe('ancestry events route', () => { describe('legacy events', () => { const endpointID = '5a0c957f-b8e7-4538-965e-57e8bb86ad3a'; From 3541edbb5d86dc58824e7abe82a5b326b1b745a9 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Wed, 26 Aug 2020 08:30:47 -0500 Subject: [PATCH 030/216] Minor developer guide doc changes (#75763) --- docs/developer/best-practices/index.asciidoc | 2 +- docs/developer/best-practices/stability.asciidoc | 10 +++++----- .../developer/getting-started/building-kibana.asciidoc | 4 ++-- docs/developer/getting-started/index.asciidoc | 4 ++-- .../getting-started/running-kibana-advanced.asciidoc | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/developer/best-practices/index.asciidoc b/docs/developer/best-practices/index.asciidoc index 63a44b54d454f0..42cee6ef0e58a7 100644 --- a/docs/developer/best-practices/index.asciidoc +++ b/docs/developer/best-practices/index.asciidoc @@ -48,7 +48,7 @@ guidelines] * Write all new code on {kib-repo}blob/{branch}/src/core/README.md[the platform], and following -{kib-repo}blob/{branch}/src/core/CONVENTIONS.md[conventions] +{kib-repo}blob/{branch}/src/core/CONVENTIONS.md[conventions]. * _Always_ use the `SavedObjectClient` for reading and writing Saved Objects. * Add `README`s to all your plugins and services. diff --git a/docs/developer/best-practices/stability.asciidoc b/docs/developer/best-practices/stability.asciidoc index f4b7ae12299093..348412e593d9e8 100644 --- a/docs/developer/best-practices/stability.asciidoc +++ b/docs/developer/best-practices/stability.asciidoc @@ -52,15 +52,15 @@ storeinSessions?) [discrete] === Browser coverage -Refer to the list of browsers and OS {kib} supports +Refer to the list of browsers and OS {kib} supports: https://www.elastic.co/support/matrix Does the feature work efficiently on the list of supported browsers? [discrete] -=== Upgrade Scenarios - Migration scenarios- +=== Upgrade and Migration scenarios -Does the feature affect old -indices, saved objects ? - Has the feature been tested with {kib} -aliases - Read/Write privileges of the indices before and after the +* Does the feature affect old indices or saved objects? +* Has the feature been tested with {kib} aliases? +* Read/Write privileges of the indices before and after the upgrade? diff --git a/docs/developer/getting-started/building-kibana.asciidoc b/docs/developer/getting-started/building-kibana.asciidoc index 72054b1628fc20..04771b34bf69ff 100644 --- a/docs/developer/getting-started/building-kibana.asciidoc +++ b/docs/developer/getting-started/building-kibana.asciidoc @@ -1,7 +1,7 @@ [[building-kibana]] == Building a {kib} distributable -The following commands will build a {kib} production distributable. +The following command will build a {kib} production distributable: [source,bash] ---- @@ -36,4 +36,4 @@ To specify a package to build you can add `rpm` or `deb` as an argument. yarn build --rpm ---- -Distributable packages can be found in `target/` after the build completes. \ No newline at end of file +Distributable packages can be found in `target/` after the build completes. diff --git a/docs/developer/getting-started/index.asciidoc b/docs/developer/getting-started/index.asciidoc index eaa35eece5a2c8..10e603a8da8bbc 100644 --- a/docs/developer/getting-started/index.asciidoc +++ b/docs/developer/getting-started/index.asciidoc @@ -49,7 +49,7 @@ ____ (You can also run `yarn kbn` to see the other available commands. For more info about this tool, see -{kib-repo}tree/{branch}/packages/kbn-pm[{kib-repo}tree/{branch}packages/kbn-pm].) +{kib-repo}tree/{branch}/packages/kbn-pm[{kib-repo}tree/{branch}/packages/kbn-pm].) When switching branches which use different versions of npm packages you may need to run: @@ -137,4 +137,4 @@ include::debugging.asciidoc[leveloffset=+1] include::building-kibana.asciidoc[leveloffset=+1] -include::development-plugin-resources.asciidoc[leveloffset=+1] \ No newline at end of file +include::development-plugin-resources.asciidoc[leveloffset=+1] diff --git a/docs/developer/getting-started/running-kibana-advanced.asciidoc b/docs/developer/getting-started/running-kibana-advanced.asciidoc index c3b7847b0f8bab..44897184f88f25 100644 --- a/docs/developer/getting-started/running-kibana-advanced.asciidoc +++ b/docs/developer/getting-started/running-kibana-advanced.asciidoc @@ -73,8 +73,8 @@ settings]. [discrete] === Potential Optimization Pitfalls -* Webpack is trying to include a file in the bundle that I deleted and -is now complaining about it is missing +* Webpack is trying to include a file in the bundle that was deleted and +is now complaining about it being missing * A module id that used to resolve to a single file now resolves to a directory, but webpack isn’t adapting * (if you discover other scenarios, please send a PR!) @@ -84,4 +84,4 @@ directory, but webpack isn’t adapting {kib} includes self-signed certificates that can be used for development purposes in the browser and for communicating with -{es}: `yarn start --ssl` & `yarn es snapshot --ssl`. \ No newline at end of file +{es}: `yarn start --ssl` & `yarn es snapshot --ssl`. From 4f2d4f8b018950481f1841792a3b7243152ae39b Mon Sep 17 00:00:00 2001 From: Bhavya RM Date: Wed, 26 Aug 2020 09:59:41 -0400 Subject: [PATCH 031/216] adding test user to pew pew maps test + adding a role for connections index pattern (#75920) --- x-pack/test/functional/apps/maps/es_pew_pew_source.js | 6 ++++++ x-pack/test/functional/config.js | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/x-pack/test/functional/apps/maps/es_pew_pew_source.js b/x-pack/test/functional/apps/maps/es_pew_pew_source.js index 382bde510170fd..b0f98f807fd44b 100644 --- a/x-pack/test/functional/apps/maps/es_pew_pew_source.js +++ b/x-pack/test/functional/apps/maps/es_pew_pew_source.js @@ -9,14 +9,20 @@ import expect from '@kbn/expect'; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); const inspector = getService('inspector'); + const security = getService('security'); const VECTOR_SOURCE_ID = '67c1de2c-2fc5-4425-8983-094b589afe61'; describe('point to point source', () => { before(async () => { + await security.testUser.setRoles(['global_maps_all', 'geoconnections_data_reader']); await PageObjects.maps.loadSavedMap('pew pew demo'); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should request source clusters for destination locations', async () => { await inspector.open(); await inspector.openInspectorRequestsView(); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index fdd694e73394e3..003d842cc3d6f2 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -273,6 +273,17 @@ export default async function ({ readConfigFile }) { }, }, + geoconnections_data_reader: { + elasticsearch: { + indices: [ + { + names: ['connections*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + }, + global_devtools_read: { kibana: [ { From 4e1b1b5d9e3ab98e177c5b8c763d08918784aad7 Mon Sep 17 00:00:00 2001 From: Bhavya RM Date: Wed, 26 Aug 2020 10:02:10 -0400 Subject: [PATCH 032/216] adding test user to auto fit to bounds test (#75914) --- x-pack/test/functional/apps/maps/auto_fit_to_bounds.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js index d3d4fe054ec348..0e8775fa611b5a 100644 --- a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js +++ b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js @@ -6,17 +6,23 @@ import expect from '@kbn/expect'; -export default function ({ getPageObjects }) { +export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); + const security = getService('security'); describe('auto fit map to bounds', () => { describe('initial location', () => { before(async () => { + await security.testUser.setRoles(['global_maps_all', 'test_logstash_reader']); await PageObjects.maps.loadSavedMap( 'document example - auto fit to bounds for initial location' ); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should automatically fit to bounds on initial map load', async () => { const hits = await PageObjects.maps.getHits(); expect(hits).to.equal('6'); From b9c820120202dc44296e080550e87c93bd37dd55 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 26 Aug 2020 10:16:17 -0400 Subject: [PATCH 033/216] [Security Solution][Exceptions] - Improve UX for missing exception list associated with rule (#75898) ## Summary **Current behavior:** - **Scenario 1:** User is in the exceptions viewer flow, they select to edit an exception item, but the list the item is associated with has since been deleted (let's say by another user) - a user is able to open modal to edit exception item and on save, an error toaster shows but no information is given to the user to indicate the issue. - **Scenario 2:** User exports rules from space 'X' and imports into space 'Y'. The exception lists associated with their newly imported rules do not exist in space 'Y' - a user goes to add an exception item and gets a modal with an error, unable to add any exceptions. - **Workaround:** current workaround exists only via API - user would need to remove the exception list from their rule via API **New behavior:** - **Scenario 1:** User is still able to oped edit modal, but on save they see an error explaining that the associated exception list does not exist and prompts them to remove the exception list --> now they're able to add exceptions to their rule - **Scenario 2:** User navigates to exceptions after importing their rule, tries to add exception, modal pops up with error informing them that they need to remove association to missing exception list, button prompts them to do so --> now can continue adding exceptions to rule --- .../exceptions/add_exception_modal/index.tsx | 90 +++++++--- .../edit_exception_modal/index.test.tsx | 5 + .../exceptions/edit_exception_modal/index.tsx | 91 +++++++--- .../exceptions/error_callout.test.tsx | 160 +++++++++++++++++ .../components/exceptions/error_callout.tsx | 169 ++++++++++++++++++ .../components/exceptions/translations.ts | 49 +++++ .../exceptions/use_add_exception.test.tsx | 44 +++++ .../exceptions/use_add_exception.tsx | 8 +- ...tch_or_create_rule_exception_list.test.tsx | 2 +- ...se_fetch_or_create_rule_exception_list.tsx | 8 +- .../components/exceptions/viewer/index.tsx | 2 + .../use_dissasociate_exception_list.test.tsx | 52 ++++++ .../rules/use_dissasociate_exception_list.tsx | 80 +++++++++ 13 files changed, 706 insertions(+), 54 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 03051ead357c96..21f82c6ab4c986 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -18,7 +18,6 @@ import { EuiCheckbox, EuiSpacer, EuiFormRow, - EuiCallOut, EuiText, } from '@elastic/eui'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; @@ -28,6 +27,7 @@ import { ExceptionListType, } from '../../../../../public/lists_plugin_deps'; import * as i18n from './translations'; +import * as sharedI18n from '../translations'; import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; import { useAppToasts } from '../../../hooks/use_app_toasts'; import { useKibana } from '../../../lib/kibana'; @@ -35,6 +35,7 @@ import { ExceptionBuilderComponent } from '../builder'; import { Loader } from '../../loader'; import { useAddOrUpdateException } from '../use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; +import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list'; import { AddExceptionComments } from '../add_exception_comments'; import { @@ -46,6 +47,7 @@ import { entryHasNonEcsType, getMappedNonEcsValue, } from '../helpers'; +import { ErrorInfo, ErrorCallout } from '../error_callout'; import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; export interface AddExceptionModalBaseProps { @@ -107,13 +109,14 @@ export const AddExceptionModal = memo(function AddExceptionModal({ }: AddExceptionModalProps) { const { http } = useKibana().services; const [comment, setComment] = useState(''); + const { rule: maybeRule } = useRuleAsync(ruleId); const [shouldCloseAlert, setShouldCloseAlert] = useState(false); const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false); const [shouldDisableBulkClose, setShouldDisableBulkClose] = useState(false); const [exceptionItemsToAdd, setExceptionItemsToAdd] = useState< Array >([]); - const [fetchOrCreateListError, setFetchOrCreateListError] = useState(false); + const [fetchOrCreateListError, setFetchOrCreateListError] = useState(null); const { addError, addSuccess } = useAppToasts(); const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); const [ @@ -164,17 +167,41 @@ export const AddExceptionModal = memo(function AddExceptionModal({ }, [onRuleChange] ); - const onFetchOrCreateExceptionListError = useCallback( - (error: Error) => { - setFetchOrCreateListError(true); + + const handleDissasociationSuccess = useCallback( + (id: string): void => { + handleRuleChange(true); + addSuccess(sharedI18n.DISSASOCIATE_LIST_SUCCESS(id)); + onCancel(); + }, + [handleRuleChange, addSuccess, onCancel] + ); + + const handleDissasociationError = useCallback( + (error: Error): void => { + addError(error, { title: sharedI18n.DISSASOCIATE_EXCEPTION_LIST_ERROR }); + onCancel(); + }, + [addError, onCancel] + ); + + const handleFetchOrCreateExceptionListError = useCallback( + (error: Error, statusCode: number | null, message: string | null) => { + setFetchOrCreateListError({ + reason: error.message, + code: statusCode, + details: message, + listListId: null, + }); }, [setFetchOrCreateListError] ); + const [isLoadingExceptionList, ruleExceptionList] = useFetchOrCreateRuleExceptionList({ http, ruleId, exceptionListType, - onError: onFetchOrCreateExceptionListError, + onError: handleFetchOrCreateExceptionListError, onSuccess: handleRuleChange, }); @@ -279,7 +306,9 @@ export const AddExceptionModal = memo(function AddExceptionModal({ ]); const isSubmitButtonDisabled = useMemo( - () => fetchOrCreateListError || exceptionItemsToAdd.every((item) => item.entries.length === 0), + () => + fetchOrCreateListError != null || + exceptionItemsToAdd.every((item) => item.entries.length === 0), [fetchOrCreateListError, exceptionItemsToAdd] ); @@ -295,19 +324,27 @@ export const AddExceptionModal = memo(function AddExceptionModal({ - {fetchOrCreateListError === true && ( - -

{i18n.ADD_EXCEPTION_FETCH_ERROR}

-
+ {fetchOrCreateListError != null && ( + + + )} - {fetchOrCreateListError === false && + {fetchOrCreateListError == null && (isLoadingExceptionList || isIndexPatternLoading || isSignalIndexLoading || isSignalIndexPatternLoading) && ( )} - {fetchOrCreateListError === false && + {fetchOrCreateListError == null && !isSignalIndexLoading && !isSignalIndexPatternLoading && !isLoadingExceptionList && @@ -377,20 +414,21 @@ export const AddExceptionModal = memo(function AddExceptionModal({ )} + {fetchOrCreateListError == null && ( + + {i18n.CANCEL} - - {i18n.CANCEL} - - - {i18n.ADD_EXCEPTION} - - + + {i18n.ADD_EXCEPTION} + + + )} ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx index 6ff218ca06059f..c724e6a2c711fd 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx @@ -77,6 +77,7 @@ describe('When the edit exception modal is opened', () => { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> void; onConfirm: () => void; + onRuleChange?: () => void; } const Modal = styled(EuiModal)` @@ -83,14 +88,18 @@ const ModalBodySection = styled.section` export const EditExceptionModal = memo(function EditExceptionModal({ ruleName, + ruleId, ruleIndices, exceptionItem, exceptionListType, onCancel, onConfirm, + onRuleChange, }: EditExceptionModalProps) { const { http } = useKibana().services; const [comment, setComment] = useState(''); + const { rule: maybeRule } = useRuleAsync(ruleId); + const [updateError, setUpdateError] = useState(null); const [hasVersionConflict, setHasVersionConflict] = useState(false); const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false); const [shouldDisableBulkClose, setShouldDisableBulkClose] = useState(false); @@ -108,18 +117,44 @@ export const EditExceptionModal = memo(function EditExceptionModal({ 'rules' ); - const onError = useCallback( - (error) => { + const handleExceptionUpdateError = useCallback( + (error: Error, statusCode: number | null, message: string | null) => { if (error.message.includes('Conflict')) { setHasVersionConflict(true); } else { - addError(error, { title: i18n.EDIT_EXCEPTION_ERROR }); - onCancel(); + setUpdateError({ + reason: error.message, + code: statusCode, + details: message, + listListId: exceptionItem.list_id, + }); } }, + [setUpdateError, setHasVersionConflict, exceptionItem.list_id] + ); + + const handleDissasociationSuccess = useCallback( + (id: string): void => { + addSuccess(sharedI18n.DISSASOCIATE_LIST_SUCCESS(id)); + + if (onRuleChange) { + onRuleChange(); + } + + onCancel(); + }, + [addSuccess, onCancel, onRuleChange] + ); + + const handleDissasociationError = useCallback( + (error: Error): void => { + addError(error, { title: sharedI18n.DISSASOCIATE_EXCEPTION_LIST_ERROR }); + onCancel(); + }, [addError, onCancel] ); - const onSuccess = useCallback(() => { + + const handleExceptionUpdateSuccess = useCallback((): void => { addSuccess(i18n.EDIT_EXCEPTION_SUCCESS); onConfirm(); }, [addSuccess, onConfirm]); @@ -127,8 +162,8 @@ export const EditExceptionModal = memo(function EditExceptionModal({ const [{ isLoading: addExceptionIsLoading }, addOrUpdateExceptionItems] = useAddOrUpdateException( { http, - onSuccess, - onError, + onSuccess: handleExceptionUpdateSuccess, + onError: handleExceptionUpdateError, } ); @@ -222,11 +257,9 @@ export const EditExceptionModal = memo(function EditExceptionModal({ {ruleName} - {(addExceptionIsLoading || isIndexPatternLoading || isSignalIndexLoading) && ( )} - {!isSignalIndexLoading && !addExceptionIsLoading && !isIndexPatternLoading && ( <> @@ -280,7 +313,18 @@ export const EditExceptionModal = memo(function EditExceptionModal({ )} - + {updateError != null && ( + + + + )} {hasVersionConflict && ( @@ -288,20 +332,21 @@ export const EditExceptionModal = memo(function EditExceptionModal({ )} + {updateError == null && ( + + {i18n.CANCEL} - - {i18n.CANCEL} - - - {i18n.EDIT_EXCEPTION_SAVE_BUTTON} - - + + {i18n.EDIT_EXCEPTION_SAVE_BUTTON} + + + )} ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx new file mode 100644 index 00000000000000..c9efa5e54dccf6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx @@ -0,0 +1,160 @@ +/* + * 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 { mountWithIntl } from 'test_utils/enzyme_helpers'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { getListMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; +import { useDissasociateExceptionList } from '../../../detections/containers/detection_engine/rules/use_dissasociate_exception_list'; +import { createKibanaCoreStartMock } from '../../mock/kibana_core'; +import { ErrorCallout } from './error_callout'; +import { savedRuleMock } from '../../../detections/containers/detection_engine/rules/mock'; + +jest.mock('../../../detections/containers/detection_engine/rules/use_dissasociate_exception_list'); + +const mockKibanaHttpService = createKibanaCoreStartMock().http; + +describe('ErrorCallout', () => { + const mockDissasociate = jest.fn(); + + beforeEach(() => { + (useDissasociateExceptionList as jest.Mock).mockReturnValue([false, mockDissasociate]); + }); + + it('it renders error details', () => { + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="errorCalloutContainer"] .euiCallOutHeader__title').text() + ).toEqual('Error: error reason (500)'); + expect(wrapper.find('[data-test-subj="errorCalloutMessage"]').at(0).text()).toEqual( + 'Error fetching exception list' + ); + }); + + it('it invokes "onCancel" when cancel button clicked', () => { + const mockOnCancel = jest.fn(); + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="errorCalloutCancelButton"]').at(0).simulate('click'); + + expect(mockOnCancel).toHaveBeenCalled(); + }); + + it('it does not render status code if not available', () => { + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="errorCalloutContainer"] .euiCallOutHeader__title').text() + ).toEqual('Error: not found'); + expect(wrapper.find('[data-test-subj="errorCalloutMessage"]').at(0).text()).toEqual( + 'Error fetching exception list' + ); + expect(wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').exists()).toBeFalsy(); + }); + + it('it renders specific missing exceptions list error', () => { + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="errorCalloutContainer"] .euiCallOutHeader__title').text() + ).toEqual('Error: not found (404)'); + expect(wrapper.find('[data-test-subj="errorCalloutMessage"]').at(0).text()).toEqual( + 'The associated exception list (some_uuid) no longer exists. Please remove the missing exception list to add additional exceptions to the detection rule.' + ); + expect(wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').exists()).toBeTruthy(); + }); + + it('it dissasociates list from rule when remove exception list clicked ', () => { + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').at(0).simulate('click'); + + expect(mockDissasociate).toHaveBeenCalledWith([]); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx new file mode 100644 index 00000000000000..a2419ef16df3ab --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.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 React, { useMemo, useEffect, useState, useCallback } from 'react'; +import { + EuiButtonEmpty, + EuiAccordion, + EuiCodeBlock, + EuiButton, + EuiCallOut, + EuiText, + EuiSpacer, +} from '@elastic/eui'; + +import { HttpSetup } from '../../../../../../../src/core/public'; +import { List } from '../../../../common/detection_engine/schemas/types/lists'; +import { Rule } from '../../../detections/containers/detection_engine/rules/types'; +import * as i18n from './translations'; +import { useDissasociateExceptionList } from '../../../detections/containers/detection_engine/rules/use_dissasociate_exception_list'; + +export interface ErrorInfo { + reason: string | null; + code: number | null; + details: string | null; + listListId: string | null; +} + +export interface ErrorCalloutProps { + http: HttpSetup; + rule: Rule | null; + errorInfo: ErrorInfo; + onCancel: () => void; + onSuccess: (listId: string) => void; + onError: (arg: Error) => void; +} + +const ErrorCalloutComponent = ({ + http, + rule, + errorInfo, + onCancel, + onError, + onSuccess, +}: ErrorCalloutProps): JSX.Element => { + const [listToDelete, setListToDelete] = useState(null); + const [errorTitle, setErrorTitle] = useState(''); + const [errorMessage, setErrorMessage] = useState(i18n.ADD_EXCEPTION_FETCH_ERROR); + + const handleOnSuccess = useCallback((): void => { + onSuccess(listToDelete != null ? listToDelete.id : ''); + }, [onSuccess, listToDelete]); + + const [isDissasociatingList, handleDissasociateExceptionList] = useDissasociateExceptionList({ + http, + ruleRuleId: rule != null ? rule.rule_id : '', + onSuccess: handleOnSuccess, + onError, + }); + + const canDisplay404Actions = useMemo( + (): boolean => + errorInfo.code === 404 && + rule != null && + listToDelete != null && + handleDissasociateExceptionList != null, + [errorInfo.code, listToDelete, handleDissasociateExceptionList, rule] + ); + + useEffect((): void => { + // Yes, it's redundant, unfortunately typescript wasn't picking up + // that `listToDelete` is checked in canDisplay404Actions + if (canDisplay404Actions && listToDelete != null) { + setErrorMessage(i18n.ADD_EXCEPTION_FETCH_404_ERROR(listToDelete.id)); + } + + setErrorTitle(`${errorInfo.reason}${errorInfo.code != null ? ` (${errorInfo.code})` : ''}`); + }, [errorInfo.reason, errorInfo.code, listToDelete, canDisplay404Actions]); + + const handleDissasociateList = useCallback((): void => { + // Yes, it's redundant, unfortunately typescript wasn't picking up + // that `handleDissasociateExceptionList` and `list` are checked in + // canDisplay404Actions + if ( + canDisplay404Actions && + rule != null && + listToDelete != null && + handleDissasociateExceptionList != null + ) { + const exceptionLists = (rule.exceptions_list ?? []).filter( + ({ id }) => id !== listToDelete.id + ); + + handleDissasociateExceptionList(exceptionLists); + } + }, [handleDissasociateExceptionList, listToDelete, canDisplay404Actions, rule]); + + useEffect((): void => { + if (errorInfo.code === 404 && rule != null && rule.exceptions_list != null) { + const [listFound] = rule.exceptions_list.filter( + ({ id, list_id: listId }) => + (errorInfo.details != null && errorInfo.details.includes(id)) || + errorInfo.listListId === listId + ); + setListToDelete(listFound); + } + }, [rule, errorInfo.details, errorInfo.code, errorInfo.listListId]); + + return ( + + +

{errorMessage}

+
+ + {listToDelete != null && ( + +

{i18n.MODAL_ERROR_ACCORDION_TEXT}

+ + } + > + + {JSON.stringify(listToDelete)} + +
+ )} + + + {i18n.CANCEL} + + {canDisplay404Actions && ( + + {i18n.CLEAR_EXCEPTIONS_LABEL} + + )} +
+ ); +}; + +ErrorCalloutComponent.displayName = 'ErrorCalloutComponent'; + +export const ErrorCallout = React.memo(ErrorCalloutComponent); + +ErrorCallout.displayName = 'ErrorCallout'; 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 index 13e9d0df549f8a..484a3d593026e1 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts @@ -190,3 +190,52 @@ export const TOTAL_ITEMS_FETCH_ERROR = i18n.translate( defaultMessage: 'Error getting exception item totals', } ); + +export const CLEAR_EXCEPTIONS_LABEL = i18n.translate( + 'xpack.securitySolution.exceptions.clearExceptionsLabel', + { + defaultMessage: 'Remove Exception List', + } +); + +export const ADD_EXCEPTION_FETCH_404_ERROR = (listId: string) => + i18n.translate('xpack.securitySolution.exceptions.fetch404Error', { + values: { listId }, + defaultMessage: + 'The associated exception list ({listId}) no longer exists. Please remove the missing exception list to add additional exceptions to the detection rule.', + }); + +export const ADD_EXCEPTION_FETCH_ERROR = i18n.translate( + 'xpack.securitySolution.exceptions.fetchError', + { + defaultMessage: 'Error fetching exception list', + } +); + +export const ERROR = i18n.translate('xpack.securitySolution.exceptions.errorLabel', { + defaultMessage: 'Error', +}); + +export const CANCEL = i18n.translate('xpack.securitySolution.exceptions.cancelLabel', { + defaultMessage: 'Cancel', +}); + +export const MODAL_ERROR_ACCORDION_TEXT = i18n.translate( + 'xpack.securitySolution.exceptions.modalErrorAccordionText', + { + defaultMessage: 'Show rule reference information:', + } +); + +export const DISSASOCIATE_LIST_SUCCESS = (id: string) => + i18n.translate('xpack.securitySolution.exceptions.dissasociateListSuccessText', { + values: { id }, + defaultMessage: 'Exception list ({id}) has successfully been removed', + }); + +export const DISSASOCIATE_EXCEPTION_LIST_ERROR = i18n.translate( + 'xpack.securitySolution.exceptions.dissasociateExceptionListError', + { + defaultMessage: 'Failed to remove exception list', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx index 6611ee2385d108..46923e07d225ad 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx @@ -148,6 +148,50 @@ describe('useAddOrUpdateException', () => { }); }); + it('invokes "onError" if call to add exception item fails', async () => { + const mockError = new Error('error adding item'); + + addExceptionListItem = jest + .spyOn(listsApi, 'addExceptionListItem') + .mockRejectedValue(mockError); + + await act(async () => { + const { rerender, result, waitForNextUpdate } = render(); + const addOrUpdateItems = await waitForAddOrUpdateFunc({ + rerender, + result, + waitForNextUpdate, + }); + if (addOrUpdateItems) { + addOrUpdateItems(...addOrUpdateItemsArgs); + } + await waitForNextUpdate(); + expect(onError).toHaveBeenCalledWith(mockError, null, null); + }); + }); + + it('invokes "onError" if call to update exception item fails', async () => { + const mockError = new Error('error updating item'); + + updateExceptionListItem = jest + .spyOn(listsApi, 'updateExceptionListItem') + .mockRejectedValue(mockError); + + await act(async () => { + const { rerender, result, waitForNextUpdate } = render(); + const addOrUpdateItems = await waitForAddOrUpdateFunc({ + rerender, + result, + waitForNextUpdate, + }); + if (addOrUpdateItems) { + addOrUpdateItems(...addOrUpdateItemsArgs); + } + await waitForNextUpdate(); + expect(onError).toHaveBeenCalledWith(mockError, null, null); + }); + }); + describe('when alertIdToClose is not passed in', () => { it('should not update the alert status', async () => { await act(async () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx index 9d45a411b51302..be289b0e85e66b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx @@ -42,7 +42,7 @@ export type ReturnUseAddOrUpdateException = [ export interface UseAddOrUpdateExceptionProps { http: HttpStart; - onError: (arg: Error) => void; + onError: (arg: Error, code: number | null, message: string | null) => void; onSuccess: () => void; } @@ -157,7 +157,11 @@ export const useAddOrUpdateException = ({ } catch (error) { if (isSubscribed) { setIsLoading(false); - onError(error); + if (error.body != null) { + onError(error, error.body.status_code, error.body.message); + } else { + onError(error, null, null); + } } } }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx index 39d88bd8e4724d..f20a58b9ffa36a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx @@ -379,7 +379,7 @@ describe('useFetchOrCreateRuleExceptionList', () => { await waitForNextUpdate(); await waitForNextUpdate(); expect(onError).toHaveBeenCalledTimes(1); - expect(onError).toHaveBeenCalledWith(error); + expect(onError).toHaveBeenCalledWith(error, null, null); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx index 0d367e03a799f3..944631d4e9fb5f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx @@ -30,7 +30,7 @@ export interface UseFetchOrCreateRuleExceptionListProps { http: HttpStart; ruleId: Rule['id']; exceptionListType: ExceptionListSchema['type']; - onError: (arg: Error) => void; + onError: (arg: Error, code: number | null, message: string | null) => void; onSuccess?: (ruleWasChanged: boolean) => void; } @@ -179,7 +179,11 @@ export const useFetchOrCreateRuleExceptionList = ({ if (isSubscribed) { setIsLoading(false); setExceptionList(null); - onError(error); + if (error.body != null) { + onError(error, error.body.status_code, error.body.message); + } else { + onError(error, null, null); + } } } } 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 index 7482068454a97c..c97895cdfe2363 100644 --- 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 @@ -322,11 +322,13 @@ const ExceptionsViewerComponent = ({ exceptionListTypeToEdit != null && ( )} diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx new file mode 100644 index 00000000000000..6b1938655dc33f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx @@ -0,0 +1,52 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; + +import { createKibanaCoreStartMock } from '../../../../common/mock/kibana_core'; + +import * as api from './api'; +import { ruleMock } from './mock'; +import { + ReturnUseDissasociateExceptionList, + UseDissasociateExceptionListProps, + useDissasociateExceptionList, +} from './use_dissasociate_exception_list'; + +const mockKibanaHttpService = createKibanaCoreStartMock().http; + +describe('useDissasociateExceptionList', () => { + const onError = jest.fn(); + const onSuccess = jest.fn(); + + beforeEach(() => { + jest.spyOn(api, 'patchRule').mockResolvedValue(ruleMock); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('initializes hook', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseDissasociateExceptionListProps, + ReturnUseDissasociateExceptionList + >(() => + useDissasociateExceptionList({ + http: mockKibanaHttpService, + ruleRuleId: 'rule_id', + onError, + onSuccess, + }) + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual([false, null]); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx new file mode 100644 index 00000000000000..dffba3e6e04368 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx @@ -0,0 +1,80 @@ +/* + * 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, useState, useRef } from 'react'; + +import { HttpStart } from '../../../../../../../../src/core/public'; +import { List } from '../../../../../common/detection_engine/schemas/types/lists'; +import { patchRule } from './api'; + +type Func = (lists: List[]) => void; +export type ReturnUseDissasociateExceptionList = [boolean, Func | null]; + +export interface UseDissasociateExceptionListProps { + http: HttpStart; + ruleRuleId: string; + onError: (arg: Error) => void; + onSuccess: () => void; +} + +/** + * Hook for removing an exception list reference from a rule + * + * @param http Kibana http service + * @param ruleRuleId a rule_id (NOT id) + * @param onError error callback + * @param onSuccess success callback + * + */ +export const useDissasociateExceptionList = ({ + http, + ruleRuleId, + onError, + onSuccess, +}: UseDissasociateExceptionListProps): ReturnUseDissasociateExceptionList => { + const [isLoading, setLoading] = useState(false); + const dissasociateList = useRef(null); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + + const dissasociateListFromRule = (id: string) => async ( + exceptionLists: List[] + ): Promise => { + try { + if (isSubscribed) { + setLoading(true); + + await patchRule({ + ruleProperties: { + rule_id: id, + exceptions_list: exceptionLists, + }, + signal: abortCtrl.signal, + }); + + onSuccess(); + setLoading(false); + } + } catch (err) { + if (isSubscribed) { + setLoading(false); + onError(err); + } + } + }; + + dissasociateList.current = dissasociateListFromRule(ruleRuleId); + + return (): void => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [http, ruleRuleId, onError, onSuccess]); + + return [isLoading, dissasociateList.current]; +}; From d6c45a2e70a20d552c91f3df89da1cd081077209 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Wed, 26 Aug 2020 09:01:32 -0600 Subject: [PATCH 034/216] Fixes runtime error with meta when it is missing (#75844) ## Summary Found in 7.9.0, if you post a rule with an action that has a missing "meta" then you are going to get errors in your UI that look something like: ```ts An error occurred during rule execution: message: "Cannot read property 'kibana_siem_app_url' of null" name: "Unusual Windows Remote User" id: "1cc27e7e-d7c7-4f6a-b918-8c272fc6b1a3" rule id: "1781d055-5c66-4adf-9e93-fc0fa69550c9" signals index: ".siem-signals-default" ``` This fixes the accidental referencing of the null/undefined property and adds both integration and unit tests in that area of code. If you have an action id handy you can manually test this by editing the json file of: ```ts test_cases/queries/action_without_meta.json ``` to have your action id and then posting it like so: ```ts ./post_rule.sh ./rules/test_cases/queries/action_without_meta.json ``` ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../rules_notification_alert_type.test.ts | 78 ++++++++++ .../rules_notification_alert_type.ts | 4 +- .../queries/action_without_meta.json | 42 ++++++ .../signals/signal_rule_alert_type.test.ts | 100 +++++++++++++ .../signals/signal_rule_alert_type.ts | 3 +- .../security_and_spaces/tests/add_actions.ts | 134 ++++++++++++++++++ .../security_and_spaces/tests/index.ts | 1 + .../detection_engine_api_integration/utils.ts | 43 ++++++ 8 files changed, 402 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/queries/action_without_meta.json create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts index 3eefd3e665cd62..593ada470b118b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts @@ -79,6 +79,84 @@ describe('rules_notification_alert_type', () => { ); }); + it('should resolve results_link when meta is undefined to use "/app/security"', async () => { + const ruleAlert = getResult(); + delete ruleAlert.params.meta; + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'rule-id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + alertServices.callCluster.mockResolvedValue({ + count: 10, + }); + + await alert.executor(payload); + expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + + const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( + 'default', + expect.objectContaining({ + results_link: + '/app/security/detections/rules/id/rule-id?timerange=(global:(linkTo:!(timeline),timerange:(from:1576255233400,kind:absolute,to:1576341633400)),timeline:(linkTo:!(global),timerange:(from:1576255233400,kind:absolute,to:1576341633400)))', + }) + ); + }); + + it('should resolve results_link when meta is an empty object to use "/app/security"', async () => { + const ruleAlert = getResult(); + ruleAlert.params.meta = {}; + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'rule-id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + alertServices.callCluster.mockResolvedValue({ + count: 10, + }); + await alert.executor(payload); + expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + + const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( + 'default', + expect.objectContaining({ + results_link: + '/app/security/detections/rules/id/rule-id?timerange=(global:(linkTo:!(timeline),timerange:(from:1576255233400,kind:absolute,to:1576341633400)),timeline:(linkTo:!(global),timerange:(from:1576255233400,kind:absolute,to:1576341633400)))', + }) + ); + }); + + it('should resolve results_link to custom kibana link when given one', async () => { + const ruleAlert = getResult(); + ruleAlert.params.meta = { + kibana_siem_app_url: 'http://localhost', + }; + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'rule-id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + alertServices.callCluster.mockResolvedValue({ + count: 10, + }); + await alert.executor(payload); + expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + + const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( + 'default', + expect.objectContaining({ + results_link: + 'http://localhost/detections/rules/id/rule-id?timerange=(global:(linkTo:!(timeline),timerange:(from:1576255233400,kind:absolute,to:1576341633400)),timeline:(linkTo:!(global),timerange:(from:1576255233400,kind:absolute,to:1576341633400)))', + }) + ); + }); + it('should not call alertInstanceFactory if signalsCount was 0', async () => { const ruleAlert = getResult(); alertServices.savedObjectsClient.get.mockResolvedValue({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts index ab824957087fc1..2eb34529d044c5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts @@ -64,8 +64,8 @@ export const rulesNotificationAlertType = ({ from: fromInMs, to: toInMs, id: ruleAlertSavedObject.id, - kibanaSiemAppUrl: (ruleAlertParams.meta as { kibana_siem_app_url?: string }) - .kibana_siem_app_url, + kibanaSiemAppUrl: (ruleAlertParams.meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, }); logger.info( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/queries/action_without_meta.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/queries/action_without_meta.json new file mode 100644 index 00000000000000..6569a641de3a2c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/queries/action_without_meta.json @@ -0,0 +1,42 @@ +{ + "type": "query", + "index": [ + "apm-*-transaction*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "filters": [], + "language": "kuery", + "query": "host.name: *", + "author": [], + "false_positives": [], + "references": [], + "risk_score": 50, + "risk_score_mapping": [], + "severity": "low", + "severity_mapping": [], + "threat": [], + "name": "Host Name Test", + "description": "Host Name Test", + "tags": [], + "license": "", + "interval": "5m", + "from": "now-30s", + "to": "now", + "actions": [ + { + "group": "default", + "id": "4c42ecf2-5e9b-4ce6-8a7a-ab620fd8b169", + "params": { + "body": "{}" + }, + "action_type_id": ".webhook" + } + ], + "enabled": true, + "throttle": "rule" +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index b0c855afa8be99..b29d15f5e5c727 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -16,6 +16,7 @@ import { getListsClient, getExceptions, sortExceptionItems, + parseScheduleDates, } from './utils'; import { RuleExecutorOptions } from './types'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; @@ -227,6 +228,105 @@ describe('rules_notification_alert_type', () => { ); }); + it('should resolve results_link when meta is an empty object to use "/app/security"', async () => { + const ruleAlert = getResult(); + ruleAlert.params.meta = {}; + ruleAlert.actions = [ + { + actionTypeId: '.slack', + params: { + message: + 'Rule generated {{state.signals_count}} signals\n\n{{context.rule.name}}\n{{{context.results_link}}}', + }, + group: 'default', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + }, + ]; + + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'rule-id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + (parseScheduleDates as jest.Mock).mockReturnValue(moment(100)); + payload.params.meta = {}; + await alert.executor(payload); + + expect(scheduleNotificationActions).toHaveBeenCalledWith( + expect.objectContaining({ + resultsLink: + '/app/security/detections/rules/id/rule-id?timerange=(global:(linkTo:!(timeline),timerange:(from:100,kind:absolute,to:100)),timeline:(linkTo:!(global),timerange:(from:100,kind:absolute,to:100)))', + }) + ); + }); + + it('should resolve results_link when meta is undefined use "/app/security"', async () => { + const ruleAlert = getResult(); + delete ruleAlert.params.meta; + ruleAlert.actions = [ + { + actionTypeId: '.slack', + params: { + message: + 'Rule generated {{state.signals_count}} signals\n\n{{context.rule.name}}\n{{{context.results_link}}}', + }, + group: 'default', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + }, + ]; + + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'rule-id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + (parseScheduleDates as jest.Mock).mockReturnValue(moment(100)); + delete payload.params.meta; + await alert.executor(payload); + + expect(scheduleNotificationActions).toHaveBeenCalledWith( + expect.objectContaining({ + resultsLink: + '/app/security/detections/rules/id/rule-id?timerange=(global:(linkTo:!(timeline),timerange:(from:100,kind:absolute,to:100)),timeline:(linkTo:!(global),timerange:(from:100,kind:absolute,to:100)))', + }) + ); + }); + + it('should resolve results_link with a custom link', async () => { + const ruleAlert = getResult(); + ruleAlert.params.meta = { kibana_siem_app_url: 'http://localhost' }; + ruleAlert.actions = [ + { + actionTypeId: '.slack', + params: { + message: + 'Rule generated {{state.signals_count}} signals\n\n{{context.rule.name}}\n{{{context.results_link}}}', + }, + group: 'default', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + }, + ]; + + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'rule-id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + (parseScheduleDates as jest.Mock).mockReturnValue(moment(100)); + payload.params.meta = { kibana_siem_app_url: 'http://localhost' }; + await alert.executor(payload); + + expect(scheduleNotificationActions).toHaveBeenCalledWith( + expect.objectContaining({ + resultsLink: + 'http://localhost/detections/rules/id/rule-id?timerange=(global:(linkTo:!(timeline),timerange:(from:100,kind:absolute,to:100)),timeline:(linkTo:!(global),timerange:(from:100,kind:absolute,to:100)))', + }) + ); + }); + describe('ML rule', () => { it('should throw an error if ML plugin was not available', async () => { const ruleAlert = getMlResult(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 0e859ecef31c6c..b5cbf80b084f79 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -356,7 +356,8 @@ export const signalRulesAlertType = ({ from: fromInMs, to: toInMs, id: savedObject.id, - kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string }).kibana_siem_app_url, + kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, }); logger.info( diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts new file mode 100644 index 00000000000000..24688512370472 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + removeServerGeneratedProperties, + waitFor, + getWebHookAction, + getRuleWithWebHookAction, + getSimpleRuleOutputWithWebHookAction, +} from '../../utils'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('add_actions', () => { + describe('adding actions', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should be able to create a new webhook action and attach it to a rule', async () => { + // create a new action + const { body: hookAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(getWebHookAction()) + .expect(200); + + // create a rule with the action attached + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getRuleWithWebHookAction(hookAction.id)) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql( + getSimpleRuleOutputWithWebHookAction(`${bodyToCompare?.actions?.[0].id}`) + ); + }); + + it('should be able to create a new webhook action and attach it to a rule without a meta field and run it correctly', async () => { + // create a new action + const { body: hookAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(getWebHookAction()) + .expect(200); + + // create a rule with the action attached + const { body: rule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getRuleWithWebHookAction(hookAction.id)) + .expect(200); + + // wait for Task Manager to execute the rule and its update status + await waitFor(async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [rule.id] }) + .expect(200); + return body[rule.id].current_status?.status === 'succeeded'; + }); + + // expected result for status should be 'succeeded' + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [rule.id] }) + .expect(200); + expect(body[rule.id].current_status.status).to.eql('succeeded'); + }); + + it('should be able to create a new webhook action and attach it to a rule with a meta field and run it correctly', async () => { + // create a new action + const { body: hookAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(getWebHookAction()) + .expect(200); + + // create a rule with the action attached and a meta field + const ruleWithAction: CreateRulesSchema = { + ...getRuleWithWebHookAction(hookAction.id), + meta: {}, + }; + + const { body: rule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleWithAction) + .expect(200); + + // wait for Task Manager to execute the rule and update status + await waitFor(async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [rule.id] }) + .expect(200); + return body[rule.id].current_status?.status === 'succeeded'; + }); + + // expected result for status should be 'succeeded' + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [rule.id] }) + .expect(200); + expect(body[rule.id].current_status.status).to.eql('succeeded'); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index a480e63ff4a923..779205377621d4 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -11,6 +11,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { describe('detection engine api security and spaces enabled', function () { this.tags('ciGroup1'); + loadTestFile(require.resolve('./add_actions')); loadTestFile(require.resolve('./add_prepackaged_rules')); loadTestFile(require.resolve('./create_rules')); loadTestFile(require.resolve('./create_rules_bulk')); diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 604133a1c2dc7b..4cbbc142edd40f 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -557,6 +557,49 @@ export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial => exceptions_list: [], }); +export const getWebHookAction = () => ({ + actionTypeId: '.webhook', + config: { + method: 'post', + url: 'http://localhost', + }, + secrets: { + user: 'example', + password: 'example', + }, + name: 'Some connector', +}); + +export const getRuleWithWebHookAction = (id: string): CreateRulesSchema => ({ + ...getSimpleRule(), + throttle: 'rule', + actions: [ + { + group: 'default', + id, + params: { + body: '{}', + }, + action_type_id: '.webhook', + }, + ], +}); + +export const getSimpleRuleOutputWithWebHookAction = (actionId: string): Partial => ({ + ...getSimpleRuleOutput(), + throttle: 'rule', + actions: [ + { + action_type_id: '.webhook', + group: 'default', + id: actionId, + params: { + body: '{}', + }, + }, + ], +}); + // Similar to ReactJs's waitFor from here: https://testing-library.com/docs/dom-testing-library/api-async#waitfor export const waitFor = async ( functionToTest: () => Promise, From e773f221a3814700d55284bc34bd4637cc7312bd Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 26 Aug 2020 08:41:09 -0700 Subject: [PATCH 035/216] Revert "[Security Solution][Exceptions] - Improve UX for missing exception list associated with rule (#75898)" This reverts commit b9c820120202dc44296e080550e87c93bd37dd55. --- .../exceptions/add_exception_modal/index.tsx | 90 +++------- .../edit_exception_modal/index.test.tsx | 5 - .../exceptions/edit_exception_modal/index.tsx | 91 +++------- .../exceptions/error_callout.test.tsx | 160 ----------------- .../components/exceptions/error_callout.tsx | 169 ------------------ .../components/exceptions/translations.ts | 49 ----- .../exceptions/use_add_exception.test.tsx | 44 ----- .../exceptions/use_add_exception.tsx | 8 +- ...tch_or_create_rule_exception_list.test.tsx | 2 +- ...se_fetch_or_create_rule_exception_list.tsx | 8 +- .../components/exceptions/viewer/index.tsx | 2 - .../use_dissasociate_exception_list.test.tsx | 52 ------ .../rules/use_dissasociate_exception_list.tsx | 80 --------- 13 files changed, 54 insertions(+), 706 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 21f82c6ab4c986..03051ead357c96 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -18,6 +18,7 @@ import { EuiCheckbox, EuiSpacer, EuiFormRow, + EuiCallOut, EuiText, } from '@elastic/eui'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; @@ -27,7 +28,6 @@ import { ExceptionListType, } from '../../../../../public/lists_plugin_deps'; import * as i18n from './translations'; -import * as sharedI18n from '../translations'; import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; import { useAppToasts } from '../../../hooks/use_app_toasts'; import { useKibana } from '../../../lib/kibana'; @@ -35,7 +35,6 @@ import { ExceptionBuilderComponent } from '../builder'; import { Loader } from '../../loader'; import { useAddOrUpdateException } from '../use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; -import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list'; import { AddExceptionComments } from '../add_exception_comments'; import { @@ -47,7 +46,6 @@ import { entryHasNonEcsType, getMappedNonEcsValue, } from '../helpers'; -import { ErrorInfo, ErrorCallout } from '../error_callout'; import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; export interface AddExceptionModalBaseProps { @@ -109,14 +107,13 @@ export const AddExceptionModal = memo(function AddExceptionModal({ }: AddExceptionModalProps) { const { http } = useKibana().services; const [comment, setComment] = useState(''); - const { rule: maybeRule } = useRuleAsync(ruleId); const [shouldCloseAlert, setShouldCloseAlert] = useState(false); const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false); const [shouldDisableBulkClose, setShouldDisableBulkClose] = useState(false); const [exceptionItemsToAdd, setExceptionItemsToAdd] = useState< Array >([]); - const [fetchOrCreateListError, setFetchOrCreateListError] = useState(null); + const [fetchOrCreateListError, setFetchOrCreateListError] = useState(false); const { addError, addSuccess } = useAppToasts(); const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); const [ @@ -167,41 +164,17 @@ export const AddExceptionModal = memo(function AddExceptionModal({ }, [onRuleChange] ); - - const handleDissasociationSuccess = useCallback( - (id: string): void => { - handleRuleChange(true); - addSuccess(sharedI18n.DISSASOCIATE_LIST_SUCCESS(id)); - onCancel(); - }, - [handleRuleChange, addSuccess, onCancel] - ); - - const handleDissasociationError = useCallback( - (error: Error): void => { - addError(error, { title: sharedI18n.DISSASOCIATE_EXCEPTION_LIST_ERROR }); - onCancel(); - }, - [addError, onCancel] - ); - - const handleFetchOrCreateExceptionListError = useCallback( - (error: Error, statusCode: number | null, message: string | null) => { - setFetchOrCreateListError({ - reason: error.message, - code: statusCode, - details: message, - listListId: null, - }); + const onFetchOrCreateExceptionListError = useCallback( + (error: Error) => { + setFetchOrCreateListError(true); }, [setFetchOrCreateListError] ); - const [isLoadingExceptionList, ruleExceptionList] = useFetchOrCreateRuleExceptionList({ http, ruleId, exceptionListType, - onError: handleFetchOrCreateExceptionListError, + onError: onFetchOrCreateExceptionListError, onSuccess: handleRuleChange, }); @@ -306,9 +279,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ ]); const isSubmitButtonDisabled = useMemo( - () => - fetchOrCreateListError != null || - exceptionItemsToAdd.every((item) => item.entries.length === 0), + () => fetchOrCreateListError || exceptionItemsToAdd.every((item) => item.entries.length === 0), [fetchOrCreateListError, exceptionItemsToAdd] ); @@ -324,27 +295,19 @@ export const AddExceptionModal = memo(function AddExceptionModal({ - {fetchOrCreateListError != null && ( - - - + {fetchOrCreateListError === true && ( + +

{i18n.ADD_EXCEPTION_FETCH_ERROR}

+
)} - {fetchOrCreateListError == null && + {fetchOrCreateListError === false && (isLoadingExceptionList || isIndexPatternLoading || isSignalIndexLoading || isSignalIndexPatternLoading) && ( )} - {fetchOrCreateListError == null && + {fetchOrCreateListError === false && !isSignalIndexLoading && !isSignalIndexPatternLoading && !isLoadingExceptionList && @@ -414,21 +377,20 @@ export const AddExceptionModal = memo(function AddExceptionModal({ )} - {fetchOrCreateListError == null && ( - - {i18n.CANCEL} - - {i18n.ADD_EXCEPTION} - - - )} + + {i18n.CANCEL} + + + {i18n.ADD_EXCEPTION} + + ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx index c724e6a2c711fd..6ff218ca06059f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx @@ -77,7 +77,6 @@ describe('When the edit exception modal is opened', () => { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> void; onConfirm: () => void; - onRuleChange?: () => void; } const Modal = styled(EuiModal)` @@ -88,18 +83,14 @@ const ModalBodySection = styled.section` export const EditExceptionModal = memo(function EditExceptionModal({ ruleName, - ruleId, ruleIndices, exceptionItem, exceptionListType, onCancel, onConfirm, - onRuleChange, }: EditExceptionModalProps) { const { http } = useKibana().services; const [comment, setComment] = useState(''); - const { rule: maybeRule } = useRuleAsync(ruleId); - const [updateError, setUpdateError] = useState(null); const [hasVersionConflict, setHasVersionConflict] = useState(false); const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false); const [shouldDisableBulkClose, setShouldDisableBulkClose] = useState(false); @@ -117,44 +108,18 @@ export const EditExceptionModal = memo(function EditExceptionModal({ 'rules' ); - const handleExceptionUpdateError = useCallback( - (error: Error, statusCode: number | null, message: string | null) => { + const onError = useCallback( + (error) => { if (error.message.includes('Conflict')) { setHasVersionConflict(true); } else { - setUpdateError({ - reason: error.message, - code: statusCode, - details: message, - listListId: exceptionItem.list_id, - }); + addError(error, { title: i18n.EDIT_EXCEPTION_ERROR }); + onCancel(); } }, - [setUpdateError, setHasVersionConflict, exceptionItem.list_id] - ); - - const handleDissasociationSuccess = useCallback( - (id: string): void => { - addSuccess(sharedI18n.DISSASOCIATE_LIST_SUCCESS(id)); - - if (onRuleChange) { - onRuleChange(); - } - - onCancel(); - }, - [addSuccess, onCancel, onRuleChange] - ); - - const handleDissasociationError = useCallback( - (error: Error): void => { - addError(error, { title: sharedI18n.DISSASOCIATE_EXCEPTION_LIST_ERROR }); - onCancel(); - }, [addError, onCancel] ); - - const handleExceptionUpdateSuccess = useCallback((): void => { + const onSuccess = useCallback(() => { addSuccess(i18n.EDIT_EXCEPTION_SUCCESS); onConfirm(); }, [addSuccess, onConfirm]); @@ -162,8 +127,8 @@ export const EditExceptionModal = memo(function EditExceptionModal({ const [{ isLoading: addExceptionIsLoading }, addOrUpdateExceptionItems] = useAddOrUpdateException( { http, - onSuccess: handleExceptionUpdateSuccess, - onError: handleExceptionUpdateError, + onSuccess, + onError, } ); @@ -257,9 +222,11 @@ export const EditExceptionModal = memo(function EditExceptionModal({ {ruleName} + {(addExceptionIsLoading || isIndexPatternLoading || isSignalIndexLoading) && ( )} + {!isSignalIndexLoading && !addExceptionIsLoading && !isIndexPatternLoading && ( <> @@ -313,18 +280,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ )} - {updateError != null && ( - - - - )} + {hasVersionConflict && ( @@ -332,21 +288,20 @@ export const EditExceptionModal = memo(function EditExceptionModal({ )} - {updateError == null && ( - - {i18n.CANCEL} - - {i18n.EDIT_EXCEPTION_SAVE_BUTTON} - - - )} + + {i18n.CANCEL} + + + {i18n.EDIT_EXCEPTION_SAVE_BUTTON} + + ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx deleted file mode 100644 index c9efa5e54dccf6..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx +++ /dev/null @@ -1,160 +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 React from 'react'; -import { ThemeProvider } from 'styled-components'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; - -import { getListMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; -import { useDissasociateExceptionList } from '../../../detections/containers/detection_engine/rules/use_dissasociate_exception_list'; -import { createKibanaCoreStartMock } from '../../mock/kibana_core'; -import { ErrorCallout } from './error_callout'; -import { savedRuleMock } from '../../../detections/containers/detection_engine/rules/mock'; - -jest.mock('../../../detections/containers/detection_engine/rules/use_dissasociate_exception_list'); - -const mockKibanaHttpService = createKibanaCoreStartMock().http; - -describe('ErrorCallout', () => { - const mockDissasociate = jest.fn(); - - beforeEach(() => { - (useDissasociateExceptionList as jest.Mock).mockReturnValue([false, mockDissasociate]); - }); - - it('it renders error details', () => { - const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - - ); - - expect( - wrapper.find('[data-test-subj="errorCalloutContainer"] .euiCallOutHeader__title').text() - ).toEqual('Error: error reason (500)'); - expect(wrapper.find('[data-test-subj="errorCalloutMessage"]').at(0).text()).toEqual( - 'Error fetching exception list' - ); - }); - - it('it invokes "onCancel" when cancel button clicked', () => { - const mockOnCancel = jest.fn(); - const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - - ); - - wrapper.find('[data-test-subj="errorCalloutCancelButton"]').at(0).simulate('click'); - - expect(mockOnCancel).toHaveBeenCalled(); - }); - - it('it does not render status code if not available', () => { - const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - - ); - - expect( - wrapper.find('[data-test-subj="errorCalloutContainer"] .euiCallOutHeader__title').text() - ).toEqual('Error: not found'); - expect(wrapper.find('[data-test-subj="errorCalloutMessage"]').at(0).text()).toEqual( - 'Error fetching exception list' - ); - expect(wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').exists()).toBeFalsy(); - }); - - it('it renders specific missing exceptions list error', () => { - const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - - ); - - expect( - wrapper.find('[data-test-subj="errorCalloutContainer"] .euiCallOutHeader__title').text() - ).toEqual('Error: not found (404)'); - expect(wrapper.find('[data-test-subj="errorCalloutMessage"]').at(0).text()).toEqual( - 'The associated exception list (some_uuid) no longer exists. Please remove the missing exception list to add additional exceptions to the detection rule.' - ); - expect(wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').exists()).toBeTruthy(); - }); - - it('it dissasociates list from rule when remove exception list clicked ', () => { - const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - - ); - - wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').at(0).simulate('click'); - - expect(mockDissasociate).toHaveBeenCalledWith([]); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx deleted file mode 100644 index a2419ef16df3ab..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx +++ /dev/null @@ -1,169 +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 React, { useMemo, useEffect, useState, useCallback } from 'react'; -import { - EuiButtonEmpty, - EuiAccordion, - EuiCodeBlock, - EuiButton, - EuiCallOut, - EuiText, - EuiSpacer, -} from '@elastic/eui'; - -import { HttpSetup } from '../../../../../../../src/core/public'; -import { List } from '../../../../common/detection_engine/schemas/types/lists'; -import { Rule } from '../../../detections/containers/detection_engine/rules/types'; -import * as i18n from './translations'; -import { useDissasociateExceptionList } from '../../../detections/containers/detection_engine/rules/use_dissasociate_exception_list'; - -export interface ErrorInfo { - reason: string | null; - code: number | null; - details: string | null; - listListId: string | null; -} - -export interface ErrorCalloutProps { - http: HttpSetup; - rule: Rule | null; - errorInfo: ErrorInfo; - onCancel: () => void; - onSuccess: (listId: string) => void; - onError: (arg: Error) => void; -} - -const ErrorCalloutComponent = ({ - http, - rule, - errorInfo, - onCancel, - onError, - onSuccess, -}: ErrorCalloutProps): JSX.Element => { - const [listToDelete, setListToDelete] = useState(null); - const [errorTitle, setErrorTitle] = useState(''); - const [errorMessage, setErrorMessage] = useState(i18n.ADD_EXCEPTION_FETCH_ERROR); - - const handleOnSuccess = useCallback((): void => { - onSuccess(listToDelete != null ? listToDelete.id : ''); - }, [onSuccess, listToDelete]); - - const [isDissasociatingList, handleDissasociateExceptionList] = useDissasociateExceptionList({ - http, - ruleRuleId: rule != null ? rule.rule_id : '', - onSuccess: handleOnSuccess, - onError, - }); - - const canDisplay404Actions = useMemo( - (): boolean => - errorInfo.code === 404 && - rule != null && - listToDelete != null && - handleDissasociateExceptionList != null, - [errorInfo.code, listToDelete, handleDissasociateExceptionList, rule] - ); - - useEffect((): void => { - // Yes, it's redundant, unfortunately typescript wasn't picking up - // that `listToDelete` is checked in canDisplay404Actions - if (canDisplay404Actions && listToDelete != null) { - setErrorMessage(i18n.ADD_EXCEPTION_FETCH_404_ERROR(listToDelete.id)); - } - - setErrorTitle(`${errorInfo.reason}${errorInfo.code != null ? ` (${errorInfo.code})` : ''}`); - }, [errorInfo.reason, errorInfo.code, listToDelete, canDisplay404Actions]); - - const handleDissasociateList = useCallback((): void => { - // Yes, it's redundant, unfortunately typescript wasn't picking up - // that `handleDissasociateExceptionList` and `list` are checked in - // canDisplay404Actions - if ( - canDisplay404Actions && - rule != null && - listToDelete != null && - handleDissasociateExceptionList != null - ) { - const exceptionLists = (rule.exceptions_list ?? []).filter( - ({ id }) => id !== listToDelete.id - ); - - handleDissasociateExceptionList(exceptionLists); - } - }, [handleDissasociateExceptionList, listToDelete, canDisplay404Actions, rule]); - - useEffect((): void => { - if (errorInfo.code === 404 && rule != null && rule.exceptions_list != null) { - const [listFound] = rule.exceptions_list.filter( - ({ id, list_id: listId }) => - (errorInfo.details != null && errorInfo.details.includes(id)) || - errorInfo.listListId === listId - ); - setListToDelete(listFound); - } - }, [rule, errorInfo.details, errorInfo.code, errorInfo.listListId]); - - return ( - - -

{errorMessage}

-
- - {listToDelete != null && ( - -

{i18n.MODAL_ERROR_ACCORDION_TEXT}

- - } - > - - {JSON.stringify(listToDelete)} - -
- )} - - - {i18n.CANCEL} - - {canDisplay404Actions && ( - - {i18n.CLEAR_EXCEPTIONS_LABEL} - - )} -
- ); -}; - -ErrorCalloutComponent.displayName = 'ErrorCalloutComponent'; - -export const ErrorCallout = React.memo(ErrorCalloutComponent); - -ErrorCallout.displayName = 'ErrorCallout'; 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 index 484a3d593026e1..13e9d0df549f8a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts @@ -190,52 +190,3 @@ export const TOTAL_ITEMS_FETCH_ERROR = i18n.translate( defaultMessage: 'Error getting exception item totals', } ); - -export const CLEAR_EXCEPTIONS_LABEL = i18n.translate( - 'xpack.securitySolution.exceptions.clearExceptionsLabel', - { - defaultMessage: 'Remove Exception List', - } -); - -export const ADD_EXCEPTION_FETCH_404_ERROR = (listId: string) => - i18n.translate('xpack.securitySolution.exceptions.fetch404Error', { - values: { listId }, - defaultMessage: - 'The associated exception list ({listId}) no longer exists. Please remove the missing exception list to add additional exceptions to the detection rule.', - }); - -export const ADD_EXCEPTION_FETCH_ERROR = i18n.translate( - 'xpack.securitySolution.exceptions.fetchError', - { - defaultMessage: 'Error fetching exception list', - } -); - -export const ERROR = i18n.translate('xpack.securitySolution.exceptions.errorLabel', { - defaultMessage: 'Error', -}); - -export const CANCEL = i18n.translate('xpack.securitySolution.exceptions.cancelLabel', { - defaultMessage: 'Cancel', -}); - -export const MODAL_ERROR_ACCORDION_TEXT = i18n.translate( - 'xpack.securitySolution.exceptions.modalErrorAccordionText', - { - defaultMessage: 'Show rule reference information:', - } -); - -export const DISSASOCIATE_LIST_SUCCESS = (id: string) => - i18n.translate('xpack.securitySolution.exceptions.dissasociateListSuccessText', { - values: { id }, - defaultMessage: 'Exception list ({id}) has successfully been removed', - }); - -export const DISSASOCIATE_EXCEPTION_LIST_ERROR = i18n.translate( - 'xpack.securitySolution.exceptions.dissasociateExceptionListError', - { - defaultMessage: 'Failed to remove exception list', - } -); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx index 46923e07d225ad..6611ee2385d108 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx @@ -148,50 +148,6 @@ describe('useAddOrUpdateException', () => { }); }); - it('invokes "onError" if call to add exception item fails', async () => { - const mockError = new Error('error adding item'); - - addExceptionListItem = jest - .spyOn(listsApi, 'addExceptionListItem') - .mockRejectedValue(mockError); - - await act(async () => { - const { rerender, result, waitForNextUpdate } = render(); - const addOrUpdateItems = await waitForAddOrUpdateFunc({ - rerender, - result, - waitForNextUpdate, - }); - if (addOrUpdateItems) { - addOrUpdateItems(...addOrUpdateItemsArgs); - } - await waitForNextUpdate(); - expect(onError).toHaveBeenCalledWith(mockError, null, null); - }); - }); - - it('invokes "onError" if call to update exception item fails', async () => { - const mockError = new Error('error updating item'); - - updateExceptionListItem = jest - .spyOn(listsApi, 'updateExceptionListItem') - .mockRejectedValue(mockError); - - await act(async () => { - const { rerender, result, waitForNextUpdate } = render(); - const addOrUpdateItems = await waitForAddOrUpdateFunc({ - rerender, - result, - waitForNextUpdate, - }); - if (addOrUpdateItems) { - addOrUpdateItems(...addOrUpdateItemsArgs); - } - await waitForNextUpdate(); - expect(onError).toHaveBeenCalledWith(mockError, null, null); - }); - }); - describe('when alertIdToClose is not passed in', () => { it('should not update the alert status', async () => { await act(async () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx index be289b0e85e66b..9d45a411b51302 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx @@ -42,7 +42,7 @@ export type ReturnUseAddOrUpdateException = [ export interface UseAddOrUpdateExceptionProps { http: HttpStart; - onError: (arg: Error, code: number | null, message: string | null) => void; + onError: (arg: Error) => void; onSuccess: () => void; } @@ -157,11 +157,7 @@ export const useAddOrUpdateException = ({ } catch (error) { if (isSubscribed) { setIsLoading(false); - if (error.body != null) { - onError(error, error.body.status_code, error.body.message); - } else { - onError(error, null, null); - } + onError(error); } } }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx index f20a58b9ffa36a..39d88bd8e4724d 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx @@ -379,7 +379,7 @@ describe('useFetchOrCreateRuleExceptionList', () => { await waitForNextUpdate(); await waitForNextUpdate(); expect(onError).toHaveBeenCalledTimes(1); - expect(onError).toHaveBeenCalledWith(error, null, null); + expect(onError).toHaveBeenCalledWith(error); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx index 944631d4e9fb5f..0d367e03a799f3 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx @@ -30,7 +30,7 @@ export interface UseFetchOrCreateRuleExceptionListProps { http: HttpStart; ruleId: Rule['id']; exceptionListType: ExceptionListSchema['type']; - onError: (arg: Error, code: number | null, message: string | null) => void; + onError: (arg: Error) => void; onSuccess?: (ruleWasChanged: boolean) => void; } @@ -179,11 +179,7 @@ export const useFetchOrCreateRuleExceptionList = ({ if (isSubscribed) { setIsLoading(false); setExceptionList(null); - if (error.body != null) { - onError(error, error.body.status_code, error.body.message); - } else { - onError(error, null, null); - } + onError(error); } } } 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 index c97895cdfe2363..7482068454a97c 100644 --- 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 @@ -322,13 +322,11 @@ const ExceptionsViewerComponent = ({ exceptionListTypeToEdit != null && ( )} diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx deleted file mode 100644 index 6b1938655dc33f..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx +++ /dev/null @@ -1,52 +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 { act, renderHook } from '@testing-library/react-hooks'; - -import { createKibanaCoreStartMock } from '../../../../common/mock/kibana_core'; - -import * as api from './api'; -import { ruleMock } from './mock'; -import { - ReturnUseDissasociateExceptionList, - UseDissasociateExceptionListProps, - useDissasociateExceptionList, -} from './use_dissasociate_exception_list'; - -const mockKibanaHttpService = createKibanaCoreStartMock().http; - -describe('useDissasociateExceptionList', () => { - const onError = jest.fn(); - const onSuccess = jest.fn(); - - beforeEach(() => { - jest.spyOn(api, 'patchRule').mockResolvedValue(ruleMock); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - test('initializes hook', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseDissasociateExceptionListProps, - ReturnUseDissasociateExceptionList - >(() => - useDissasociateExceptionList({ - http: mockKibanaHttpService, - ruleRuleId: 'rule_id', - onError, - onSuccess, - }) - ); - - await waitForNextUpdate(); - - expect(result.current).toEqual([false, null]); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx deleted file mode 100644 index dffba3e6e04368..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx +++ /dev/null @@ -1,80 +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 { useEffect, useState, useRef } from 'react'; - -import { HttpStart } from '../../../../../../../../src/core/public'; -import { List } from '../../../../../common/detection_engine/schemas/types/lists'; -import { patchRule } from './api'; - -type Func = (lists: List[]) => void; -export type ReturnUseDissasociateExceptionList = [boolean, Func | null]; - -export interface UseDissasociateExceptionListProps { - http: HttpStart; - ruleRuleId: string; - onError: (arg: Error) => void; - onSuccess: () => void; -} - -/** - * Hook for removing an exception list reference from a rule - * - * @param http Kibana http service - * @param ruleRuleId a rule_id (NOT id) - * @param onError error callback - * @param onSuccess success callback - * - */ -export const useDissasociateExceptionList = ({ - http, - ruleRuleId, - onError, - onSuccess, -}: UseDissasociateExceptionListProps): ReturnUseDissasociateExceptionList => { - const [isLoading, setLoading] = useState(false); - const dissasociateList = useRef(null); - - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - - const dissasociateListFromRule = (id: string) => async ( - exceptionLists: List[] - ): Promise => { - try { - if (isSubscribed) { - setLoading(true); - - await patchRule({ - ruleProperties: { - rule_id: id, - exceptions_list: exceptionLists, - }, - signal: abortCtrl.signal, - }); - - onSuccess(); - setLoading(false); - } - } catch (err) { - if (isSubscribed) { - setLoading(false); - onError(err); - } - } - }; - - dissasociateList.current = dissasociateListFromRule(ruleRuleId); - - return (): void => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, [http, ruleRuleId, onError, onSuccess]); - - return [isLoading, dissasociateList.current]; -}; From 5a9d227eee1b53673c6445c00746a0846bb69e48 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 26 Aug 2020 08:03:12 -0700 Subject: [PATCH 036/216] Downloads Chrome 84 and adds to PATH Signed-off-by: Tyler Smalley --- src/dev/ci_setup/setup_env.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 72ec73ad810e6d..a82ca011b8a5df 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -10,6 +10,7 @@ installNode=$1 dir="$(pwd)" cacheDir="$HOME/.kibana" +downloads="$cacheDir/downloads" RED='\033[0;31m' C_RESET='\033[0m' # Reset color @@ -133,6 +134,26 @@ export CYPRESS_DOWNLOAD_MIRROR="https://us-central1-elastic-kibana-184716.cloudf export CHECKS_REPORTER_ACTIVE=false +### +### Download Chrome and install to this shell +### + +# Available using the version information search at https://omahaproxy.appspot.com/ +chromeVersion=84 + +mkdir -p "$downloads" + +if [ -d $cacheDir/chrome-$chromeVersion/chrome-linux ]; then + echo " -- Chrome already downloaded and extracted" +else + mkdir -p "$cacheDir/chrome-$chromeVersion" + + echo " -- Downloading and extracting Chrome" + curl -o "$downloads/chrome.zip" -L "https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/chrome_$chromeVersion.zip" + unzip -o "$downloads/chrome.zip" -d "$cacheDir/chrome-$chromeVersion" + export PATH="$cacheDir/chrome-$chromeVersion/chrome-linux:$PATH" +fi + # This is mainly for release-manager builds, which run in an environment that doesn't have Chrome installed if [[ "$(which google-chrome-stable)" || "$(which google-chrome)" ]]; then echo "Chrome detected, setting DETECT_CHROMEDRIVER_VERSION=true" From 1ca76514933463220e32f4b246c5ba14a553d9a9 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 26 Aug 2020 09:28:22 -0700 Subject: [PATCH 037/216] Revert "Downloads Chrome 84 and adds to PATH" This reverts commit 5a9d227eee1b53673c6445c00746a0846bb69e48. --- src/dev/ci_setup/setup_env.sh | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index a82ca011b8a5df..72ec73ad810e6d 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -10,7 +10,6 @@ installNode=$1 dir="$(pwd)" cacheDir="$HOME/.kibana" -downloads="$cacheDir/downloads" RED='\033[0;31m' C_RESET='\033[0m' # Reset color @@ -134,26 +133,6 @@ export CYPRESS_DOWNLOAD_MIRROR="https://us-central1-elastic-kibana-184716.cloudf export CHECKS_REPORTER_ACTIVE=false -### -### Download Chrome and install to this shell -### - -# Available using the version information search at https://omahaproxy.appspot.com/ -chromeVersion=84 - -mkdir -p "$downloads" - -if [ -d $cacheDir/chrome-$chromeVersion/chrome-linux ]; then - echo " -- Chrome already downloaded and extracted" -else - mkdir -p "$cacheDir/chrome-$chromeVersion" - - echo " -- Downloading and extracting Chrome" - curl -o "$downloads/chrome.zip" -L "https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/chrome_$chromeVersion.zip" - unzip -o "$downloads/chrome.zip" -d "$cacheDir/chrome-$chromeVersion" - export PATH="$cacheDir/chrome-$chromeVersion/chrome-linux:$PATH" -fi - # This is mainly for release-manager builds, which run in an environment that doesn't have Chrome installed if [[ "$(which google-chrome-stable)" || "$(which google-chrome)" ]]; then echo "Chrome detected, setting DETECT_CHROMEDRIVER_VERSION=true" From 5447565f0b6b4bca90785268e5008fa9243869ea Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Wed, 26 Aug 2020 10:55:27 -0700 Subject: [PATCH 038/216] [Ingest Manager] Return ID when default output is found (#75930) * Return ID when default output is found * Fix typing --- x-pack/plugins/ingest_manager/server/services/output.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ingest_manager/server/services/output.ts b/x-pack/plugins/ingest_manager/server/services/output.ts index b4af2310243701..1e5632719fb72c 100644 --- a/x-pack/plugins/ingest_manager/server/services/output.ts +++ b/x-pack/plugins/ingest_manager/server/services/output.ts @@ -15,7 +15,7 @@ let cachedAdminUser: null | { username: string; password: string } = null; class OutputService { public async getDefaultOutput(soClient: SavedObjectsClientContract) { - return await soClient.find({ + return await soClient.find({ type: OUTPUT_SAVED_OBJECT_TYPE, searchFields: ['is_default'], search: 'true', @@ -42,6 +42,7 @@ class OutputService { } return { + id: outputs.saved_objects[0].id, ...outputs.saved_objects[0].attributes, }; } From 61550b7ce0ada786e6962caf5b8c91e37dd4cf31 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 26 Aug 2020 20:08:39 +0100 Subject: [PATCH 039/216] [ML] Adding authorization header to DFA job update request (#75899) --- x-pack/plugins/ml/server/routes/data_frame_analytics.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 3b964588bef199..75d48056cf4584 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -496,6 +496,7 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat const results = await legacyClient.callAsInternalUser('ml.updateDataFrameAnalytics', { body: request.body, analyticsId, + ...getAuthorizationHeader(request), }); return response.ok({ body: results, From eee139295d1d6edff2666be5af855e9370db16e7 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Wed, 26 Aug 2020 21:40:03 +0200 Subject: [PATCH 040/216] Migrate data folder creation from legacy to KP (#75527) * rename uuid service to environment service * adapt resolve_uuid to directly use the configurations * move data folder creation to core * update generated doc * fix types * fix monitoring tests * move instanceUuid to plugin initializer context * update generated doc --- .../kibana-plugin-core-server.coresetup.md | 1 - ...ibana-plugin-core-server.coresetup.uuid.md | 13 -- .../core/server/kibana-plugin-core-server.md | 1 - ...ore-server.plugininitializercontext.env.md | 1 + ...in-core-server.plugininitializercontext.md | 2 +- ...server.uuidservicesetup.getinstanceuuid.md | 17 --- ...ana-plugin-core-server.uuidservicesetup.md | 20 --- .../environment/create_data_folder.test.ts | 79 ++++++++++++ .../server/environment/create_data_folder.ts | 40 ++++++ .../environment_service.mock.ts} | 12 +- .../environment_service.test.ts} | 55 +++++++- .../environment_service.ts} | 28 ++-- src/core/server/{uuid => environment}/fs.ts | 1 + .../server/{uuid => environment}/index.ts | 2 +- .../resolve_uuid.test.ts | 67 +++++----- .../{uuid => environment}/resolve_uuid.ts | 18 +-- src/core/server/index.ts | 4 - src/core/server/internal_types.ts | 4 +- src/core/server/legacy/legacy_service.test.ts | 10 +- src/core/server/legacy/legacy_service.ts | 5 +- src/core/server/mocks.ts | 6 +- .../discovery/plugins_discovery.test.ts | 70 +++++++--- .../plugins/discovery/plugins_discovery.ts | 24 +++- .../integration_tests/plugins_service.test.ts | 4 +- src/core/server/plugins/plugin.test.ts | 122 +++++++++++++++--- .../server/plugins/plugin_context.test.ts | 26 +++- src/core/server/plugins/plugin_context.ts | 11 +- .../server/plugins/plugins_service.test.ts | 39 +++--- src/core/server/plugins/plugins_service.ts | 12 +- src/core/server/plugins/types.ts | 1 + src/core/server/server.api.md | 8 +- src/core/server/server.test.mocks.ts | 8 +- src/core/server/server.ts | 15 ++- src/legacy/core_plugins/kibana/index.js | 17 --- x-pack/plugins/event_log/server/plugin.ts | 2 +- .../plugins/monitoring/server/plugin.test.ts | 3 - x-pack/plugins/monitoring/server/plugin.ts | 4 +- .../plugins/reporting/server/config/config.ts | 2 +- x-pack/plugins/task_manager/server/plugin.ts | 2 +- 39 files changed, 504 insertions(+), 252 deletions(-) delete mode 100644 docs/development/core/server/kibana-plugin-core-server.coresetup.uuid.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.uuidservicesetup.getinstanceuuid.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.uuidservicesetup.md create mode 100644 src/core/server/environment/create_data_folder.test.ts create mode 100644 src/core/server/environment/create_data_folder.ts rename src/core/server/{uuid/uuid_service.mock.ts => environment/environment_service.mock.ts} (74%) rename src/core/server/{uuid/uuid_service.test.ts => environment/environment_service.test.ts} (52%) rename src/core/server/{uuid/uuid_service.ts => environment/environment_service.ts} (65%) rename src/core/server/{uuid => environment}/fs.ts (95%) rename src/core/server/{uuid => environment}/index.ts (89%) rename src/core/server/{uuid => environment}/resolve_uuid.test.ts (81%) rename src/core/server/{uuid => environment}/resolve_uuid.ts (88%) diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md index 597bb9bc2376a0..ccc73d4fb858ec 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -26,5 +26,4 @@ export interface CoreSetupSavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | | [status](./kibana-plugin-core-server.coresetup.status.md) | StatusServiceSetup | [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) | | [uiSettings](./kibana-plugin-core-server.coresetup.uisettings.md) | UiSettingsServiceSetup | [UiSettingsServiceSetup](./kibana-plugin-core-server.uisettingsservicesetup.md) | -| [uuid](./kibana-plugin-core-server.coresetup.uuid.md) | UuidServiceSetup | [UuidServiceSetup](./kibana-plugin-core-server.uuidservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.uuid.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.uuid.md deleted file mode 100644 index c709c74497bd0f..00000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.uuid.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreSetup](./kibana-plugin-core-server.coresetup.md) > [uuid](./kibana-plugin-core-server.coresetup.uuid.md) - -## CoreSetup.uuid property - -[UuidServiceSetup](./kibana-plugin-core-server.uuidservicesetup.md) - -Signature: - -```typescript -uuid: UuidServiceSetup; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 98d7b0610abeab..e9bc19e9c92a98 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -214,7 +214,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [UiSettingsServiceStart](./kibana-plugin-core-server.uisettingsservicestart.md) | | | [URLMeaningfulParts](./kibana-plugin-core-server.urlmeaningfulparts.md) | We define our own typings because the current version of @types/node declares properties to be optional "hostname?: string". Although, parse call returns "hostname: null \| string". | | [UserProvidedValues](./kibana-plugin-core-server.userprovidedvalues.md) | Describes the values explicitly set by user. | -| [UuidServiceSetup](./kibana-plugin-core-server.uuidservicesetup.md) | APIs to access the application's instance uuid. | ## Variables diff --git a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.env.md b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.env.md index 4d111c8f20887f..76e4f222f02285 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.env.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.env.md @@ -10,5 +10,6 @@ env: { mode: EnvironmentMode; packageInfo: Readonly; + instanceUuid: string; }; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md index 0d7fcf3b10bca3..18760170afa1f2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md @@ -17,7 +17,7 @@ export interface PluginInitializerContext | Property | Type | Description | | --- | --- | --- | | [config](./kibana-plugin-core-server.plugininitializercontext.config.md) | {
legacy: {
globalConfig$: Observable<SharedGlobalConfig>;
};
create: <T = ConfigSchema>() => Observable<T>;
createIfExists: <T = ConfigSchema>() => Observable<T | undefined>;
} | | -| [env](./kibana-plugin-core-server.plugininitializercontext.env.md) | {
mode: EnvironmentMode;
packageInfo: Readonly<PackageInfo>;
} | | +| [env](./kibana-plugin-core-server.plugininitializercontext.env.md) | {
mode: EnvironmentMode;
packageInfo: Readonly<PackageInfo>;
instanceUuid: string;
} | | | [logger](./kibana-plugin-core-server.plugininitializercontext.logger.md) | LoggerFactory | | | [opaqueId](./kibana-plugin-core-server.plugininitializercontext.opaqueid.md) | PluginOpaqueId | | diff --git a/docs/development/core/server/kibana-plugin-core-server.uuidservicesetup.getinstanceuuid.md b/docs/development/core/server/kibana-plugin-core-server.uuidservicesetup.getinstanceuuid.md deleted file mode 100644 index f33176a32954d4..00000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.uuidservicesetup.getinstanceuuid.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UuidServiceSetup](./kibana-plugin-core-server.uuidservicesetup.md) > [getInstanceUuid](./kibana-plugin-core-server.uuidservicesetup.getinstanceuuid.md) - -## UuidServiceSetup.getInstanceUuid() method - -Retrieve the Kibana instance uuid. - -Signature: - -```typescript -getInstanceUuid(): string; -``` -Returns: - -`string` - diff --git a/docs/development/core/server/kibana-plugin-core-server.uuidservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.uuidservicesetup.md deleted file mode 100644 index 99ce4cb08af479..00000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.uuidservicesetup.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UuidServiceSetup](./kibana-plugin-core-server.uuidservicesetup.md) - -## UuidServiceSetup interface - -APIs to access the application's instance uuid. - -Signature: - -```typescript -export interface UuidServiceSetup -``` - -## Methods - -| Method | Description | -| --- | --- | -| [getInstanceUuid()](./kibana-plugin-core-server.uuidservicesetup.getinstanceuuid.md) | Retrieve the Kibana instance uuid. | - diff --git a/src/core/server/environment/create_data_folder.test.ts b/src/core/server/environment/create_data_folder.test.ts new file mode 100644 index 00000000000000..2a480a7a3954f4 --- /dev/null +++ b/src/core/server/environment/create_data_folder.test.ts @@ -0,0 +1,79 @@ +/* + * 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 { PathConfigType } from '../path'; +import { createDataFolder } from './create_data_folder'; +import { mkdir } from './fs'; +import { loggingSystemMock } from '../logging/logging_system.mock'; + +jest.mock('./fs', () => ({ + mkdir: jest.fn(() => Promise.resolve('')), +})); + +const mkdirMock = mkdir as jest.Mock; + +describe('createDataFolder', () => { + let logger: ReturnType; + let pathConfig: PathConfigType; + + beforeEach(() => { + logger = loggingSystemMock.createLogger(); + pathConfig = { + data: '/path/to/data/folder', + }; + mkdirMock.mockResolvedValue(undefined); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('calls `mkdir` with the correct parameters', async () => { + await createDataFolder({ pathConfig, logger }); + expect(mkdirMock).toHaveBeenCalledTimes(1); + expect(mkdirMock).toHaveBeenCalledWith(pathConfig.data, { recursive: true }); + }); + + it('does not log error if the `mkdir` call is successful', async () => { + await createDataFolder({ pathConfig, logger }); + expect(logger.error).not.toHaveBeenCalled(); + }); + + it('throws an error if the `mkdir` call fails', async () => { + mkdirMock.mockRejectedValue('some-error'); + await expect(() => createDataFolder({ pathConfig, logger })).rejects.toMatchInlineSnapshot( + `"some-error"` + ); + }); + + it('logs an error message if the `mkdir` call fails', async () => { + mkdirMock.mockRejectedValue('some-error'); + try { + await createDataFolder({ pathConfig, logger }); + } catch (e) { + /* trap */ + } + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Error trying to create data folder at /path/to/data/folder: some-error", + ] + `); + }); +}); diff --git a/src/core/server/environment/create_data_folder.ts b/src/core/server/environment/create_data_folder.ts new file mode 100644 index 00000000000000..641d95cbf94117 --- /dev/null +++ b/src/core/server/environment/create_data_folder.ts @@ -0,0 +1,40 @@ +/* + * 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 { mkdir } from './fs'; +import { Logger } from '../logging'; +import { PathConfigType } from '../path'; + +export async function createDataFolder({ + pathConfig, + logger, +}: { + pathConfig: PathConfigType; + logger: Logger; +}): Promise { + const dataFolder = pathConfig.data; + try { + // Create the data directory (recursively, if the a parent dir doesn't exist). + // If it already exists, does nothing. + await mkdir(dataFolder, { recursive: true }); + } catch (e) { + logger.error(`Error trying to create data folder at ${dataFolder}: ${e}`); + throw e; + } +} diff --git a/src/core/server/uuid/uuid_service.mock.ts b/src/core/server/environment/environment_service.mock.ts similarity index 74% rename from src/core/server/uuid/uuid_service.mock.ts rename to src/core/server/environment/environment_service.mock.ts index bf40eaee206365..8bf726b4a6388b 100644 --- a/src/core/server/uuid/uuid_service.mock.ts +++ b/src/core/server/environment/environment_service.mock.ts @@ -17,25 +17,25 @@ * under the License. */ -import { UuidService, UuidServiceSetup } from './uuid_service'; +import { EnvironmentService, InternalEnvironmentServiceSetup } from './environment_service'; const createSetupContractMock = () => { - const setupContract: jest.Mocked = { - getInstanceUuid: jest.fn().mockImplementation(() => 'uuid'), + const setupContract: jest.Mocked = { + instanceUuid: 'uuid', }; return setupContract; }; -type UuidServiceContract = PublicMethodsOf; +type EnvironmentServiceContract = PublicMethodsOf; const createMock = () => { - const mocked: jest.Mocked = { + const mocked: jest.Mocked = { setup: jest.fn(), }; mocked.setup.mockResolvedValue(createSetupContractMock()); return mocked; }; -export const uuidServiceMock = { +export const environmentServiceMock = { create: createMock, createSetupContract: createSetupContractMock, }; diff --git a/src/core/server/uuid/uuid_service.test.ts b/src/core/server/environment/environment_service.test.ts similarity index 52% rename from src/core/server/uuid/uuid_service.test.ts rename to src/core/server/environment/environment_service.test.ts index 3b1087d72c677a..06fd250ebe4f93 100644 --- a/src/core/server/uuid/uuid_service.test.ts +++ b/src/core/server/environment/environment_service.test.ts @@ -17,10 +17,13 @@ * under the License. */ -import { UuidService } from './uuid_service'; +import { BehaviorSubject } from 'rxjs'; +import { EnvironmentService } from './environment_service'; import { resolveInstanceUuid } from './resolve_uuid'; +import { createDataFolder } from './create_data_folder'; import { CoreContext } from '../core_context'; +import { configServiceMock } from '../config/config_service.mock'; import { loggingSystemMock } from '../logging/logging_system.mock'; import { mockCoreContext } from '../core_context.mock'; @@ -28,31 +31,69 @@ jest.mock('./resolve_uuid', () => ({ resolveInstanceUuid: jest.fn().mockResolvedValue('SOME_UUID'), })); +jest.mock('./create_data_folder', () => ({ + createDataFolder: jest.fn(), +})); + +const pathConfig = { + data: 'data-folder', +}; +const serverConfig = { + uuid: 'SOME_UUID', +}; + +const getConfigService = () => { + const configService = configServiceMock.create(); + configService.atPath.mockImplementation((path) => { + if (path === 'path') { + return new BehaviorSubject(pathConfig); + } + if (path === 'server') { + return new BehaviorSubject(serverConfig); + } + return new BehaviorSubject({}); + }); + return configService; +}; + describe('UuidService', () => { let logger: ReturnType; + let configService: ReturnType; let coreContext: CoreContext; beforeEach(() => { jest.clearAllMocks(); logger = loggingSystemMock.create(); - coreContext = mockCoreContext.create({ logger }); + configService = getConfigService(); + coreContext = mockCoreContext.create({ logger, configService }); }); describe('#setup()', () => { - it('calls resolveInstanceUuid with core configuration service', async () => { - const service = new UuidService(coreContext); + it('calls resolveInstanceUuid with correct parameters', async () => { + const service = new EnvironmentService(coreContext); await service.setup(); expect(resolveInstanceUuid).toHaveBeenCalledTimes(1); expect(resolveInstanceUuid).toHaveBeenCalledWith({ - configService: coreContext.configService, + pathConfig, + serverConfig, + logger: logger.get('uuid'), + }); + }); + + it('calls createDataFolder with correct parameters', async () => { + const service = new EnvironmentService(coreContext); + await service.setup(); + expect(createDataFolder).toHaveBeenCalledTimes(1); + expect(createDataFolder).toHaveBeenCalledWith({ + pathConfig, logger: logger.get('uuid'), }); }); it('returns the uuid resolved from resolveInstanceUuid', async () => { - const service = new UuidService(coreContext); + const service = new EnvironmentService(coreContext); const setup = await service.setup(); - expect(setup.getInstanceUuid()).toEqual('SOME_UUID'); + expect(setup.instanceUuid).toEqual('SOME_UUID'); }); }); }); diff --git a/src/core/server/uuid/uuid_service.ts b/src/core/server/environment/environment_service.ts similarity index 65% rename from src/core/server/uuid/uuid_service.ts rename to src/core/server/environment/environment_service.ts index d7c1b3331c4479..6a0b1122c70534 100644 --- a/src/core/server/uuid/uuid_service.ts +++ b/src/core/server/environment/environment_service.ts @@ -17,25 +17,27 @@ * under the License. */ -import { resolveInstanceUuid } from './resolve_uuid'; +import { take } from 'rxjs/operators'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { IConfigService } from '../config'; +import { PathConfigType, config as pathConfigDef } from '../path'; +import { HttpConfigType, config as httpConfigDef } from '../http'; +import { resolveInstanceUuid } from './resolve_uuid'; +import { createDataFolder } from './create_data_folder'; /** - * APIs to access the application's instance uuid. - * - * @public + * @internal */ -export interface UuidServiceSetup { +export interface InternalEnvironmentServiceSetup { /** * Retrieve the Kibana instance uuid. */ - getInstanceUuid(): string; + instanceUuid: string; } /** @internal */ -export class UuidService { +export class EnvironmentService { private readonly log: Logger; private readonly configService: IConfigService; private uuid: string = ''; @@ -46,13 +48,21 @@ export class UuidService { } public async setup() { + const [pathConfig, serverConfig] = await Promise.all([ + this.configService.atPath(pathConfigDef.path).pipe(take(1)).toPromise(), + this.configService.atPath(httpConfigDef.path).pipe(take(1)).toPromise(), + ]); + + await createDataFolder({ pathConfig, logger: this.log }); + this.uuid = await resolveInstanceUuid({ - configService: this.configService, + pathConfig, + serverConfig, logger: this.log, }); return { - getInstanceUuid: () => this.uuid, + instanceUuid: this.uuid, }; } } diff --git a/src/core/server/uuid/fs.ts b/src/core/server/environment/fs.ts similarity index 95% rename from src/core/server/uuid/fs.ts rename to src/core/server/environment/fs.ts index f10d6370c09d15..dc040ccb736159 100644 --- a/src/core/server/uuid/fs.ts +++ b/src/core/server/environment/fs.ts @@ -22,3 +22,4 @@ import { promisify } from 'util'; export const readFile = promisify(Fs.readFile); export const writeFile = promisify(Fs.writeFile); +export const mkdir = promisify(Fs.mkdir); diff --git a/src/core/server/uuid/index.ts b/src/core/server/environment/index.ts similarity index 89% rename from src/core/server/uuid/index.ts rename to src/core/server/environment/index.ts index ad57041a124c99..57a26d5ea3c797 100644 --- a/src/core/server/uuid/index.ts +++ b/src/core/server/environment/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { UuidService, UuidServiceSetup } from './uuid_service'; +export { EnvironmentService, InternalEnvironmentServiceSetup } from './environment_service'; diff --git a/src/core/server/uuid/resolve_uuid.test.ts b/src/core/server/environment/resolve_uuid.test.ts similarity index 81% rename from src/core/server/uuid/resolve_uuid.test.ts rename to src/core/server/environment/resolve_uuid.test.ts index 3132f639e536f9..d162c9d8e364b7 100644 --- a/src/core/server/uuid/resolve_uuid.test.ts +++ b/src/core/server/environment/resolve_uuid.test.ts @@ -18,12 +18,11 @@ */ import { join } from 'path'; +import { loggingSystemMock } from '../logging/logging_system.mock'; import { readFile, writeFile } from './fs'; import { resolveInstanceUuid, UUID_7_6_0_BUG } from './resolve_uuid'; -import { configServiceMock } from '../config/config_service.mock'; -import { loggingSystemMock } from '../logging/logging_system.mock'; -import { BehaviorSubject } from 'rxjs'; -import { Logger } from '../logging'; +import { PathConfigType } from '../path'; +import { HttpConfigType } from '../http'; jest.mock('uuid', () => ({ v4: () => 'NEW_UUID', @@ -66,40 +65,34 @@ const mockWriteFile = (error?: object) => { }); }; -const getConfigService = (serverUuid: string | undefined) => { - const configService = configServiceMock.create(); - configService.atPath.mockImplementation((path) => { - if (path === 'path') { - return new BehaviorSubject({ - data: 'data-folder', - }); - } - if (path === 'server') { - return new BehaviorSubject({ - uuid: serverUuid, - }); - } - return new BehaviorSubject({}); - }); - return configService; +const createServerConfig = (serverUuid: string | undefined) => { + return { + uuid: serverUuid, + } as HttpConfigType; }; describe('resolveInstanceUuid', () => { - let configService: ReturnType; - let logger: jest.Mocked; + let logger: ReturnType; + let pathConfig: PathConfigType; + let serverConfig: HttpConfigType; beforeEach(() => { jest.clearAllMocks(); mockReadFile({ uuid: DEFAULT_FILE_UUID }); mockWriteFile(); - configService = getConfigService(DEFAULT_CONFIG_UUID); - logger = loggingSystemMock.create().get() as any; + + pathConfig = { + data: 'data-folder', + }; + serverConfig = createServerConfig(DEFAULT_CONFIG_UUID); + + logger = loggingSystemMock.createLogger(); }); describe('when file is present and config property is set', () => { describe('when they mismatch', () => { it('writes to file and returns the config uuid', async () => { - const uuid = await resolveInstanceUuid({ configService, logger }); + const uuid = await resolveInstanceUuid({ pathConfig, serverConfig, logger }); expect(uuid).toEqual(DEFAULT_CONFIG_UUID); expect(writeFile).toHaveBeenCalledWith( join('data-folder', 'uuid'), @@ -118,7 +111,7 @@ describe('resolveInstanceUuid', () => { describe('when they match', () => { it('does not write to file', async () => { mockReadFile({ uuid: DEFAULT_CONFIG_UUID }); - const uuid = await resolveInstanceUuid({ configService, logger }); + const uuid = await resolveInstanceUuid({ pathConfig, serverConfig, logger }); expect(uuid).toEqual(DEFAULT_CONFIG_UUID); expect(writeFile).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledTimes(1); @@ -134,7 +127,7 @@ describe('resolveInstanceUuid', () => { describe('when file is not present and config property is set', () => { it('writes the uuid to file and returns the config uuid', async () => { mockReadFile({ error: fileNotFoundError }); - const uuid = await resolveInstanceUuid({ configService, logger }); + const uuid = await resolveInstanceUuid({ pathConfig, serverConfig, logger }); expect(uuid).toEqual(DEFAULT_CONFIG_UUID); expect(writeFile).toHaveBeenCalledWith( join('data-folder', 'uuid'), @@ -152,8 +145,8 @@ describe('resolveInstanceUuid', () => { describe('when file is present and config property is not set', () => { it('does not write to file and returns the file uuid', async () => { - configService = getConfigService(undefined); - const uuid = await resolveInstanceUuid({ configService, logger }); + serverConfig = createServerConfig(undefined); + const uuid = await resolveInstanceUuid({ pathConfig, serverConfig, logger }); expect(uuid).toEqual(DEFAULT_FILE_UUID); expect(writeFile).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledTimes(1); @@ -169,8 +162,8 @@ describe('resolveInstanceUuid', () => { describe('when config property is not set', () => { it('writes new uuid to file and returns new uuid', async () => { mockReadFile({ uuid: UUID_7_6_0_BUG }); - configService = getConfigService(undefined); - const uuid = await resolveInstanceUuid({ configService, logger }); + serverConfig = createServerConfig(undefined); + const uuid = await resolveInstanceUuid({ pathConfig, serverConfig, logger }); expect(uuid).not.toEqual(UUID_7_6_0_BUG); expect(uuid).toEqual('NEW_UUID'); expect(writeFile).toHaveBeenCalledWith( @@ -195,8 +188,8 @@ describe('resolveInstanceUuid', () => { describe('when config property is set', () => { it('writes config uuid to file and returns config uuid', async () => { mockReadFile({ uuid: UUID_7_6_0_BUG }); - configService = getConfigService(DEFAULT_CONFIG_UUID); - const uuid = await resolveInstanceUuid({ configService, logger }); + serverConfig = createServerConfig(DEFAULT_CONFIG_UUID); + const uuid = await resolveInstanceUuid({ pathConfig, serverConfig, logger }); expect(uuid).not.toEqual(UUID_7_6_0_BUG); expect(uuid).toEqual(DEFAULT_CONFIG_UUID); expect(writeFile).toHaveBeenCalledWith( @@ -221,9 +214,9 @@ describe('resolveInstanceUuid', () => { describe('when file is not present and config property is not set', () => { it('generates a new uuid and write it to file', async () => { - configService = getConfigService(undefined); + serverConfig = createServerConfig(undefined); mockReadFile({ error: fileNotFoundError }); - const uuid = await resolveInstanceUuid({ configService, logger }); + const uuid = await resolveInstanceUuid({ pathConfig, serverConfig, logger }); expect(uuid).toEqual('NEW_UUID'); expect(writeFile).toHaveBeenCalledWith( join('data-folder', 'uuid'), @@ -243,7 +236,7 @@ describe('resolveInstanceUuid', () => { it('throws an explicit error for file read errors', async () => { mockReadFile({ error: permissionError }); await expect( - resolveInstanceUuid({ configService, logger }) + resolveInstanceUuid({ pathConfig, serverConfig, logger }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Unable to read Kibana UUID file, please check the uuid.server configuration value in kibana.yml and ensure Kibana has sufficient permissions to read / write to this file. Error was: EACCES"` ); @@ -251,7 +244,7 @@ describe('resolveInstanceUuid', () => { it('throws an explicit error for file write errors', async () => { mockWriteFile(isDirectoryError); await expect( - resolveInstanceUuid({ configService, logger }) + resolveInstanceUuid({ pathConfig, serverConfig, logger }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Unable to write Kibana UUID file, please check the uuid.server configuration value in kibana.yml and ensure Kibana has sufficient permissions to read / write to this file. Error was: EISDIR"` ); diff --git a/src/core/server/uuid/resolve_uuid.ts b/src/core/server/environment/resolve_uuid.ts similarity index 88% rename from src/core/server/uuid/resolve_uuid.ts rename to src/core/server/environment/resolve_uuid.ts index 36f0eb73b1de7b..0267e069399972 100644 --- a/src/core/server/uuid/resolve_uuid.ts +++ b/src/core/server/environment/resolve_uuid.ts @@ -19,11 +19,9 @@ import uuid from 'uuid'; import { join } from 'path'; -import { take } from 'rxjs/operators'; import { readFile, writeFile } from './fs'; -import { IConfigService } from '../config'; -import { PathConfigType, config as pathConfigDef } from '../path'; -import { HttpConfigType, config as httpConfigDef } from '../http'; +import { PathConfigType } from '../path'; +import { HttpConfigType } from '../http'; import { Logger } from '../logging'; const FILE_ENCODING = 'utf8'; @@ -35,19 +33,15 @@ const FILE_NAME = 'uuid'; export const UUID_7_6_0_BUG = `ce42b997-a913-4d58-be46-bb1937feedd6`; export async function resolveInstanceUuid({ - configService, + pathConfig, + serverConfig, logger, }: { - configService: IConfigService; + pathConfig: PathConfigType; + serverConfig: HttpConfigType; logger: Logger; }): Promise { - const [pathConfig, serverConfig] = await Promise.all([ - configService.atPath(pathConfigDef.path).pipe(take(1)).toPromise(), - configService.atPath(httpConfigDef.path).pipe(take(1)).toPromise(), - ]); - const uuidFilePath = join(pathConfig.data, FILE_NAME); - const uuidFromFile = await readUuidFromFile(uuidFilePath, logger); const uuidFromConfig = serverConfig.uuid; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 76bcf5f7df665b..5c91d5a8c73ed6 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -60,7 +60,6 @@ import { SavedObjectsServiceStart, } from './saved_objects'; import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; -import { UuidServiceSetup } from './uuid'; import { MetricsServiceStart } from './metrics'; import { StatusServiceSetup } from './status'; import { Auditor, AuditTrailSetup, AuditTrailStart } from './audit_trail'; @@ -432,8 +431,6 @@ export interface CoreSetup; /** {@link AuditTrailSetup} */ @@ -483,7 +480,6 @@ export { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId, - UuidServiceSetup, AuditTrailStart, }; diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index 4f4bf50f07b8e1..6780ca6b59f4d7 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -32,7 +32,7 @@ import { InternalSavedObjectsServiceStart, } from './saved_objects'; import { InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart } from './ui_settings'; -import { UuidServiceSetup } from './uuid'; +import { InternalEnvironmentServiceSetup } from './environment'; import { InternalMetricsServiceStart } from './metrics'; import { InternalRenderingServiceSetup } from './rendering'; import { InternalHttpResourcesSetup } from './http_resources'; @@ -49,7 +49,7 @@ export interface InternalCoreSetup { savedObjects: InternalSavedObjectsServiceSetup; status: InternalStatusServiceSetup; uiSettings: InternalUiSettingsServiceSetup; - uuid: UuidServiceSetup; + environment: InternalEnvironmentServiceSetup; rendering: InternalRenderingServiceSetup; httpResources: InternalHttpResourcesSetup; auditTrail: AuditTrailSetup; diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index f8f04c59766b38..45869fd12d2b4b 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -45,7 +45,7 @@ import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service. import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock'; import { httpResourcesMock } from '../http_resources/http_resources_service.mock'; import { setupMock as renderingServiceMock } from '../rendering/__mocks__/rendering_service'; -import { uuidServiceMock } from '../uuid/uuid_service.mock'; +import { environmentServiceMock } from '../environment/environment_service.mock'; import { findLegacyPluginSpecs } from './plugins'; import { LegacyVars, LegacyServiceSetupDeps, LegacyServiceStartDeps } from './types'; import { LegacyService } from './legacy_service'; @@ -66,13 +66,13 @@ let startDeps: LegacyServiceStartDeps; const logger = loggingSystemMock.create(); let configService: ReturnType; -let uuidSetup: ReturnType; +let environmentSetup: ReturnType; beforeEach(() => { coreId = Symbol(); env = Env.createDefault(getEnvOptions()); configService = configServiceMock.create(); - uuidSetup = uuidServiceMock.createSetupContract(); + environmentSetup = environmentServiceMock.createSetupContract(); findLegacyPluginSpecsMock.mockClear(); MockKbnServer.prototype.ready = jest.fn().mockReturnValue(Promise.resolve()); @@ -97,7 +97,7 @@ beforeEach(() => { contracts: new Map([['plugin-id', 'plugin-value']]), }, rendering: renderingServiceMock, - uuid: uuidSetup, + environment: environmentSetup, status: statusServiceMock.createInternalSetupContract(), auditTrail: auditTrailServiceMock.createSetupContract(), logging: loggingServiceMock.createInternalSetupContract(), @@ -523,7 +523,7 @@ test('Sets the server.uuid property on the legacy configuration', async () => { configService: configService as any, }); - uuidSetup.getInstanceUuid.mockImplementation(() => 'UUID_FROM_SERVICE'); + environmentSetup.instanceUuid = 'UUID_FROM_SERVICE'; const configSetMock = jest.fn(); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index f39282a6f9cb0c..adfdecdd7c9761 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -188,7 +188,7 @@ export class LegacyService implements CoreService { } // propagate the instance uuid to the legacy config, as it was the legacy way to access it. - this.legacyRawConfig!.set('server.uuid', setupDeps.core.uuid.getInstanceUuid()); + this.legacyRawConfig!.set('server.uuid', setupDeps.core.environment.instanceUuid); this.setupDeps = setupDeps; this.legacyInternals = new LegacyInternals( this.legacyPlugins.uiExports, @@ -327,9 +327,6 @@ export class LegacyService implements CoreService { uiSettings: { register: setupDeps.core.uiSettings.register, }, - uuid: { - getInstanceUuid: setupDeps.core.uuid.getInstanceUuid, - }, auditTrail: setupDeps.core.auditTrail, getStartServices: () => Promise.resolve([coreStart, startDeps.plugins, {}]), }; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index bf9dcc4abe01c1..3c79706422cd43 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -33,7 +33,7 @@ import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { SharedGlobalConfig } from './plugins'; import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; import { metricsServiceMock } from './metrics/metrics_service.mock'; -import { uuidServiceMock } from './uuid/uuid_service.mock'; +import { environmentServiceMock } from './environment/environment_service.mock'; import { statusServiceMock } from './status/status_service.mock'; import { auditTrailServiceMock } from './audit_trail/audit_trail_service.mock'; @@ -94,6 +94,7 @@ function pluginInitializerContextMock(config: T = {} as T) { buildSha: 'buildSha', dist: false, }, + instanceUuid: 'instance-uuid', }, config: pluginInitializerContextConfigMock(config), }; @@ -130,7 +131,6 @@ function createCoreSetupMock({ savedObjects: savedObjectsServiceMock.createInternalSetupContract(), status: statusServiceMock.createSetupContract(), uiSettings: uiSettingsMock, - uuid: uuidServiceMock.createSetupContract(), auditTrail: auditTrailServiceMock.createSetupContract(), logging: loggingServiceMock.createSetupContract(), getStartServices: jest @@ -163,7 +163,7 @@ function createInternalCoreSetupMock() { http: httpServiceMock.createInternalSetupContract(), savedObjects: savedObjectsServiceMock.createInternalSetupContract(), status: statusServiceMock.createInternalSetupContract(), - uuid: uuidServiceMock.createSetupContract(), + environment: environmentServiceMock.createSetupContract(), httpResources: httpResourcesMock.createSetupContract(), rendering: renderingMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), diff --git a/src/core/server/plugins/discovery/plugins_discovery.test.ts b/src/core/server/plugins/discovery/plugins_discovery.test.ts index 70413757de9da2..4894f19e38df42 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.test.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.test.ts @@ -26,6 +26,7 @@ import { resolve } from 'path'; import { ConfigService, Env } from '../../config'; import { getEnvOptions } from '../../config/__mocks__/env'; import { PluginsConfig, PluginsConfigType, config } from '../plugins_config'; +import type { InstanceInfo } from '../plugin_context'; import { discover } from './plugins_discovery'; import { rawConfigServiceMock } from '../../config/raw_config_service.mock'; import { CoreContext } from '../../core_context'; @@ -77,6 +78,7 @@ const manifestPath = (...pluginPath: string[]) => describe('plugins discovery system', () => { let logger: ReturnType; + let instanceInfo: InstanceInfo; let env: Env; let configService: ConfigService; let pluginConfig: PluginsConfigType; @@ -87,6 +89,10 @@ describe('plugins discovery system', () => { mockPackage.raw = packageMock; + instanceInfo = { + uuid: 'instance-uuid', + }; + env = Env.createDefault( getEnvOptions({ cliArgs: { envName: 'development' }, @@ -127,7 +133,7 @@ describe('plugins discovery system', () => { }); it('discovers plugins in the search locations', async () => { - const { plugin$ } = discover(new PluginsConfig(pluginConfig, env), coreContext); + const { plugin$ } = discover(new PluginsConfig(pluginConfig, env), coreContext, instanceInfo); mockFs( { @@ -146,7 +152,11 @@ describe('plugins discovery system', () => { }); it('return errors when the manifest is invalid or incompatible', async () => { - const { plugin$, error$ } = discover(new PluginsConfig(pluginConfig, env), coreContext); + const { plugin$, error$ } = discover( + new PluginsConfig(pluginConfig, env), + coreContext, + instanceInfo + ); mockFs( { @@ -184,7 +194,11 @@ describe('plugins discovery system', () => { }); it('return errors when the plugin search path is not accessible', async () => { - const { plugin$, error$ } = discover(new PluginsConfig(pluginConfig, env), coreContext); + const { plugin$, error$ } = discover( + new PluginsConfig(pluginConfig, env), + coreContext, + instanceInfo + ); mockFs( { @@ -219,7 +233,11 @@ describe('plugins discovery system', () => { }); it('return an error when the manifest file is not accessible', async () => { - const { plugin$, error$ } = discover(new PluginsConfig(pluginConfig, env), coreContext); + const { plugin$, error$ } = discover( + new PluginsConfig(pluginConfig, env), + coreContext, + instanceInfo + ); mockFs( { @@ -250,7 +268,11 @@ describe('plugins discovery system', () => { }); it('discovers plugins in nested directories', async () => { - const { plugin$, error$ } = discover(new PluginsConfig(pluginConfig, env), coreContext); + const { plugin$, error$ } = discover( + new PluginsConfig(pluginConfig, env), + coreContext, + instanceInfo + ); mockFs( { @@ -287,7 +309,7 @@ describe('plugins discovery system', () => { }); it('does not discover plugins nested inside another plugin', async () => { - const { plugin$ } = discover(new PluginsConfig(pluginConfig, env), coreContext); + const { plugin$ } = discover(new PluginsConfig(pluginConfig, env), coreContext, instanceInfo); mockFs( { @@ -306,7 +328,7 @@ describe('plugins discovery system', () => { }); it('stops scanning when reaching `maxDepth`', async () => { - const { plugin$ } = discover(new PluginsConfig(pluginConfig, env), coreContext); + const { plugin$ } = discover(new PluginsConfig(pluginConfig, env), coreContext, instanceInfo); mockFs( { @@ -332,7 +354,7 @@ describe('plugins discovery system', () => { }); it('works with symlinks', async () => { - const { plugin$ } = discover(new PluginsConfig(pluginConfig, env), coreContext); + const { plugin$ } = discover(new PluginsConfig(pluginConfig, env), coreContext, instanceInfo); const pluginFolder = resolve(KIBANA_ROOT, '..', 'ext-plugins'); @@ -365,12 +387,16 @@ describe('plugins discovery system', () => { }) ); - discover(new PluginsConfig({ ...pluginConfig, paths: [extraPluginTestPath] }, env), { - coreId: Symbol(), - configService, - env, - logger, - }); + discover( + new PluginsConfig({ ...pluginConfig, paths: [extraPluginTestPath] }, env), + { + coreId: Symbol(), + configService, + env, + logger, + }, + instanceInfo + ); expect(loggingSystemMock.collect(logger).warn).toEqual([ [ @@ -388,12 +414,16 @@ describe('plugins discovery system', () => { }) ); - discover(new PluginsConfig({ ...pluginConfig, paths: [extraPluginTestPath] }, env), { - coreId: Symbol(), - configService, - env, - logger, - }); + discover( + new PluginsConfig({ ...pluginConfig, paths: [extraPluginTestPath] }, env), + { + coreId: Symbol(), + configService, + env, + logger, + }, + instanceInfo + ); expect(loggingSystemMock.collect(logger).warn).toEqual([]); }); diff --git a/src/core/server/plugins/discovery/plugins_discovery.ts b/src/core/server/plugins/discovery/plugins_discovery.ts index 5e765a9632e55a..2b5b8ad071fb58 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.ts @@ -24,7 +24,7 @@ import { catchError, filter, map, mergeMap, shareReplay } from 'rxjs/operators'; import { CoreContext } from '../../core_context'; import { Logger } from '../../logging'; import { PluginWrapper } from '../plugin'; -import { createPluginInitializerContext } from '../plugin_context'; +import { createPluginInitializerContext, InstanceInfo } from '../plugin_context'; import { PluginsConfig } from '../plugins_config'; import { PluginDiscoveryError } from './plugin_discovery_error'; import { parseManifest } from './plugin_manifest_parser'; @@ -49,7 +49,11 @@ interface PluginSearchPathEntry { * @param coreContext Kibana core values. * @internal */ -export function discover(config: PluginsConfig, coreContext: CoreContext) { +export function discover( + config: PluginsConfig, + coreContext: CoreContext, + instanceInfo: InstanceInfo +) { const log = coreContext.logger.get('plugins-discovery'); log.debug('Discovering plugins...'); @@ -65,7 +69,7 @@ export function discover(config: PluginsConfig, coreContext: CoreContext) { ).pipe( mergeMap((pluginPathOrError) => { return typeof pluginPathOrError === 'string' - ? createPlugin$(pluginPathOrError, log, coreContext) + ? createPlugin$(pluginPathOrError, log, coreContext, instanceInfo) : [pluginPathOrError]; }), shareReplay() @@ -180,7 +184,12 @@ function mapSubdirectories( * @param log Plugin discovery logger instance. * @param coreContext Kibana core context. */ -function createPlugin$(path: string, log: Logger, coreContext: CoreContext) { +function createPlugin$( + path: string, + log: Logger, + coreContext: CoreContext, + instanceInfo: InstanceInfo +) { return from(parseManifest(path, coreContext.env.packageInfo, log)).pipe( map((manifest) => { log.debug(`Successfully discovered plugin "${manifest.id}" at "${path}"`); @@ -189,7 +198,12 @@ function createPlugin$(path: string, log: Logger, coreContext: CoreContext) { path, manifest, opaqueId, - initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + initializerContext: createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ), }); }), catchError((err) => [err]) diff --git a/src/core/server/plugins/integration_tests/plugins_service.test.ts b/src/core/server/plugins/integration_tests/plugins_service.test.ts index 49c129d0ae67d7..5a216b75a83b9a 100644 --- a/src/core/server/plugins/integration_tests/plugins_service.test.ts +++ b/src/core/server/plugins/integration_tests/plugins_service.test.ts @@ -28,12 +28,14 @@ import { BehaviorSubject, from } from 'rxjs'; import { rawConfigServiceMock } from '../../config/raw_config_service.mock'; import { config } from '../plugins_config'; import { loggingSystemMock } from '../../logging/logging_system.mock'; +import { environmentServiceMock } from '../../environment/environment_service.mock'; import { coreMock } from '../../mocks'; import { Plugin } from '../types'; import { PluginWrapper } from '../plugin'; describe('PluginsService', () => { const logger = loggingSystemMock.create(); + const environmentSetup = environmentServiceMock.createSetupContract(); let pluginsService: PluginsService; const createPlugin = ( @@ -158,7 +160,7 @@ describe('PluginsService', () => { } ); - await pluginsService.discover(); + await pluginsService.discover({ environment: environmentSetup }); const setupDeps = coreMock.createInternalSetup(); await pluginsService.setup(setupDeps); diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts index 4f26686e1f5e0c..1108ffc2481615 100644 --- a/src/core/server/plugins/plugin.test.ts +++ b/src/core/server/plugins/plugin.test.ts @@ -30,7 +30,11 @@ import { loggingSystemMock } from '../logging/logging_system.mock'; import { PluginWrapper } from './plugin'; import { PluginManifest } from './types'; -import { createPluginInitializerContext, createPluginSetupContext } from './plugin_context'; +import { + createPluginInitializerContext, + createPluginSetupContext, + InstanceInfo, +} from './plugin_context'; const mockPluginInitializer = jest.fn(); const logger = loggingSystemMock.create(); @@ -67,12 +71,16 @@ configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true })); let coreId: symbol; let env: Env; let coreContext: CoreContext; +let instanceInfo: InstanceInfo; const setupDeps = coreMock.createInternalSetup(); beforeEach(() => { coreId = Symbol('core'); env = Env.createDefault(getEnvOptions()); + instanceInfo = { + uuid: 'instance-uuid', + }; coreContext = { coreId, env, logger, configService: configService as any }; }); @@ -88,7 +96,12 @@ test('`constructor` correctly initializes plugin instance', () => { path: 'some-plugin-path', manifest, opaqueId, - initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + initializerContext: createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ), }); expect(plugin.name).toBe('some-plugin-id'); @@ -105,7 +118,12 @@ test('`setup` fails if `plugin` initializer is not exported', async () => { path: 'plugin-without-initializer-path', manifest, opaqueId, - initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + initializerContext: createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ), }); await expect( @@ -122,7 +140,12 @@ test('`setup` fails if plugin initializer is not a function', async () => { path: 'plugin-with-wrong-initializer-path', manifest, opaqueId, - initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + initializerContext: createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ), }); await expect( @@ -139,7 +162,12 @@ test('`setup` fails if initializer does not return object', async () => { path: 'plugin-with-initializer-path', manifest, opaqueId, - initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + initializerContext: createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ), }); mockPluginInitializer.mockReturnValue(null); @@ -158,7 +186,12 @@ test('`setup` fails if object returned from initializer does not define `setup` path: 'plugin-with-initializer-path', manifest, opaqueId, - initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + initializerContext: createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ), }); const mockPluginInstance = { run: jest.fn() }; @@ -174,7 +207,12 @@ test('`setup` fails if object returned from initializer does not define `setup` test('`setup` initializes plugin and calls appropriate lifecycle hook', async () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); - const initializerContext = createPluginInitializerContext(coreContext, opaqueId, manifest); + const initializerContext = createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ); const plugin = new PluginWrapper({ path: 'plugin-with-initializer-path', manifest, @@ -203,7 +241,12 @@ test('`start` fails if setup is not called first', async () => { path: 'some-plugin-path', manifest, opaqueId, - initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + initializerContext: createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ), }); await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( @@ -218,7 +261,12 @@ test('`start` calls plugin.start with context and dependencies', async () => { path: 'plugin-with-initializer-path', manifest, opaqueId, - initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + initializerContext: createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ), }); const context = { any: 'thing' } as any; const deps = { otherDep: 'value' }; @@ -247,7 +295,12 @@ test("`start` resolves `startDependencies` Promise after plugin's start", async path: 'plugin-with-initializer-path', manifest, opaqueId, - initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + initializerContext: createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ), }); const startContext = { any: 'thing' } as any; const pluginDeps = { someDep: 'value' }; @@ -286,7 +339,12 @@ test('`stop` fails if plugin is not set up', async () => { path: 'plugin-with-initializer-path', manifest, opaqueId, - initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + initializerContext: createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ), }); const mockPluginInstance = { setup: jest.fn(), stop: jest.fn() }; @@ -305,7 +363,12 @@ test('`stop` does nothing if plugin does not define `stop` function', async () = path: 'plugin-with-initializer-path', manifest, opaqueId, - initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + initializerContext: createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ), }); mockPluginInitializer.mockReturnValue({ setup: jest.fn() }); @@ -321,7 +384,12 @@ test('`stop` calls `stop` defined by the plugin instance', async () => { path: 'plugin-with-initializer-path', manifest, opaqueId, - initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + initializerContext: createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ), }); const mockPluginInstance = { setup: jest.fn(), stop: jest.fn() }; @@ -351,7 +419,12 @@ describe('#getConfigSchema()', () => { path: 'plugin-with-schema', manifest, opaqueId, - initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + initializerContext: createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ), }); expect(plugin.getConfigDescriptor()).toBe(configDescriptor); @@ -365,7 +438,12 @@ describe('#getConfigSchema()', () => { path: 'plugin-with-no-definition', manifest, opaqueId, - initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + initializerContext: createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ), }); expect(plugin.getConfigDescriptor()).toBe(null); }); @@ -377,7 +455,12 @@ describe('#getConfigSchema()', () => { path: 'plugin-with-no-definition', manifest, opaqueId, - initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + initializerContext: createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ), }); expect(plugin.getConfigDescriptor()).toBe(null); }); @@ -400,7 +483,12 @@ describe('#getConfigSchema()', () => { path: 'plugin-invalid-schema', manifest, opaqueId, - initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + initializerContext: createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ), }); expect(() => plugin.getConfigDescriptor()).toThrowErrorMatchingInlineSnapshot( `"Configuration schema expected to be an instance of Type"` diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts index ebd068caadfb9f..578c5f39d71ea1 100644 --- a/src/core/server/plugins/plugin_context.test.ts +++ b/src/core/server/plugins/plugin_context.test.ts @@ -19,7 +19,7 @@ import { duration } from 'moment'; import { first } from 'rxjs/operators'; -import { createPluginInitializerContext } from './plugin_context'; +import { createPluginInitializerContext, InstanceInfo } from './plugin_context'; import { CoreContext } from '../core_context'; import { Env } from '../config'; import { loggingSystemMock } from '../logging/logging_system.mock'; @@ -35,6 +35,7 @@ let coreId: symbol; let env: Env; let coreContext: CoreContext; let server: Server; +let instanceInfo: InstanceInfo; function createPluginManifest(manifestProps: Partial = {}): PluginManifest { return { @@ -51,9 +52,12 @@ function createPluginManifest(manifestProps: Partial = {}): Plug }; } -describe('Plugin Context', () => { +describe('createPluginInitializerContext', () => { beforeEach(async () => { coreId = Symbol('core'); + instanceInfo = { + uuid: 'instance-uuid', + }; env = Env.createDefault(getEnvOptions()); const config$ = rawConfigServiceMock.create({ rawConfig: {} }); server = new Server(config$, env, logger); @@ -67,7 +71,8 @@ describe('Plugin Context', () => { const pluginInitializerContext = createPluginInitializerContext( coreContext, opaqueId, - manifest + manifest, + instanceInfo ); expect(pluginInitializerContext.config.legacy.globalConfig$).toBeDefined(); @@ -90,4 +95,19 @@ describe('Plugin Context', () => { path: { data: fromRoot('data') }, }); }); + + it('allow to access the provided instance uuid', () => { + const manifest = createPluginManifest(); + const opaqueId = Symbol(); + instanceInfo = { + uuid: 'kibana-uuid', + }; + const pluginInitializerContext = createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ); + expect(pluginInitializerContext.env.instanceUuid).toBe('kibana-uuid'); + }); }); diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 62058f6d478e7b..fa2659ca130a03 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -37,6 +37,10 @@ import { import { pick, deepFreeze } from '../../utils'; import { CoreSetup, CoreStart } from '..'; +export interface InstanceInfo { + uuid: string; +} + /** * This returns a facade for `CoreContext` that will be exposed to the plugin initializer. * This facade should be safe to use across entire plugin lifespan. @@ -53,7 +57,8 @@ import { CoreSetup, CoreStart } from '..'; export function createPluginInitializerContext( coreContext: CoreContext, opaqueId: PluginOpaqueId, - pluginManifest: PluginManifest + pluginManifest: PluginManifest, + instanceInfo: InstanceInfo ): PluginInitializerContext { return { opaqueId, @@ -64,6 +69,7 @@ export function createPluginInitializerContext( env: { mode: coreContext.env.mode, packageInfo: coreContext.env.packageInfo, + instanceUuid: instanceInfo.uuid, }, /** @@ -183,9 +189,6 @@ export function createPluginSetupContext( uiSettings: { register: deps.uiSettings.register, }, - uuid: { - getInstanceUuid: deps.uuid.getInstanceUuid, - }, getStartServices: () => plugin.startDependencies, auditTrail: deps.auditTrail, }; diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index aa77335991e2cd..5e613343c302fa 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -29,6 +29,7 @@ import { rawConfigServiceMock } from '../config/raw_config_service.mock'; import { getEnvOptions } from '../config/__mocks__/env'; import { coreMock } from '../mocks'; import { loggingSystemMock } from '../logging/logging_system.mock'; +import { environmentServiceMock } from '../environment/environment_service.mock'; import { PluginDiscoveryError } from './discovery'; import { PluginWrapper } from './plugin'; import { PluginsService } from './plugins_service'; @@ -45,6 +46,7 @@ let configService: ConfigService; let coreId: symbol; let env: Env; let mockPluginSystem: jest.Mocked; +let environmentSetup: ReturnType; const setupDeps = coreMock.createInternalSetup(); const logger = loggingSystemMock.create(); @@ -124,6 +126,8 @@ describe('PluginsService', () => { [mockPluginSystem] = MockPluginsSystem.mock.instances as any; mockPluginSystem.uiPlugins.mockReturnValue(new Map()); + + environmentSetup = environmentServiceMock.createSetupContract(); }); afterEach(() => { @@ -137,7 +141,8 @@ describe('PluginsService', () => { plugin$: from([]), }); - await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(` + await expect(pluginsService.discover({ environment: environmentSetup })).rejects + .toMatchInlineSnapshot(` [Error: Failed to initialize plugins: Invalid JSON (invalid-manifest, path-1)] `); @@ -158,7 +163,8 @@ describe('PluginsService', () => { plugin$: from([]), }); - await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(` + await expect(pluginsService.discover({ environment: environmentSetup })).rejects + .toMatchInlineSnapshot(` [Error: Failed to initialize plugins: Incompatible version (incompatible-version, path-3)] `); @@ -192,7 +198,9 @@ describe('PluginsService', () => { ]), }); - await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot( + await expect( + pluginsService.discover({ environment: environmentSetup }) + ).rejects.toMatchInlineSnapshot( `[Error: Plugin with id "conflicting-id" is already registered!]` ); @@ -253,7 +261,7 @@ describe('PluginsService', () => { ]), }); - await pluginsService.discover(); + await pluginsService.discover({ environment: environmentSetup }); const setup = await pluginsService.setup(setupDeps); expect(setup.contracts).toBeInstanceOf(Map); @@ -300,7 +308,7 @@ describe('PluginsService', () => { plugin$: from([firstPlugin, secondPlugin]), }); - const { pluginTree } = await pluginsService.discover(); + const { pluginTree } = await pluginsService.discover({ environment: environmentSetup }); expect(pluginTree).toBeUndefined(); expect(mockDiscover).toHaveBeenCalledTimes(1); @@ -336,7 +344,7 @@ describe('PluginsService', () => { plugin$: from([firstPlugin, secondPlugin, thirdPlugin, lastPlugin, missingDepsPlugin]), }); - const { pluginTree } = await pluginsService.discover(); + const { pluginTree } = await pluginsService.discover({ environment: environmentSetup }); expect(pluginTree).toBeUndefined(); expect(mockDiscover).toHaveBeenCalledTimes(1); @@ -369,7 +377,7 @@ describe('PluginsService', () => { plugin$: from([firstPlugin, secondPlugin]), }); - await pluginsService.discover(); + await pluginsService.discover({ environment: environmentSetup }); expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin); expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin); @@ -386,7 +394,8 @@ describe('PluginsService', () => { resolve(process.cwd(), '..', 'kibana-extra'), ], }, - { coreId, env, logger, configService } + { coreId, env, logger, configService }, + { uuid: 'uuid' } ); const logs = loggingSystemMock.collect(logger); @@ -417,7 +426,7 @@ describe('PluginsService', () => { }), ]), }); - await pluginsService.discover(); + await pluginsService.discover({ environment: environmentSetup }); expect(configService.setSchema).toBeCalledWith('path', configSchema); }); @@ -448,7 +457,7 @@ describe('PluginsService', () => { }), ]), }); - await pluginsService.discover(); + await pluginsService.discover({ environment: environmentSetup }); expect(configService.addDeprecationProvider).toBeCalledWith( 'config-path', deprecationProvider @@ -496,7 +505,7 @@ describe('PluginsService', () => { }); mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)])); - const { uiPlugins } = await pluginsService.discover(); + const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup }); const uiConfig$ = uiPlugins.browserConfigs.get('plugin-with-expose'); expect(uiConfig$).toBeDefined(); @@ -532,7 +541,7 @@ describe('PluginsService', () => { }); mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)])); - const { uiPlugins } = await pluginsService.discover(); + const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup }); expect([...uiPlugins.browserConfigs.entries()]).toHaveLength(0); }); }); @@ -561,7 +570,7 @@ describe('PluginsService', () => { describe('uiPlugins.internal', () => { it('includes disabled plugins', async () => { config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } }); - const { uiPlugins } = await pluginsService.discover(); + const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup }); expect(uiPlugins.internal).toMatchInlineSnapshot(` Map { "plugin-1" => Object { @@ -582,7 +591,7 @@ describe('PluginsService', () => { describe('plugin initialization', () => { it('does initialize if plugins.initialize is true', async () => { config$.next({ plugins: { initialize: true } }); - await pluginsService.discover(); + await pluginsService.discover({ environment: environmentSetup }); const { initialized } = await pluginsService.setup(setupDeps); expect(mockPluginSystem.setupPlugins).toHaveBeenCalled(); expect(initialized).toBe(true); @@ -590,7 +599,7 @@ describe('PluginsService', () => { it('does not initialize if plugins.initialize is false', async () => { config$.next({ plugins: { initialize: false } }); - await pluginsService.discover(); + await pluginsService.discover({ environment: environmentSetup }); const { initialized } = await pluginsService.setup(setupDeps); expect(mockPluginSystem.setupPlugins).not.toHaveBeenCalled(); expect(initialized).toBe(false); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 06de48a215881a..30cd47c9d44e17 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -32,6 +32,7 @@ import { PluginsSystem } from './plugins_system'; import { InternalCoreSetup, InternalCoreStart } from '../internal_types'; import { IConfigService } from '../config'; import { pick } from '../../utils'; +import { InternalEnvironmentServiceSetup } from '../environment'; /** @internal */ export interface PluginsServiceSetup { @@ -72,6 +73,11 @@ export type PluginsServiceSetupDeps = InternalCoreSetup; /** @internal */ export type PluginsServiceStartDeps = InternalCoreStart; +/** @internal */ +export interface PluginsServiceDiscoverDeps { + environment: InternalEnvironmentServiceSetup; +} + /** @internal */ export class PluginsService implements CoreService { private readonly log: Logger; @@ -90,12 +96,14 @@ export class PluginsService implements CoreService new PluginsConfig(rawConfig, coreContext.env))); } - public async discover() { + public async discover({ environment }: PluginsServiceDiscoverDeps) { this.log.debug('Discovering plugins'); const config = await this.config$.pipe(first()).toPromise(); - const { error$, plugin$ } = discover(config, this.coreContext); + const { error$, plugin$ } = discover(config, this.coreContext, { + uuid: environment.instanceUuid, + }); await this.handleDiscoveryErrors(error$); await this.handleDiscoveredPlugins(plugin$); diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 9695c9171a7714..eb2a9ca3daf5f7 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -278,6 +278,7 @@ export interface PluginInitializerContext { env: { mode: EnvironmentMode; packageInfo: Readonly; + instanceUuid: string; }; logger: LoggerFactory; config: { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index cd7f4973f886c0..6186906bc3a42b 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -499,8 +499,6 @@ export interface CoreSetup { env: { mode: EnvironmentMode; packageInfo: Readonly; + instanceUuid: string; }; // (undocumented) logger: LoggerFactory; @@ -2883,11 +2882,6 @@ export interface UserProvidedValues { userValue?: T; } -// @public -export interface UuidServiceSetup { - getInstanceUuid(): string; -} - // @public export const validBodyOutput: readonly ["data", "stream"]; diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index 82d0c095bfe95b..471e482a20e962 100644 --- a/src/core/server/server.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -74,10 +74,10 @@ import { RenderingService, mockRenderingService } from './rendering/__mocks__/re export { mockRenderingService }; jest.doMock('./rendering/rendering_service', () => ({ RenderingService })); -import { uuidServiceMock } from './uuid/uuid_service.mock'; -export const mockUuidService = uuidServiceMock.create(); -jest.doMock('./uuid/uuid_service', () => ({ - UuidService: jest.fn(() => mockUuidService), +import { environmentServiceMock } from './environment/environment_service.mock'; +export const mockEnvironmentService = environmentServiceMock.create(); +jest.doMock('./environment/environment_service', () => ({ + EnvironmentService: jest.fn(() => mockEnvironmentService), })); import { metricsServiceMock } from './metrics/metrics_service.mock'; diff --git a/src/core/server/server.ts b/src/core/server/server.ts index aff749ca975342..cc6d8171e7a037 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -31,7 +31,7 @@ import { PluginsService, config as pluginsConfig } from './plugins'; import { SavedObjectsService } from '../server/saved_objects'; import { MetricsService, opsConfig } from './metrics'; import { CapabilitiesService } from './capabilities'; -import { UuidService } from './uuid'; +import { EnvironmentService } from './environment'; import { StatusService } from './status/status_service'; import { config as cspConfig } from './csp'; @@ -64,7 +64,7 @@ export class Server { private readonly plugins: PluginsService; private readonly savedObjects: SavedObjectsService; private readonly uiSettings: UiSettingsService; - private readonly uuid: UuidService; + private readonly environment: EnvironmentService; private readonly metrics: MetricsService; private readonly httpResources: HttpResourcesService; private readonly status: StatusService; @@ -95,7 +95,7 @@ export class Server { this.savedObjects = new SavedObjectsService(core); this.uiSettings = new UiSettingsService(core); this.capabilities = new CapabilitiesService(core); - this.uuid = new UuidService(core); + this.environment = new EnvironmentService(core); this.metrics = new MetricsService(core); this.status = new StatusService(core); this.coreApp = new CoreApp(core); @@ -107,8 +107,12 @@ export class Server { public async setup() { this.log.debug('setting up server'); + const environmentSetup = await this.environment.setup(); + // Discover any plugins before continuing. This allows other systems to utilize the plugin dependency graph. - const { pluginTree, uiPlugins } = await this.plugins.discover(); + const { pluginTree, uiPlugins } = await this.plugins.discover({ + environment: environmentSetup, + }); const legacyPlugins = await this.legacy.discoverPlugins(); // Immediately terminate in case of invalid configuration @@ -124,7 +128,6 @@ export class Server { }); const auditTrailSetup = this.auditTrail.setup(); - const uuidSetup = await this.uuid.setup(); const httpSetup = await this.http.setup({ context: contextServiceSetup, @@ -174,11 +177,11 @@ export class Server { capabilities: capabilitiesSetup, context: contextServiceSetup, elasticsearch: elasticsearchServiceSetup, + environment: environmentSetup, http: httpSetup, savedObjects: savedObjectsSetup, status: statusSetup, uiSettings: uiSettingsSetup, - uuid: uuidSetup, rendering: renderingSetup, httpResources: httpResourcesSetup, auditTrail: auditTrailSetup, diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 176c5386961a54..722d75d00f78fc 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -17,13 +17,8 @@ * under the License. */ -import Fs from 'fs'; -import { promisify } from 'util'; - import { getUiSettingDefaults } from './server/ui_setting_defaults'; -const mkdirAsync = promisify(Fs.mkdir); - export default function (kibana) { return new kibana.Plugin({ id: 'kibana', @@ -40,17 +35,5 @@ export default function (kibana) { uiExports: { uiSettingDefaults: getUiSettingDefaults(), }, - - preInit: async function (server) { - try { - // Create the data directory (recursively, if the a parent dir doesn't exist). - // If it already exists, does nothing. - await mkdirAsync(server.config().get('path.data'), { recursive: true }); - } catch (err) { - server.log(['error', 'init'], err); - // Stop the server startup with a fatal error - throw err; - } - }, }); } diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index 1353877fa4629a..4439a4fb9fdbbe 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -85,7 +85,7 @@ export class Plugin implements CorePlugin { serverBasePath: '', }, }, - uuid: { - getInstanceUuid: jest.fn(), - }, elasticsearch: { legacy: { client: {}, diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 043435c48a211e..501c96b12fde86 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -92,7 +92,7 @@ export class Plugin { const router = core.http.createRouter(); this.legacyShimDependencies = { router, - instanceUuid: core.uuid.getInstanceUuid(), + instanceUuid: this.initializerContext.env.instanceUuid, esDataClient: core.elasticsearch.legacy.client, kibanaStatsCollector: plugins.usageCollection?.getCollectorByType( KIBANA_STATS_TYPE_MONITORING @@ -159,7 +159,7 @@ export class Plugin { config, log: kibanaMonitoringLog, kibanaStats: { - uuid: core.uuid.getInstanceUuid(), + uuid: this.initializerContext.env.instanceUuid, name: serverInfo.name, index: get(legacyConfig, 'kibana.index'), host: serverInfo.hostname, diff --git a/x-pack/plugins/reporting/server/config/config.ts b/x-pack/plugins/reporting/server/config/config.ts index ca07fd84523728..088598829a3e14 100644 --- a/x-pack/plugins/reporting/server/config/config.ts +++ b/x-pack/plugins/reporting/server/config/config.ts @@ -75,7 +75,7 @@ export const buildConfig = async ( host: serverInfo.hostname, name: serverInfo.name, port: serverInfo.port, - uuid: core.uuid.getInstanceUuid(), + uuid: initContext.env.instanceUuid, protocol: serverInfo.protocol, }, }; diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 70295046d19f41..d7dcf779376bff 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -42,7 +42,7 @@ export class TaskManagerPlugin .toPromise(); setupSavedObjects(core.savedObjects, this.config); - this.taskManagerId = core.uuid.getInstanceUuid(); + this.taskManagerId = this.initContext.env.instanceUuid; return { addMiddleware: (middleware: Middleware) => { From 2946e68581a81c6a685a248192c8f08fec77a552 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 26 Aug 2020 15:51:47 -0400 Subject: [PATCH 041/216] [Ingest Manager] Remove useless saved object update in agent checkin (#75586) --- .../server/services/agents/checkin/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/index.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/index.ts index 78e6a11fa78a47..19a5c2dc087625 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import deepEqual from 'fast-deep-equal'; import { SavedObjectsClientContract, SavedObjectsBulkCreateObject } from 'src/core/server'; import { Agent, @@ -29,16 +30,19 @@ export async function agentCheckin( ) { const updateData: Partial = {}; const { updatedErrorEvents } = await processEventsForCheckin(soClient, agent, data.events); - if (updatedErrorEvents) { + if ( + updatedErrorEvents && + !(updatedErrorEvents.length === 0 && agent.current_error_events.length === 0) + ) { updateData.current_error_events = JSON.stringify(updatedErrorEvents); } - if (data.localMetadata) { + if (data.localMetadata && !deepEqual(data.localMetadata, agent.local_metadata)) { updateData.local_metadata = data.localMetadata; } - if (data.status !== agent.last_checkin_status) { updateData.last_checkin_status = data.status; } + // Update agent only if something changed if (Object.keys(updateData).length > 0) { await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, updateData); } From 638df5820c04d2c21311dda3198855e2ae569b64 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Wed, 26 Aug 2020 13:56:18 -0600 Subject: [PATCH 042/216] [Security Solution][Detections] Fixes Alerts Table 'Select all [x] alerts' action (#75945) ## Summary Resolves https://github.com/elastic/kibana/issues/75194 Fixes issue where the `Select all [x] alerts` feature would not select the checkboxes within the Alerts Table. Also resolves issue where bulk actions wouldn't work with Building Block Alerts. ##### Select All Before

##### Select All After

##### Building Block Query Before

##### Building Block Query After

--- .../components/alerts_table/index.tsx | 42 ++++++++++++------- .../components/manage_timeline/index.tsx | 24 +++++++++++ .../timeline/body/stateful_body.tsx | 4 +- 3 files changed, 53 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 66423259ec1556..07e69d850f1732 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -105,7 +105,6 @@ export const AlertsTableComponent: React.FC = ({ updateTimelineIsLoading, }) => { const dispatch = useDispatch(); - const [selectAll, setSelectAll] = useState(false); const apolloClient = useApolloClient(); const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); @@ -120,6 +119,12 @@ export const AlertsTableComponent: React.FC = ({ ); const kibana = useKibana(); const [, dispatchToaster] = useStateToaster(); + const { + initializeTimeline, + setSelectAll, + setTimelineRowActions, + setIndexToAdd, + } = useManageTimeline(); const getGlobalQuery = useCallback( (customFilters: Filter[]) => { @@ -141,8 +146,7 @@ export const AlertsTableComponent: React.FC = ({ } return null; }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [browserFields, globalFilters, globalQuery, indexPatterns, kibana, to, from] + [browserFields, defaultFilters, globalFilters, globalQuery, indexPatterns, kibana, to, from] ); // Callback for creating a new timeline -- utilized by row/batch actions @@ -240,12 +244,15 @@ export const AlertsTableComponent: React.FC = ({ // Catches state change isSelectAllChecked->false upon user selection change to reset utility bar useEffect(() => { - if (!isSelectAllChecked) { - setShowClearSelectionAction(false); + if (isSelectAllChecked) { + setSelectAll({ + id: timelineId, + selectAll: false, + }); } else { - setSelectAll(false); + setShowClearSelectionAction(false); } - }, [isSelectAllChecked]); + }, [isSelectAllChecked, setSelectAll, timelineId]); // Callback for when open/closed filter changes const onFilterGroupChangedCallback = useCallback( @@ -261,17 +268,23 @@ export const AlertsTableComponent: React.FC = ({ // Callback for clearing entire selection from utility bar const clearSelectionCallback = useCallback(() => { clearSelected!({ id: timelineId }); - setSelectAll(false); + setSelectAll({ + id: timelineId, + selectAll: false, + }); setShowClearSelectionAction(false); }, [clearSelected, setSelectAll, setShowClearSelectionAction, timelineId]); // Callback for selecting all events on all pages from utility bar // Dispatches to stateful_body's selectAll via TimelineTypeContext props // as scope of response data required to actually set selectedEvents - const selectAllCallback = useCallback(() => { - setSelectAll(true); + const selectAllOnAllPagesCallback = useCallback(() => { + setSelectAll({ + id: timelineId, + selectAll: true, + }); setShowClearSelectionAction(true); - }, [setSelectAll, setShowClearSelectionAction]); + }, [setSelectAll, setShowClearSelectionAction, timelineId]); const updateAlertsStatusCallback: UpdateAlertsStatusCallback = useCallback( async ( @@ -314,7 +327,7 @@ export const AlertsTableComponent: React.FC = ({ clearSelection={clearSelectionCallback} hasIndexWrite={hasIndexWrite} currentFilter={filterGroup} - selectAll={selectAllCallback} + selectAll={selectAllOnAllPagesCallback} selectedEventIds={selectedEventIds} showBuildingBlockAlerts={showBuildingBlockAlerts} onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChanged} @@ -332,7 +345,7 @@ export const AlertsTableComponent: React.FC = ({ showBuildingBlockAlerts, onShowBuildingBlockAlertsChanged, loadingEventIds.length, - selectAllCallback, + selectAllOnAllPagesCallback, selectedEventIds, showClearSelectionAction, updateAlertsStatusCallback, @@ -384,7 +397,6 @@ export const AlertsTableComponent: React.FC = ({ } }, [defaultFilters, filterGroup]); const { filterManager } = useKibana().services.data.query; - const { initializeTimeline, setTimelineRowActions, setIndexToAdd } = useManageTimeline(); useEffect(() => { initializeTimeline({ @@ -395,7 +407,7 @@ export const AlertsTableComponent: React.FC = ({ id: timelineId, indexToAdd: defaultIndices, loadingText: i18n.LOADING_ALERTS, - selectAll: canUserCRUD ? selectAll : false, + selectAll: false, timelineRowActions: () => [getInvestigateInResolverAction({ dispatch, timelineId })], title: '', }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx index a425f9b49add0d..560d4c6928e4e4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx @@ -71,6 +71,11 @@ type ActionManageTimeline = id: string; payload: string[]; } + | { + type: 'SET_SELECT_ALL'; + id: string; + payload: boolean; + } | { type: 'SET_TIMELINE_ACTIONS'; id: string; @@ -116,6 +121,14 @@ const reducerManageTimeline = ( indexToAdd: action.payload, }, } as ManageTimelineById; + case 'SET_SELECT_ALL': + return { + ...state, + [action.id]: { + ...state[action.id], + selectAll: action.payload, + }, + } as ManageTimelineById; case 'SET_TIMELINE_ACTIONS': return { ...state, @@ -145,6 +158,7 @@ export interface UseTimelineManager { isManagedTimeline: (id: string) => boolean; setIndexToAdd: (indexToAddArgs: { id: string; indexToAdd: string[] }) => void; setIsTimelineLoading: (isLoadingArgs: { id: string; isLoading: boolean }) => void; + setSelectAll: (selectAllArgs: { id: string; selectAll: boolean }) => void; setTimelineRowActions: (actionsArgs: { id: string; queryFields?: string[]; @@ -205,6 +219,14 @@ export const useTimelineManager = ( }); }, []); + const setSelectAll = useCallback(({ id, selectAll }: { id: string; selectAll: boolean }) => { + dispatch({ + type: 'SET_SELECT_ALL', + id, + payload: selectAll, + }); + }, []); + const getTimelineFilterManager = useCallback( (id: string): FilterManager | undefined => state[id]?.filterManager, [state] @@ -238,6 +260,7 @@ export const useTimelineManager = ( isManagedTimeline, setIndexToAdd, setIsTimelineLoading, + setSelectAll, setTimelineRowActions, }; }; @@ -250,6 +273,7 @@ const init = { isManagedTimeline: () => false, setIndexToAdd: () => undefined, setIsTimelineLoading: () => noop, + setSelectAll: () => noop, setTimelineRowActions: () => noop, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx index 15fa13b1a08f12..8deda03ece70eb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx @@ -169,10 +169,10 @@ const StatefulBodyComponent = React.memo( // Sync to selectAll so parent components can select all events useEffect(() => { - if (selectAll) { + if (selectAll && !isSelectAllChecked) { onSelectAll({ isSelected: true }); } - }, [onSelectAll, selectAll]); + }, [isSelectAllChecked, onSelectAll, selectAll]); const enabledRowRenderers = useMemo(() => { if ( From 532f2d70e84af2aec4df06331643fbb6e0ba5033 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Wed, 26 Aug 2020 13:00:00 -0700 Subject: [PATCH 043/216] [Home] Elastic home page redesign (#70571) Co-authored-by: Catherine Liu Co-authored-by: Ryan Keairns Co-authored-by: Catherine Liu Co-authored-by: Michael Marcialis --- .../collapsible_nav.test.tsx.snap | 10 +- .../header/__snapshots__/header.test.tsx.snap | 6 + src/core/public/chrome/ui/header/header.tsx | 2 +- .../overlays/banners/_banners_list.scss | 2 +- src/core/utils/default_app_categories.ts | 2 +- src/plugins/advanced_settings/kibana.json | 3 +- .../management_app/advanced_settings.tsx | 15 + .../field/__snapshots__/field.test.tsx.snap | 54 + .../management_app/components/field/field.tsx | 1 + .../advanced_settings/public/plugin.ts | 18 +- src/plugins/advanced_settings/public/types.ts | 3 + src/plugins/console/kibana.json | 6 +- src/plugins/console/public/plugin.ts | 28 +- .../public/types/plugin_dependencies.ts | 2 +- src/plugins/dashboard/public/plugin.tsx | 2 +- .../discover/public/register_feature.ts | 2 +- src/plugins/home/common/constants.ts | 21 + .../home/public/application/application.tsx | 8 +- .../__snapshots__/add_data.test.js.snap | 1163 -------- .../__snapshots__/home.test.js.snap | 2360 ++++++++++------- .../__snapshots__/synopsis.test.js.snap | 12 +- .../application/components/_add_data.scss | 83 +- .../public/application/components/_home.scss | 74 +- .../public/application/components/_index.scss | 4 +- .../application/components/_manage_data.scss | 22 + .../components/_solutions_section.scss | 122 + .../application/components/_synopsis.scss | 4 + .../public/application/components/add_data.js | 320 --- .../application/components/add_data.test.js | 68 - .../__snapshots__/add_data.test.tsx.snap | 96 + .../components/add_data/add_data.test.tsx | 95 + .../components/add_data/add_data.tsx | 95 + .../application/components/add_data/index.ts | 20 + .../components/app_navigation_handler.ts | 1 + .../components/feature_directory.js | 2 + .../public/application/components/home.js | 250 +- .../application/components/home.test.js | 113 +- .../public/application/components/home_app.js | 19 +- .../__snapshots__/manage_data.test.tsx.snap | 91 + .../components/manage_data/index.tsx | 20 + .../manage_data/manage_data.test.tsx | 91 + .../components/manage_data/manage_data.tsx | 81 + .../solution_panel.test.tsx.snap | 47 + .../solution_title.test.tsx.snap | 41 + .../solutions_section.test.tsx.snap | 288 ++ .../components/solutions_section/index.ts | 20 + .../solutions_section/solution_panel.test.tsx | 43 + .../solutions_section/solution_panel.tsx | 83 + .../solutions_section/solution_title.test.tsx | 45 + .../solutions_section/solution_title.tsx | 59 + .../solutions_section.test.tsx | 94 + .../solutions_section/solutions_section.tsx | 93 + .../public/application/components/synopsis.js | 4 +- .../application/components/synopsis.test.js | 4 + .../components/tutorial_directory.js | 3 + src/plugins/home/public/index.ts | 1 + src/plugins/home/public/plugin.test.ts | 36 + src/plugins/home/public/plugin.ts | 59 +- .../feature_catalogue_registry.mock.ts | 2 + .../feature_catalogue_registry.test.ts | 20 +- .../feature_catalogue_registry.ts | 40 + .../services/feature_catalogue/index.ts | 1 + src/plugins/management/kibana.json | 5 +- src/plugins/management/public/plugin.ts | 30 +- .../saved_objects_management/kibana.json | 6 +- .../saved_objects_management/public/plugin.ts | 32 +- src/plugins/visualize/public/plugin.ts | 2 +- test/functional/page_objects/home_page.ts | 8 +- x-pack/plugins/apm/kibana.json | 7 +- .../apm/public/featureCatalogueEntry.ts | 2 +- x-pack/plugins/apm/public/plugin.ts | 8 +- x-pack/plugins/canvas/kibana.json | 6 +- .../canvas/public/feature_catalogue_entry.ts | 2 +- x-pack/plugins/canvas/public/plugin.tsx | 6 +- x-pack/plugins/enterprise_search/kibana.json | 7 +- .../enterprise_search/public/plugin.ts | 71 +- .../enterprise_search/server/plugin.ts | 3 +- x-pack/plugins/graph/public/plugin.ts | 5 +- .../index_lifecycle_management/kibana.json | 7 +- .../public/plugin.tsx | 23 +- .../public/types.ts | 2 + x-pack/plugins/infra/kibana.json | 7 +- x-pack/plugins/infra/public/plugin.ts | 4 +- .../plugins/infra/public/register_feature.ts | 4 +- x-pack/plugins/infra/public/types.ts | 2 +- x-pack/plugins/infra/server/features.ts | 12 +- x-pack/plugins/ingest_manager/kibana.json | 2 +- .../plugins/ingest_manager/public/plugin.ts | 21 +- .../plugins/ingest_manager/server/plugin.ts | 3 + x-pack/plugins/logstash/public/plugin.ts | 2 +- x-pack/plugins/maps/kibana.json | 5 +- .../maps/public/feature_catalogue_entry.ts | 4 +- x-pack/plugins/maps/public/plugin.ts | 6 +- .../plugins/ml/common/types/capabilities.ts | 2 + x-pack/plugins/ml/kibana.json | 5 +- x-pack/plugins/ml/public/plugin.ts | 6 +- x-pack/plugins/ml/public/register_feature.ts | 17 +- x-pack/plugins/ml/server/plugin.ts | 2 +- x-pack/plugins/monitoring/public/plugin.ts | 9 +- x-pack/plugins/observability/kibana.json | 3 +- x-pack/plugins/observability/public/plugin.ts | 35 +- x-pack/plugins/painless_lab/public/plugin.tsx | 2 +- x-pack/plugins/rollup/public/plugin.ts | 2 +- x-pack/plugins/security/public/plugin.tsx | 8 +- .../cypress/screens/kibana_navigation.ts | 17 +- .../security_solution/cypress/tasks/login.ts | 6 +- x-pack/plugins/security_solution/kibana.json | 4 +- .../security_solution/public/plugin.tsx | 45 +- .../plugins/security_solution/public/types.ts | 2 +- .../security_solution/server/plugin.ts | 3 +- x-pack/plugins/snapshot_restore/kibana.json | 7 +- .../plugins/snapshot_restore/public/plugin.ts | 25 +- .../public/create_feature_catalogue_entry.ts | 2 +- x-pack/plugins/spaces/public/plugin.test.ts | 2 +- .../transform/public/register_feature.ts | 2 +- .../translations/translations/ja-JP.json | 28 - .../translations/translations/zh-CN.json | 28 - x-pack/plugins/uptime/public/apps/plugin.ts | 5 +- x-pack/plugins/watcher/public/plugin.ts | 1 - x-pack/test/accessibility/apps/home.ts | 2 +- .../security_and_spaces/tests/catalogue.ts | 14 +- .../security_only/tests/catalogue.ts | 14 +- 122 files changed, 4073 insertions(+), 2903 deletions(-) create mode 100644 src/plugins/home/common/constants.ts delete mode 100644 src/plugins/home/public/application/components/__snapshots__/add_data.test.js.snap create mode 100644 src/plugins/home/public/application/components/_manage_data.scss create mode 100644 src/plugins/home/public/application/components/_solutions_section.scss delete mode 100644 src/plugins/home/public/application/components/add_data.js delete mode 100644 src/plugins/home/public/application/components/add_data.test.js create mode 100644 src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap create mode 100644 src/plugins/home/public/application/components/add_data/add_data.test.tsx create mode 100644 src/plugins/home/public/application/components/add_data/add_data.tsx create mode 100644 src/plugins/home/public/application/components/add_data/index.ts create mode 100644 src/plugins/home/public/application/components/manage_data/__snapshots__/manage_data.test.tsx.snap create mode 100644 src/plugins/home/public/application/components/manage_data/index.tsx create mode 100644 src/plugins/home/public/application/components/manage_data/manage_data.test.tsx create mode 100644 src/plugins/home/public/application/components/manage_data/manage_data.tsx create mode 100644 src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_panel.test.tsx.snap create mode 100644 src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_title.test.tsx.snap create mode 100644 src/plugins/home/public/application/components/solutions_section/__snapshots__/solutions_section.test.tsx.snap create mode 100644 src/plugins/home/public/application/components/solutions_section/index.ts create mode 100644 src/plugins/home/public/application/components/solutions_section/solution_panel.test.tsx create mode 100644 src/plugins/home/public/application/components/solutions_section/solution_panel.tsx create mode 100644 src/plugins/home/public/application/components/solutions_section/solution_title.test.tsx create mode 100644 src/plugins/home/public/application/components/solutions_section/solution_title.tsx create mode 100644 src/plugins/home/public/application/components/solutions_section/solutions_section.test.tsx create mode 100644 src/plugins/home/public/application/components/solutions_section/solutions_section.tsx diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 2cfe232bf5653c..fe959e570ab986 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -147,7 +147,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "baseUrl": "/", "category": Object { "euiIconType": "logoSecurity", - "id": "security", + "id": "securitySolution", "label": "Security", "order": 4000, }, @@ -1393,11 +1393,11 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` @@ -1433,7 +1433,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
} className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-security" + data-test-subj="collapsibleNavGroup-securitySolution" id="mockId" initialIsOpen={true} onToggle={[Function]} @@ -1441,7 +1441,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` >
- + {navType === 'modern' ? ( diff --git a/src/core/public/overlays/banners/_banners_list.scss b/src/core/public/overlays/banners/_banners_list.scss index ff260f7dc42fd8..9d4df065a0a4fa 100644 --- a/src/core/public/overlays/banners/_banners_list.scss +++ b/src/core/public/overlays/banners/_banners_list.scss @@ -3,5 +3,5 @@ } .kbnGlobalBannerList__item + .kbnGlobalBannerList__item { - margin-top: $euiSize; + margin-top: $euiSizeS; } diff --git a/src/core/utils/default_app_categories.ts b/src/core/utils/default_app_categories.ts index cc9bfb1db04d5e..1fb7c284c0dfda 100644 --- a/src/core/utils/default_app_categories.ts +++ b/src/core/utils/default_app_categories.ts @@ -46,7 +46,7 @@ export const DEFAULT_APP_CATEGORIES = Object.freeze({ order: 3000, }, security: { - id: 'security', + id: 'securitySolution', label: i18n.translate('core.ui.securityNavList.label', { defaultMessage: 'Security', }), diff --git a/src/plugins/advanced_settings/kibana.json b/src/plugins/advanced_settings/kibana.json index 8cf9b9c656d8f2..0e49fe17089f0b 100644 --- a/src/plugins/advanced_settings/kibana.json +++ b/src/plugins/advanced_settings/kibana.json @@ -4,5 +4,6 @@ "server": true, "ui": true, "requiredPlugins": ["management"], - "requiredBundles": ["kibanaReact"] + "optionalPlugins": ["home"], + "requiredBundles": ["kibanaReact", "home"] } diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx index d8853015d362a5..4afcba14abef48 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx @@ -114,6 +114,21 @@ export class AdvancedSettingsComponent extends Component< filteredSettings: this.mapSettings(Query.execute(query, this.settings)), }); }); + + // scrolls to setting provided in the URL hash + const { hash } = window.location; + if (hash !== '') { + setTimeout(() => { + const id = hash.replace('#', ''); + const element = document.getElementById(id); + const globalNavOffset = document.getElementById('headerGlobalNav')?.offsetHeight || 0; + + if (element) { + element.scrollIntoView(); + window.scrollBy(0, -globalNavOffset); // offsets scroll by height of the global nav + } + }, 0); + } } componentWillUnmount() { diff --git a/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap b/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap index da18eb70e5874c..2aabacb061667f 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap +++ b/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap @@ -15,6 +15,7 @@ exports[`Field for array setting should render as read only if saving is disable } fullWidth={true} + id="array:test:setting" title={

} fullWidth={true} + id="array:test:setting" title={

} fullWidth={true} + id="array:test:setting" title={

} fullWidth={true} + id="array:test:setting" title={

} fullWidth={true} + id="array:test:setting" title={

} fullWidth={true} + id="array:test:setting" title={

} fullWidth={true} + id="boolean:test:setting" title={

} fullWidth={true} + id="boolean:test:setting" title={

} fullWidth={true} + id="boolean:test:setting" title={

} fullWidth={true} + id="boolean:test:setting" title={

} fullWidth={true} + id="boolean:test:setting" title={

} fullWidth={true} + id="boolean:test:setting" title={

} fullWidth={true} + id="image:test:setting" title={

} fullWidth={true} + id="image:test:setting" title={

} fullWidth={true} + id="image:test:setting" title={

} fullWidth={true} + id="image:test:setting" title={

} fullWidth={true} + id="image:test:setting" title={

} fullWidth={true} + id="image:test:setting" title={

} fullWidth={true} + id="json:test:setting" title={

} fullWidth={true} + id="json:test:setting" title={

} fullWidth={true} + id="json:test:setting" title={

} fullWidth={true} + id="json:test:setting" title={

} fullWidth={true} + id="json:test:setting" title={

} fullWidth={true} + id="json:test:setting" title={

} fullWidth={true} + id="markdown:test:setting" title={

} fullWidth={true} + id="markdown:test:setting" title={

} fullWidth={true} + id="markdown:test:setting" title={

} fullWidth={true} + id="markdown:test:setting" title={

} fullWidth={true} + id="markdown:test:setting" title={

} fullWidth={true} + id="markdown:test:setting" title={

} fullWidth={true} + id="number:test:setting" title={

} fullWidth={true} + id="number:test:setting" title={

} fullWidth={true} + id="number:test:setting" title={

} fullWidth={true} + id="number:test:setting" title={

} fullWidth={true} + id="number:test:setting" title={

} fullWidth={true} + id="number:test:setting" title={

} fullWidth={true} + id="select:test:setting" title={

} fullWidth={true} + id="select:test:setting" title={

} fullWidth={true} + id="select:test:setting" title={

} fullWidth={true} + id="select:test:setting" title={

} fullWidth={true} + id="select:test:setting" title={

} fullWidth={true} + id="select:test:setting" title={

} fullWidth={true} + id="string:test:setting" title={

} fullWidth={true} + id="string:test:setting" title={

} fullWidth={true} + id="string:test:setting" title={

} fullWidth={true} + id="string:test:setting" title={

} fullWidth={true} + id="string:test:setting" title={

} fullWidth={true} + id="string:test:setting" title={

} fullWidth={true} + id="string:test-validation:setting" title={

} fullWidth={true} + id="string:test-validation:setting" title={

} fullWidth={true} + id="string:test-validation:setting" title={

} fullWidth={true} + id="string:test-validation:setting" title={

} fullWidth={true} + id="string:test-validation:setting" title={

} fullWidth={true} + id="string:test-validation:setting" title={

{ return ( { - public setup(core: CoreSetup, { management }: AdvancedSettingsPluginSetup) { + public setup(core: CoreSetup, { management, home }: AdvancedSettingsPluginSetup) { const kibanaSection = management.sections.section.kibana; kibanaSection.registerApp({ @@ -44,6 +45,21 @@ export class AdvancedSettingsPlugin }, }); + if (home) { + home.featureCatalogue.register({ + id: 'advanced_settings', + title, + description: i18n.translate('advancedSettings.featureCatalogueTitle', { + defaultMessage: + 'Customize your Kibana experience — change the date format, turn on dark mode, and more.', + }), + icon: 'gear', + path: '/app/management/kibana/settings', + showOnHomePage: false, + category: FeatureCatalogueCategory.ADMIN, + }); + } + return { component: component.setup, }; diff --git a/src/plugins/advanced_settings/public/types.ts b/src/plugins/advanced_settings/public/types.ts index a233b3debab8d0..cc59f52b1f30fb 100644 --- a/src/plugins/advanced_settings/public/types.ts +++ b/src/plugins/advanced_settings/public/types.ts @@ -18,6 +18,8 @@ */ import { ComponentRegistry } from './component_registry'; +import { HomePublicPluginSetup } from '../../home/public'; + import { ManagementSetup } from '../../management/public'; export interface AdvancedSettingsSetup { @@ -29,6 +31,7 @@ export interface AdvancedSettingsStart { export interface AdvancedSettingsPluginSetup { management: ManagementSetup; + home?: HomePublicPluginSetup; } export { ComponentRegistry }; diff --git a/src/plugins/console/kibana.json b/src/plugins/console/kibana.json index 031aa00eb6613d..ca43e4f258adda 100644 --- a/src/plugins/console/kibana.json +++ b/src/plugins/console/kibana.json @@ -3,7 +3,7 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["devTools", "home"], - "optionalPlugins": ["usageCollection"], - "requiredBundles": ["esUiShared", "kibanaReact", "kibanaUtils"] + "requiredPlugins": ["devTools"], + "optionalPlugins": ["usageCollection", "home"], + "requiredBundles": ["esUiShared", "kibanaReact", "kibanaUtils", "home"] } diff --git a/src/plugins/console/public/plugin.ts b/src/plugins/console/public/plugin.ts index 03b65a8bd145c5..f3421aefaf38df 100644 --- a/src/plugins/console/public/plugin.ts +++ b/src/plugins/console/public/plugin.ts @@ -28,19 +28,21 @@ export class ConsoleUIPlugin implements Plugin { const homeTitle = i18n.translate('home.breadcrumbs.homeTitle', { defaultMessage: 'Home' }); const { featureCatalogue, chrome } = getServices(); + const navLinks = chrome.navLinks.getAll(); // all the directories could be get in "start" phase of plugin after all of the legacy plugins will be moved to a NP const directories = featureCatalogue.get(); + // Filters solutions by available nav links + const solutions = featureCatalogue + .getSolutions() + .filter(({ id }) => navLinks.find(({ category, hidden }) => !hidden && category?.id === id)); + chrome.setBreadcrumbs([{ text: homeTitle }]); render( - + , element ); 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 deleted file mode 100644 index 9178d0e08f3e04..00000000000000 --- a/src/plugins/home/public/application/components/__snapshots__/add_data.test.js.snap +++ /dev/null @@ -1,1163 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`apmUiEnabled 1`] = ` - - - - - - - - - - -

- -

-
-
-
- - - - - APM automatically collects in-depth performance metrics and errors from inside your applications. -
- } - footer={ - - - - } - textAlign="left" - title="APM" - titleSize="xs" - /> - - - - Ingest logs from popular data sources and easily visualize in preconfigured dashboards. - - } - footer={ - - - - } - textAlign="left" - title="Logs" - titleSize="xs" - /> - - - - Collect metrics from the operating system and services running on your servers. - - } - footer={ - - - - } - textAlign="left" - title="Metrics" - titleSize="xs" - /> - - - - - - - - - - -

- -

-
-
-
- - - Protect hosts, analyze security information and events, hunt threats, automate detections, and create cases. - - } - footer={ - - - - } - textAlign="left" - title="SIEM + Endpoint Security" - titleSize="xs" - /> -
- - - - - - - - - - - - - - - - - - - - - - - - - -`; - -exports[`isNewKibanaInstance 1`] = ` - - - - - - - - - - -

- -

-
-
-
- - - - - Ingest logs from popular data sources and easily visualize in preconfigured dashboards. - - } - footer={ - - - - } - textAlign="left" - title="Logs" - titleSize="xs" - /> - - - - Collect metrics from the operating system and services running on your servers. - - } - footer={ - - - - } - textAlign="left" - title="Metrics" - titleSize="xs" - /> - - -
- - - - - - - -

- -

-
-
-
- - - Protect hosts, analyze security information and events, hunt threats, automate detections, and create cases. - - } - footer={ - - - - } - textAlign="left" - title="SIEM + Endpoint Security" - titleSize="xs" - /> -
-
- - - - - - - - - - - - - - - - - - - - - - - -
-`; - -exports[`mlEnabled 1`] = ` - - - - - - - - - - -

- -

-
-
-
- - - - - APM automatically collects in-depth performance metrics and errors from inside your applications. - - } - footer={ - - - - } - textAlign="left" - title="APM" - titleSize="xs" - /> - - - - Ingest logs from popular data sources and easily visualize in preconfigured dashboards. - - } - footer={ - - - - } - textAlign="left" - title="Logs" - titleSize="xs" - /> - - - - Collect metrics from the operating system and services running on your servers. - - } - footer={ - - - - } - textAlign="left" - title="Metrics" - titleSize="xs" - /> - - -
- - - - - - - -

- -

-
-
-
- - - Protect hosts, analyze security information and events, hunt threats, automate detections, and create cases. - - } - footer={ - - - - } - textAlign="left" - title="SIEM + Endpoint Security" - titleSize="xs" - /> -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-`; - -exports[`render 1`] = ` - - - - - - - - - - -

- -

-
-
-
- - - - - Ingest logs from popular data sources and easily visualize in preconfigured dashboards. - - } - footer={ - - - - } - textAlign="left" - title="Logs" - titleSize="xs" - /> - - - - Collect metrics from the operating system and services running on your servers. - - } - footer={ - - - - } - textAlign="left" - title="Metrics" - titleSize="xs" - /> - - -
- - - - - - - -

- -

-
-
-
- - - Protect hosts, analyze security information and events, hunt threats, automate detections, and create cases. - - } - footer={ - - - - } - textAlign="left" - title="SIEM + Endpoint Security" - titleSize="xs" - /> -
-
- - - - - - - - - - - - - - - - - - - - - - - -
-`; diff --git a/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap b/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap index 4fa04bb64b1777..1b10756c2975c3 100644 --- a/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap +++ b/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap @@ -1,1066 +1,1615 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`home directories should not render directory entry when showOnHomePage is false 1`] = ` - - - -

- -

-
- - - - - +
+ + -

+

-

+

- - - - - - + - -

- -

-
- - -
+ + + Add data + + + +
+ +

+ +
+ 0 + + + + + + -
+ `; -exports[`home directories should render ADMIN directory entry in "Manage" panel 1`] = ` - - - -

- -

-
- - - - - +
+ + -

+

-

+
- - - +
+ + + + + Add data + + + + + Manage + + + + +
+
+ +
+ 0 + + + - + + +
+ +`; + +exports[`home directories should render ADMIN directory entry in "Manage your data" panel 1`] = ` +
+
+
+ + -

+

-

+
- - + + - + + Add data + - - +
+ + +
+
+
+ 0 + + + + + + -
+
`; -exports[`home directories should render DATA directory entry in "Explore Data" panel 1`] = ` - - - -

- -

-
- - - - - +
+ + -

+

-

+
- - + + - + + Add data + - - +
+ + +
+ +
+ 0 + + + - + + +
+ +`; + +exports[`home directories should render solutions in the "solution section" 1`] = ` +
+
+
+ + -

+

-

+
- - - +
+ + + + + Add data + + + + +
+
+
+
+ + + + + + + -
+
`; -exports[`home isNewKibanaInstance should safely handle execeptions 1`] = ` - - - -

- -

-
- - - - - +
+ + -

+

-

+
- - - +
+ + + + + Add data + + + + +
+
+ +
+ 0 + + + - + + +
+ +`; + +exports[`home header should show "Dev tools" link if console is available 1`] = ` +
+
+
+ + -

+

-

+
- - - +
+ + + + + Add data + + + + + Dev tools + + + + +
+
+
+
+ 0 + + + + + + -
+
`; -exports[`home isNewKibanaInstance should set isNewKibanaInstance to false when there are index patterns 1`] = ` - - - -

- -

-
- - - - - +
+ + -

+

-

+
- - - +
+ + + + + Add data + + + + + Manage + + + + +
+
+ +
+ 0 + + + - + + +
+ +`; + +exports[`home isNewKibanaInstance should safely handle execeptions 1`] = ` +
+
+
+ + -

+

-

+
- - - +
+ + + + + Add data + + + + +
+
+
+
+ 0 + + + + + + -
+
`; -exports[`home isNewKibanaInstance should set isNewKibanaInstance to true when there are no index patterns 1`] = ` - - - -

- -

-
- - - - - +
+ + -

+

-

+
- - - +
+ + + + + Add data + + + + +
+
+ +
+ 0 + + + - + + +
+ +`; + +exports[`home isNewKibanaInstance should set isNewKibanaInstance to true when there are no index patterns 1`] = ` +
+
+
+ + -

+

-

+
- - - +
+ + + + + Add data + + + + +
+
+
+
+ 0 + + + + + + -
+
`; exports[`home should render home component 1`] = ` - - - -

- -

-
- - - - - +
+ + -

+

-

+
- - - -
- - + - -

- -

-
- - -
+ + + Add data + + +
+ + +
+ +
+ 0 + + + + + + -
+ `; exports[`home welcome should show the normal home page if loading fails 1`] = ` - - - -

- -

-
- - - - - +
+ + -

+

-

+
- - - -
- - + - -

- -

-
- - -
+ + + Add data + + +
+ + +
+ +
+ 0 + + + + + + -
+ `; exports[`home welcome should show the normal home page if welcome screen is disabled locally 1`] = ` - - - -

- -

-
- - - - - +
+ + -

+

-

+
- - - -
- - + - -

- -

-
- - -
+ + + Add data + + +
+ + +
+ +
+ 0 + + + + + + -
+ `; exports[`home welcome should show the welcome screen if enabled, and there are no index patterns defined 1`] = ` @@ -1071,116 +1620,107 @@ exports[`home welcome should show the welcome screen if enabled, and there are n `; exports[`home welcome stores skip welcome setting if skipped 1`] = ` - - - -

- -

-
- - - - - +
+ + -

+

-

+
- - - -
- - + - -

- -

-
- - -
+ + + Add data + + +
+ + +
+ +
+ 0 + + + + + + -
+ `; diff --git a/src/plugins/home/public/application/components/__snapshots__/synopsis.test.js.snap b/src/plugins/home/public/application/components/__snapshots__/synopsis.test.js.snap index d757d6a8b73051..190985f70659d4 100644 --- a/src/plugins/home/public/application/components/__snapshots__/synopsis.test.js.snap +++ b/src/plugins/home/public/application/components/__snapshots__/synopsis.test.js.snap @@ -4,7 +4,7 @@ exports[`props iconType 1`] = ` `; @@ -24,7 +25,7 @@ exports[`props iconUrl 1`] = ` `; @@ -44,11 +46,12 @@ exports[`props isBeta 1`] = ` `; @@ -57,11 +60,12 @@ exports[`render 1`] = ` `; diff --git a/src/plugins/home/public/application/components/_add_data.scss b/src/plugins/home/public/application/components/_add_data.scss index 836b34227a37c2..e588edfe352406 100644 --- a/src/plugins/home/public/application/components/_add_data.scss +++ b/src/plugins/home/public/application/components/_add_data.scss @@ -1,63 +1,22 @@ -.homAddData__card { - border: none; - box-shadow: none; -} - -.homAddData__cardDivider { - position: relative; - - &:after { - position: absolute; - content: ''; - width: 1px; - right: -$euiSizeS; - top: 0; - bottom: 0; - background: $euiBorderColor; - } -} - -.homAddData__icon { - width: $euiSizeXL * 2; - height: $euiSizeXL * 2; -} - -.homAddData__footerItem--highlight { - background-color: tintOrShade($euiColorPrimary, 90%, 70%); - padding: $euiSize; -} - -.homAddData__footerItem { - text-align: center; -} - -.homAddData__logo { - margin-left: $euiSize; -} - -@include euiBreakpoint('xs', 's') { - .homeAddData__flexGroup { - flex-wrap: wrap; - } -} - -@include euiBreakpoint('xs', 's', 'm') { - .homAddDat__flexTablet { - flex-direction: column; - } - - .homAddData__cardDivider:after { - display: none; - } - - .homAddData__cardDivider { - flex-grow: 0 !important; - flex-basis: 100% !important; - } -} - -@include euiBreakpoint('l', 'xl') { - .homeAddData__flexGroup { - flex-wrap: nowrap; - } +/* + * 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. + */ + +.homDataAdd__content .euiIcon__fillSecondary { + fill: $euiColorDarkestShade; } diff --git a/src/plugins/home/public/application/components/_home.scss b/src/plugins/home/public/application/components/_home.scss index 4101f6519829b7..d9b7602971e8dd 100644 --- a/src/plugins/home/public/application/components/_home.scss +++ b/src/plugins/home/public/application/components/_home.scss @@ -1,5 +1,73 @@ -@include euiBreakpoint('xs', 's', 'm') { - .homHome__synopsisItem { - flex-basis: 100% !important; +/* + * 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. + */ + +// Local page variables +$homePageWidth: 1200px; + +.homWrapper { + background-color: $euiColorEmptyShade; + display: flex; + flex-direction: column; + min-height: calc(100vh - #{$euiHeaderHeightCompensation}); +} + +.homHeader { + background-color: $euiPageBackgroundColor; + border-bottom: $euiBorderWidthThin solid $euiColorLightShade; +} + +.homHeader__inner { + margin: 0 auto; + max-width: $homePageWidth; + padding: $euiSizeXL $euiSize; + + .homHeader--hasSolutions & { + padding-bottom: $euiSizeXL + $euiSizeL; + } +} + +#homHeader__title { + @include euiBreakpoint('xs', 's') { + text-align: center; + } +} + +.homHeader__actionItem { + @include euiBreakpoint('xs', 's') { + margin-bottom: 0 !important; + margin-top: 0 !important; + } +} + +.homContent { + margin: 0 auto; + max-width: $homePageWidth; + padding: $euiSizeXL $euiSize; + width: 100%; +} + +.homData--expanded { + flex-direction: column; + + &, + & > * { + margin-bottom: 0 !important; + margin-top: 0 !important; } } diff --git a/src/plugins/home/public/application/components/_index.scss b/src/plugins/home/public/application/components/_index.scss index 870099ffb350e4..a0547a45885611 100644 --- a/src/plugins/home/public/application/components/_index.scss +++ b/src/plugins/home/public/application/components/_index.scss @@ -5,9 +5,11 @@ // homChart__legend--small // homChart__legend-isLoading -@import 'add_data'; @import 'home'; +@import 'add_data'; +@import 'manage_data'; @import 'sample_data_set_cards'; +@import 'solutions_section'; @import 'synopsis'; @import 'welcome'; diff --git a/src/plugins/home/public/application/components/_manage_data.scss b/src/plugins/home/public/application/components/_manage_data.scss new file mode 100644 index 00000000000000..389d8c8b3bf0f0 --- /dev/null +++ b/src/plugins/home/public/application/components/_manage_data.scss @@ -0,0 +1,22 @@ +/* + * 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. + */ + +.homDataManage__content .euiIcon__fillSecondary { + fill: $euiColorDarkestShade; +} diff --git a/src/plugins/home/public/application/components/_solutions_section.scss b/src/plugins/home/public/application/components/_solutions_section.scss new file mode 100644 index 00000000000000..be693707e06b4c --- /dev/null +++ b/src/plugins/home/public/application/components/_solutions_section.scss @@ -0,0 +1,122 @@ +/* + * 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. + */ + +.homSolutions { + margin-top: -($euiSizeXL + $euiSizeL + $euiSizeM); +} + +.homSolutions__content { + min-height: $euiSize * 16; + + @include euiBreakpoint('xs', 's') { + flex-direction: column; + } +} + +.homSolutions__group { + max-width: 50%; + + @include euiBreakpoint('xs', 's') { + max-width: none; + } +} + +.homSolutionPanel { + border-radius: $euiBorderRadius; + color: inherit; + flex: 1; + transition: all $euiAnimSpeedFast $euiAnimSlightResistance; + + &:hover, + &:focus { + @include euiSlightShadowHover; + transform: translateY(-2px); + + .euiTitle { + text-decoration: underline; + } + } + + &, + .euiPanel { + display: flex; + flex-direction: column; + } + + .euiPanel { + overflow: hidden; + } +} + +.homSolutionPanel__header { + color: $euiColorEmptyShade; + padding: $euiSize; +} + +.homSolutionPanel__icon { + background-color: $euiColorEmptyShade !important; + box-shadow: none !important; + margin: 0 auto $euiSizeS; + padding: $euiSizeS; +} + +.homSolutionPanel__subtitle { + margin-top: $euiSizeXS; +} + +.homSolutionPanel__content { + flex-direction: column; + justify-content: center; + padding: $euiSize; + + @include euiBreakpoint('xs', 's') { + text-align: center; + } +} + +.homSolutionPanel__header { + background-color: $euiColorPrimary; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQYAAAFjCAYAAADfDKXVAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAfiSURBVHgB7d1JblRZGobh45SrhJiWGOQ6EENLSKyABTBmOcwRq2EnTKtUdInBuEln/OkMdB3HTTjiNqd5HinA3YBUSq++24UPUkq/J+jE+/fv09HRURrD27dv0+vXr1OLfksAG4QByAgDkBEGICMMQEYYgIwwABlhADLCAGSEAcgIA5ARBiAjDEBGGICMMAAZYQAywgBkhAHICAOQEQZg05/CAGy6FAYgIwzApnNhADLCAGy6EAZgkzAAGVclgIyTj0DGYgAyZ8IADP2ZLAZgw1n8IQzA0Hn8IQzA0EX8IQzAkEMJICMMwDURhcv44DDBlp4+fZrevHmTxvLq1av04cOHRDHO1h8IA1t7/PhxOjo6SjTrdP2BQwlg7ddiEAYgxGXK8/UnwgCEs+EnwgCEk+EnwgCE0+EnwgDEYcTF8AvCAJxufkEYgO+bXxAG6Nu1y5RrwgB9O7npi8IAfTu+6YvCAP3KrkasCQP06/i2bwgD9Ov0tm8IA/QpLlFe3PZNYYA+/bjrm8IA/YmTjqd3/YAwQH+O7/sBYYC+xHmFH/f9kDBAX/7Y5oeEAfqx1VoIwgD9+L7tDwoD9GHrtRCEAfoQ5xYutv1hYYD2PWgtBGGA9m11JWJIGKBtD14LQRigbf9POxAGaNedT1DeRRigTRGEb2lHwgBtetDlyU3CAO35mXY44TgkDNCWy9XrS9qTMEBb9jqEWBMGaEdchbj3TVi2IQzQhr2uQmwSBmjD5zTCIcSaMED94rzCaRqRMEDd4tLkaIcQa8IA9YpDh70vTd5EGKBOcb9CPCA12nmFIWGAOo16snGTMEB94mTjSZqQMEBdIgqjn2zcJAxQj7ircfIoBGGAOsTtzl/TTIQByhe/nXqSy5K3EQYoW0Rhp/dt3MdhAkoVdzV+Slf3LMxKGKBMcU5h1sOHIYcSUJ5FoxAsBijLLPcp3EcYoBxxOXKUd2DalzDA8uLk4sc08nsq7EMYYFnxINRkT0nuShhgOfEgVDwlOfvlyPsIAyyjmPMJNxEGmFccMsRKKOZ8wk2EAeZT7KHDJmGA6UUI4v6EYg8dNgkDTCued4i7GIu66nAfYYBpVLcShoQBxlflShgSBhhP1SthSBhgHNWvhCFhgP00sxKGhAF29OTJkyKfcxiDN2qBHb18+TIOH5qLQhAGICMMQEYYgIwwABlhADLCAGSEAcgIA5ARBiAjDEBGGICMMNCTy0ePHhX/RqwlEAZ6EQ88/e/Zs2dniXt57JrWNfl+CVMTBloW6+BTavTR6CkJA62KlfAtsRNhoDWxEr788zc7EgZaYiWMRBhogZUwMmGgdlbCBISBWlkJExIGamQlTEwYqImVMBNhoBZx5+LXxCyEgdLFXYufV6/TxGyEgZLFSojzCZ6InJkwUCIrYWHCQGmshAIIA6WwEgoiDJTASiiMMLCYw8NDK6FQ3tqNxbx79+5jEoUiCQOLef78uUOHQgkDkBEGICMMQEYYgIwwABlhADLCAGSEAcgIA5ARBiAjDEBGGNjWxYsXL/5IdOFg9fo9wd3+fr+Ey8vLf63+/k8az38PDg5m/RX1q/+G+Pf/O43j++rf/yU1yPsxcBfvl9ApYeA23lWpY8LAJisBYeAaK4G/CQMhQhAr4SRBEgauYhBRsBL4RRj6ZSVwK2Hok5XAnYShL1YCWxGGflgJbE0Y2mcl8GDC0DYrgZ0IQ5usBPYiDO35uXrFE3+zPrVIW4ShHbES4nbm4wR7EoY2WAmMShjqZiUwCWGol5XAZIShPlYCkxOGulgJzEIY6mAlMCthKJ+VwOyEoVxWAosRhjJZCSxKGMpiJVAEYSiHlUAxhGF5VgLFEYZlWQkUSRiWYSVQNGGYn5VA8YRhPlYC1RCGeVgJVEUYpmUlUCVhmI6VQLWEYXxWAtUThnFZCTRBGMZhJdAUYdiflUBzhGF3VgLNEobdnK1en5KVQKOE4eFiJXxL0DBh2F6shC///A1NE4btWAl0RRjuZiXQJWG4nZVAt4QhZyXQPWG4zkqAJAxrVgIMCMPVnYtfE/BLz2GIuxY/r16nCbim1zDESojzCZeJ3sTh4lj/389To3oLg5XQuYODA4eNW+gpDFYCbKmHMFgJ8ECth8FKgB20GgYrAfbQYhisBNhTS2GwEmAkrYTBSoAR1R4GKwEmUHMYrASYSI1hsBJgYrWFwUqAGdQSBisBZlRDGKwEmFnJYbASYCGlhsFKgAWVFgYrAQpQUhisBChECWGwEqAwS4fBSoACLRUGKwEKtkQYrAQo3JxhsBKgEnOFwUqAikwdhghBrISTBFRjyjBEDCIKVgJUZoowOJcAlRs7DM4lQAPGCsPP1etbshKgCfuGIQ4bYiH8SEAzdg1DHCrEQjhODhugOQ8NgyBAB7YNgyBAR+4LgyBAh24Lg6sM0LFhGGIRfE9XdywKAnQswrBeB2fJ4QKQrsLwMQEM/JYANggDkBEGICMMQEYYgIwwABlhADLCAGSEAcgIA5ARBiAjDEBGGICMMAAZYQAywgBkhAHICAOQEQYgIwxARhiAjDAAGWEAMg/9bdf0LX4h0XkCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACq8hdouPfUCk+KHQAAAABJRU5ErkJggg=='), + url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAWgAAAFKCAYAAAAwgcXoAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAjGSURBVHgB7d27ctRaFoDh1WBzsQ9wLkVsngHHfglSIOUdgJQiJSfmIUjhJUwGjiGaU7hmzoB7vLA1GPCt21vSlvR9Vao+5lAFbbf+EktbUgQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABUaxYAEzCfz/NldX9biTK+zGaz/0aLSv1FAYbg6v52I8r4e39rNdCXAoAqCTRApQQaoFICDVApgQaolEADVEqgASol0ACVEmiASgk0QKUEGqBSAg1QKYEGqJRAA1RKoAEqJdAAlRJogEp5ogowJV/3t/9EGV8DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFjELABOMZ/Po6TZrL/sDO29rATA6Vb3t/UoY29/+1f04DDO1w63Ev59uLVGoIGz5GHi9Sjja/QU6EPZvFLv5Uu07FIAUCWBBqiUQANUSqABKiXQAJUSaIBKCTRApQQaoFICDVApgQaolEADVEqgASol0ACVEmiASgl0ANRJoAEqJdAAlfJEFeAs+RSUv6OMsg8FXNw/Ue69/BMAAAAAAAAAAAAAAAAAAAAAQCdmAcBpspNn3flzL1q4EZRAA2PRhLR5vXz4683ryk+/7+j/ixO+vqiM9t6Rr78e899ffvq9za9/FWigdhnNbNVqfA/wyuHr5fgxxqMi0EDfMq4Z3ya4K4fbaMN7XgINdKGJ8OUjW/M1JxBooKSj4c3X5mhYa5bgmwYsoxlHXA0hbo1vJnCW5gRds10Jo4lOCDTws4xvRjiPjvOoeDXohUDDtB09Or4W31dTUAE/CJiWJshNjK8E1RJoGDdBHjCBhvHJCBtZjIAfHAxf7scZ46uHr/brkfCDhGFqVllcCWOL0RJoGIajs+S16Hjfnc3K/XHzefG7cp7b0N6HQEO9qhhdbG5uxosXL6KET58+xb1796IPGef79+/Ho0ePooSXL1/Gq1evok0rAdSkiXIeJVdxgm9tbS22traihJ2dnejTxsZGsffy+vXraJtAQ/+qizJ1EGjoRzNTvhGizAkEGrqVKy56OdHH8Ag0tC9vPpRBXg9RZgECDe3IEGeU82jZOmWWItBQlhEGxQg0XFyG+Lc4CLN9imJ8mGB5ebScc+W8kMTRMsUJNCzGbJnOCDScj5UYdE6g4XR5lHwjHC3TA4GGXx299FqY6Y1Aw3fNagxjDKog0CDMVEqgmbIcX+QY43pAhQSaKXLij0EQaKZEmJfU52OqShvSexFopkCYL2B7ezsePnwYJezu7kZfMsz5FJQPHz5ECe/evYu2OSHCmAkzgybQjJEwMwoCzZgIM6Mi0IxB3ifjZhxc/QejIdAMWX5+84h5PWCEBJohcuUfk+DDzdBklPOo2WeX0fMhZyjyxN/vcTBvhkkQaGq3GgcnAK3MYHIEmlo5AcjkCTQ1MmeGsANQlxxj5DhjNQCBpgr5ObwV7ssMPxBo+macASewU9AX982AMwg0XbM6A85JoOmSi01gAQJNF9xtDpYg0LTNSUBYkp2GtuRRc44znASEJQk0bXDUDAXYgSjJUTMUJNCU4qgZCrMzcVGOmqElAs1F5LK5jLPPEbTAjsUyXA0IHRBoFuVqQOiIQLOIPGr+LZic2axcKubzefSl5PtIbb+XSwFny6Plv0KcJ2lrayv29vaKbO/fv4++ZJyfPHlS7L08fvw42rYScDonAqEnAs1JnAiEngk0x8mRxh/h2YDQK4HmZ2txcGtQIw3omUBzVIbZSAMqIdAkIw2okEDjwhOolEBPW44zbgZQJYGepjwBeGt/ux5AtQR6epqrAo00oHICPS05b/4zLKGDQRDo6TBvhoER6GmwvhkGSKDHzfpmGDCBHi8nA2HgBHqcnAyEERDo8XEyEEZCoMfFI6loRZ+PqSptSO/FP4HHI++n4cpAWrGxsRGl7OzsRF/W1tbi9u3bUcLHjx9jd3c32iTQw5c/wzwZaKUGjIxAD5uVGjBiAj1c4gwjJ9DDlOOMjLOfH4yYHXx4PDMQJsJOPiwZ51sBTIJAD4c1zjAxAj0M4gwTJND1E2eYKIGum/s4w4QJdL1cug0TJ9B1EmdAoCskzsA3Al0XcQb+T6DrIc7ADwS6DuIM/EKg+yfOwLEEul/iDJxIoPsjzsCpLgV9yCsExRk4lUB3L++t4fJt4ExGHN1y4yPg3AS6O+IMLESguyHOwMLMoNuXj6kSZ2BhAt2ua+EZgsCSBLo9q3Gw1hlgKStBGy7vb3+EGT8jsL6+Hpubm1HK27dvoy8bGxvfthJ2dna+bW0S6PIyzn8dvsLg3b17N968eRMlZNDu3LkTfZjNZvHgwYN49uxZlPD06dN4/vx5tMmIo6w8YhZnoAiBLuvPEGegEIEuJ++vcSUAChHoMtxfAyhOoC8uw+xCFKA4gb6YXOt8MwBaINDLa9Y6A7RCoJdjOR3QOoFeTl7CLc5AqwR6cbli41oAtEygF2PFBtAZgT4/KzaATgn0+VixAXROoM8n4+ykINApgT5bjjVWA6BjAn269XCPDaAnAn2yHGk4KQj8Yj6fRxc8UeV4zVNRYPJ2d3dje3s7Smj7EVFn+fz5c5H3koHO70vbPDPveHlS0MUoQK+MOH7lSkGgCgL9o3wiiisFgSoI9Hc5d/49ACoh0N+5Qx1QFYE+kHNnD3wFqiLQ5s5ApaYeaHNnoFpTD3SONsydgSpNOdB5j43rAVCpqQbafTaA6k010O6zAVRvioE2dwYGYWqBtqQOGIwpBdqSOmBQphRoow1gUKYS6OthSR0wMFMIdB413wiAgZlCoI02gEEae6CNNoDBGnOgjTaAQRtzoI02gEEba6CNNoDBG2OgZ2G0AYzA/wBP5hsF50HhogAAAABJRU5ErkJggg=='); + background-repeat: no-repeat; + background-position: top 0 left 0, bottom 0 right 0; + background-size: $euiSizeXL * 4, $euiSizeXL * 6; + + .homSolutionPanel--enterpriseSearch & { + background-color: $euiColorSecondary; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAABKCAYAAADJwhY8AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAANMSURBVHgBzZp7buIwEMYHKC1QuqsF7Up7Hs4Al+Ff4DCcAonrtPuAPjcL1B/YyA0p2JkZK580SuQm6Y+xx4+xiSqumrG+d7/z/rYx9t+WZdZ2lFhXxq4jngfkP2Ov9qquq8jnm9Zu6eBNgD6TImwsoC80ibY1NIe1sRcSVsPYHfFVN9Yy1jG2pUPbFZEUoJMP+kYHWJakAZ0AemuvrPZZJ10B8jsdHFFKWh70BSe07X1GkUoBCCHib+w1qsq1qzivLkU6JDUgTSaT7m63C4ZMCmjgaDwe4zYYMhmgB+cEyPal95IAFsA5fTGQZ4dbdcAzcO7/9wxk7bMH8AfMTvJDUsO+fG2tSfJwvp5qtdqKPgEMEYABekeBo0IEnNODgTzpI0MBfaFhnwUtAQdlBvA+X1gG0Kmw0y0J57QykE9+ASdIHo3hF29cARMO6uYDhhvFGPwfjG0E4BxPxy/gVPFR8/m8MRwOsTosPa3y9KEtigBCpmoAJwV5jGixjtp8EG3xD8noOASKjiT2V6+Jr5YLFo2hDuvkDfEErqa7EZXx4naxWKyIr9b+e6QkU0U/iBcw+2jWnM08E09NtMMqA0JNNUC0ReInlRraE9bodXBOzaoD1rQBuVku9SpmZ7eSL9wjVa864LbqgOpVzJ4bagNyv7/RBozZgymSOuAN8bRTA7RrFM4+DJRpepBbvZAqIDf3jQmrThXbxCS3i5FdduYksXOAjUp5QJt75npvK75whwwclopd4uvV3YgB2m7lG8nouK0rAojkEcnlZTZ+plUCsDEajfom9SYBB31InXA/is64h+8sl0vKsowGgwExBO/99QvKDkXISHQpFxCz2Wx/nU6nVFIniacyHkTOpGevJ2J48sR7UKgHnceCRoiSnvxVVOj2P6C6ZyjHVKljwb7a54KDKtKT2MgpPDECD/ZJSYGedEdaCqW+437BkzgcdG/zOIVKciTgDOTawL2dezfVmYUiyHV+V6lIyQAhDxJwjyHvoPv4SWmFgAiCg1JnFqLgIO6qK1SIVowS0afjUgCin/tNJZOZ2oCIUlRr6aOlWoDwFjZz2CczpQHhKXjsYv8WKilAeAwBgL0R0ZPCHECAvFirzCFb/5hyknPV7zL4DLH0CVGgAAAAAElFTkSuQmCC'), + url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAA+CAYAAACbQR1vAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACBSURBVHgB7dpBDcAwDMVQdyzGH9xojEQjtbIfBEu5fGUx62WPjyEPcgVArgDIFQC5AiBXAOQKgFwBkCsAcgVATh9gsW+4vFIngFwBkCsAcgVArgDIFQC5AiBXAORahJjVm9zpCoBcAZArAHIFQK4AyBUAuQIgVwDkCoBcAZDTB/gBLrgCk8OhuvYAAAAASUVORK5CYII='); + background-position: top $euiSizeS left 0, bottom $euiSizeS right $euiSizeS; + background-size: $euiSize * 1.25, $euiSizeXL; + } + + .homSolutionPanel--observability & { + background-color: $euiColorAccent; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAADNSURBVHgB7duxCcIAEEDR08IFrGyz/0pZwVYEIcYF/FWKwHtwXP/hyrvMQbZtu+3rPid3Hf4SKAgUBAoCBYGCQEGgIFAQKAgUBAoCBYGCQEGgIFAQKAgUBAoCBYGCQEGgIFAQKAgUBAoCBYGCQEGgIFAQKAgUBAoCBYGCQEGgIFAQKAgUBAoCBYGCQEGgcJmDnt6WZfms6/qak/sFeswx3vs85+ScWBAoCBQECgIFgYJAQaAgUBAoCBQECgIFgYJAQaAgUBAoCBQECgKFLx4KCqcIFEJnAAAAAElFTkSuQmCC'); + background-position: top $euiSizeS right $euiSizeS; + background-size: $euiSizeL * 1.5; + } + + .homSolutionPanel--securitySolution & { + background-color: $euiColorDarkestShade; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGIAAABiCAYAAACrpQYOAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAKRSURBVHgB7d3tSgMxEIXhMeqPQoWC9f4vte5Z3dLvTbKZzCQ5LyxqKwg+tNjdUxQR+RJmXpiOvRDDvPD/kRjGhYvPgbETZlK4+fogxDApPLiNGAaFJ7cD40NYtcKL+76FGNUKK/cRo1Ih4n5gvAtTbQ1i+R5iKBcDgYBADMViIRAxFEuBQMRQKhUCEUOhHAi0YLwJK1IuBCJGwbZAoE8hRpG2QqAFg22oBAQCxkFYdqUgEE6dEyOzkhCIGJmVhkDA4PXvxDQgEMcIiWlBIGIkpAmBiBGZNgQiRkQ1IBAxVqoFgThge1FNCMTN1JNqQyBiPMgCAnHAdpMVBOJm6iJLCA7YLrKEWH4+r3+LPYQIMeY8QKDhlyFeINDQGJ4g0LAY3iDQkBgeIdBwmymvEGgoDM8QaJgBm3cINMSArQUI1P2ArRUI1PVmqiUI1C1GaxCoywFbixCouzFCqxCIyxBPnU6nLjBafkQs7YExHdJyPUCg+WmqZYxeIBAwmv3TticItJseFYcWHxm9QaD5RV9rGD1CIGAcJ4xmztr2CoHms7atYPQMgc4Y3p+qeodAwPiZjnfPGCNAoPMgwSvGKBDINcZIEMgtxmgQyCXGiBDIHcaoEAgYx+n48IAxMgQ6v1nGGmN0COQCgxB/4feAF307KwxCXDe/9dgCgxD3mWAQ4nHAqHrplRDPq3odnBCvq4ZBiPWqYBAiLnUMQsSnikGItObtlAYGIdJTmesQIq/iGITIr+h2ihDbKradIsT2imynCFGmzdspQpRr03VwQpQtG4MQ5cvCIIROyRiE0CsJgxC6RW+nCKFf1FyHEHVaxSBEvV5upwhRv4dzHULYdIdBCLuutlOEsO18HZz/u8E+YMgvrbKfmp8y7IEAAAAASUVORK5CYII='); + background-position: top $euiSizeS left $euiSizeS; + background-size: $euiSizeL * 2; + } +} diff --git a/src/plugins/home/public/application/components/_synopsis.scss b/src/plugins/home/public/application/components/_synopsis.scss index 49e71f159fe6ff..3eac2bc9705e01 100644 --- a/src/plugins/home/public/application/components/_synopsis.scss +++ b/src/plugins/home/public/application/components/_synopsis.scss @@ -5,6 +5,10 @@ box-shadow: none; } + .homSynopsis__cardTitle { + display: flex; + } + // SASSTODO: Fix in EUI .euiCard__content { padding-top: 0 !important; diff --git a/src/plugins/home/public/application/components/add_data.js b/src/plugins/home/public/application/components/add_data.js deleted file mode 100644 index c35b7b04932fbd..00000000000000 --- a/src/plugins/home/public/application/components/add_data.js +++ /dev/null @@ -1,320 +0,0 @@ -/* - * 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 React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; -import { getServices } from '../kibana_services'; - -import { - EuiButton, - EuiLink, - EuiPanel, - EuiTitle, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiCard, - EuiIcon, - EuiHorizontalRule, - EuiFlexGrid, -} from '@elastic/eui'; - -const AddDataUi = ({ apmUiEnabled, isNewKibanaInstance, intl, mlEnabled }) => { - const basePath = getServices().getBasePath(); - - const renderCards = () => { - const apmData = { - title: intl.formatMessage({ - id: 'home.addData.apm.nameTitle', - defaultMessage: 'APM', - }), - description: intl.formatMessage({ - id: 'home.addData.apm.nameDescription', - defaultMessage: - 'APM automatically collects in-depth performance metrics and errors from inside your applications.', - }), - ariaDescribedby: 'aria-describedby.addAmpButtonLabel', - }; - const loggingData = { - title: intl.formatMessage({ - id: 'home.addData.logging.nameTitle', - defaultMessage: 'Logs', - }), - description: intl.formatMessage({ - id: 'home.addData.logging.nameDescription', - defaultMessage: - 'Ingest logs from popular data sources and easily visualize in preconfigured dashboards.', - }), - ariaDescribedby: 'aria-describedby.addLogDataButtonLabel', - }; - const metricsData = { - title: intl.formatMessage({ - id: 'home.addData.metrics.nameTitle', - defaultMessage: 'Metrics', - }), - description: intl.formatMessage({ - id: 'home.addData.metrics.nameDescription', - defaultMessage: - 'Collect metrics from the operating system and services running on your servers.', - }), - ariaDescribedby: 'aria-describedby.addMetricsButtonLabel', - }; - const siemData = { - title: intl.formatMessage({ - id: 'home.addData.securitySolution.nameTitle', - defaultMessage: 'SIEM + Endpoint Security', - }), - description: intl.formatMessage({ - id: 'home.addData.securitySolution.nameDescription', - defaultMessage: - 'Protect hosts, analyze security information and events, hunt threats, automate detections, and create cases.', - }), - ariaDescribedby: 'aria-describedby.addSiemButtonLabel', - }; - - const getApmCard = () => ( - - {apmData.description}} - footer={ - - - - } - /> - - ); - - return ( - - - - - - - - - -

- -

-
-
-
- - - {apmUiEnabled !== false && getApmCard()} - - - {loggingData.description} - } - footer={ - - - - } - /> - - - - {metricsData.description} - } - footer={ - - - - } - /> - - -
- - - - - - - - -

- -

-
-
-
- - {siemData.description}} - footer={ - - - - } - /> -
-
- ); - }; - - const footerItemClasses = classNames('homAddData__footerItem', { - 'homAddData__footerItem--highlight': isNewKibanaInstance, - }); - - return ( - - {renderCards()} - - - - - - - - - - - - - - - {mlEnabled !== false ? ( - - - - - - - - - - - ) : null} - - - - - - - - - - - - - ); -}; - -AddDataUi.propTypes = { - apmUiEnabled: PropTypes.bool.isRequired, - mlEnabled: PropTypes.bool.isRequired, - isNewKibanaInstance: PropTypes.bool.isRequired, -}; - -export const AddData = injectI18n(AddDataUi); diff --git a/src/plugins/home/public/application/components/add_data.test.js b/src/plugins/home/public/application/components/add_data.test.js deleted file mode 100644 index 9457f766409b8f..00000000000000 --- a/src/plugins/home/public/application/components/add_data.test.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 React from 'react'; -import { AddData } from './add_data'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { getServices } from '../kibana_services'; - -jest.mock('../kibana_services', () => { - const mock = { - getBasePath: jest.fn(() => 'path'), - }; - return { - getServices: () => mock, - }; -}); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -test('render', () => { - const component = shallowWithIntl( - - ); - expect(component).toMatchSnapshot(); // eslint-disable-line - expect(getServices().getBasePath).toHaveBeenCalledTimes(1); -}); - -test('mlEnabled', () => { - const component = shallowWithIntl( - - ); - expect(component).toMatchSnapshot(); // eslint-disable-line - expect(getServices().getBasePath).toHaveBeenCalledTimes(1); -}); - -test('apmUiEnabled', () => { - const component = shallowWithIntl( - - ); - expect(component).toMatchSnapshot(); // eslint-disable-line - expect(getServices().getBasePath).toHaveBeenCalledTimes(1); -}); - -test('isNewKibanaInstance', () => { - const component = shallowWithIntl( - - ); - expect(component).toMatchSnapshot(); // eslint-disable-line - expect(getServices().getBasePath).toHaveBeenCalledTimes(1); -}); diff --git a/src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap b/src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap new file mode 100644 index 00000000000000..787802e508ca7b --- /dev/null +++ b/src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap @@ -0,0 +1,96 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AddData render 1`] = ` +
+ + + +

+ +

+
+
+ + + + + +
+ + + + + + + + + + + + +
+`; diff --git a/src/plugins/home/public/application/components/add_data/add_data.test.tsx b/src/plugins/home/public/application/components/add_data/add_data.test.tsx new file mode 100644 index 00000000000000..e76e8348022844 --- /dev/null +++ b/src/plugins/home/public/application/components/add_data/add_data.test.tsx @@ -0,0 +1,95 @@ +/* + * 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. + */ + +/* + * 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 React from 'react'; +import { AddData } from './add_data'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; + +jest.mock('../app_navigation_handler', () => { + return { + createAppNavigationHandler: jest.fn(() => () => {}), + }; +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +const addBasePathMock = jest.fn((path: string) => (path ? path : 'path')); + +const mockFeatures = [ + { + category: 'data', + description: 'Ingest data from popular apps and services.', + homePageSection: 'add_data', + icon: 'indexOpen', + id: 'home_tutorial_directory', + order: 500, + path: '/app/home#/tutorial_directory', + title: 'Ingest data', + }, + { + category: 'admin', + description: 'Add and manage your fleet of Elastic Agents and integrations.', + homePageSection: 'add_data', + icon: 'indexManagementApp', + id: 'ingestManager', + order: 510, + path: '/app/ingestManager', + title: 'Add Elastic Agent', + }, + { + category: 'data', + description: 'Import your own CSV, NDJSON, or log file', + homePageSection: 'add_data', + icon: 'document', + id: 'ml_file_data_visualizer', + order: 520, + path: '/app/ml#/filedatavisualizer', + title: 'Upload a file', + }, +]; + +describe('AddData', () => { + test('render', () => { + const component = shallowWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/home/public/application/components/add_data/add_data.tsx b/src/plugins/home/public/application/components/add_data/add_data.tsx new file mode 100644 index 00000000000000..82f0020b1b3895 --- /dev/null +++ b/src/plugins/home/public/application/components/add_data/add_data.tsx @@ -0,0 +1,95 @@ +/* + * 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 React, { FC } from 'react'; +import PropTypes from 'prop-types'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +// @ts-expect-error untyped service +import { FeatureCatalogueEntry } from '../../services'; +import { createAppNavigationHandler } from '../app_navigation_handler'; +// @ts-expect-error untyped component +import { Synopsis } from '../synopsis'; + +interface Props { + addBasePath: (path: string) => string; + features: FeatureCatalogueEntry[]; +} + +export const AddData: FC = ({ addBasePath, features }) => ( +
+ + + +

+ +

+
+
+ + + + + + +
+ + + + + {features.map((feature) => ( + + + + ))} + +
+); + +AddData.propTypes = { + addBasePath: PropTypes.func.isRequired, + features: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, + showOnHomePage: PropTypes.bool.isRequired, + category: PropTypes.string.isRequired, + order: PropTypes.number, + }) + ), +}; diff --git a/src/plugins/home/public/application/components/add_data/index.ts b/src/plugins/home/public/application/components/add_data/index.ts new file mode 100644 index 00000000000000..a7d465d1776366 --- /dev/null +++ b/src/plugins/home/public/application/components/add_data/index.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 * from './add_data'; diff --git a/src/plugins/home/public/application/components/app_navigation_handler.ts b/src/plugins/home/public/application/components/app_navigation_handler.ts index 6e78af7f42f52b..61d85c033b5448 100644 --- a/src/plugins/home/public/application/components/app_navigation_handler.ts +++ b/src/plugins/home/public/application/components/app_navigation_handler.ts @@ -17,6 +17,7 @@ * under the License. */ +import { MouseEvent } from 'react'; import { getServices } from '../kibana_services'; export const createAppNavigationHandler = (targetUrl: string) => (event: MouseEvent) => { diff --git a/src/plugins/home/public/application/components/feature_directory.js b/src/plugins/home/public/application/components/feature_directory.js index e9ab348f164c72..36ececcdfd8df5 100644 --- a/src/plugins/home/public/application/components/feature_directory.js +++ b/src/plugins/home/public/application/components/feature_directory.js @@ -115,6 +115,7 @@ export class FeatureDirectory extends React.Component { return ( { - const { addBasePath, directories } = this.props; - return directories - .filter((directory) => { - return directory.showOnHomePage && directory.category === category; - }) - .map((directory) => { - return ( - - - - ); - }); - }; + findDirectoryById = (id) => this.props.directories.find((directory) => directory.id === id); + + getFeaturesByCategory = (category) => + this.props.directories + .filter((directory) => directory.showOnHomePage && directory.category === category) + .sort((directoryA, directoryB) => directoryA.order - directoryB.order); renderNormal() { - const { apmUiEnabled, mlEnabled } = this.props; + const { addBasePath, solutions } = this.props; + + const devTools = this.findDirectoryById('console'); + const stackManagement = this.findDirectoryById('stack-management'); + const advancedSettings = this.findDirectoryById('advanced_settings'); + + const addDataFeatures = this.getFeaturesByCategory(FeatureCatalogueCategory.DATA); + const manageDataFeatures = this.getFeaturesByCategory(FeatureCatalogueCategory.ADMIN); + + // Show card for console if none of the manage data plugins are available, most likely in OSS + if (manageDataFeatures.length < 1 && devTools) { + manageDataFeatures.push(devTools); + } return ( - - - -

- -

-
- - - - - - - - - -

- -

+
+
+
+ + + +

+ +

- - - {this.renderDirectories(FeatureCatalogueCategory.DATA)} - - +
+ + + + + + {i18n.translate('home.pageHeader.addDataButtonLabel', { + defaultMessage: 'Add data', + })} + + + + {stackManagement ? ( + + + {i18n.translate('home.pageHeader.stackManagementButtonLabel', { + defaultMessage: 'Manage', + })} + + + ) : null} + + {devTools ? ( + + + {i18n.translate('home.pageHeader.devToolsButtonLabel', { + defaultMessage: 'Dev tools', + })} + + + ) : null} + + +
+
+
+ +
+ {solutions.length && } + + + + + - - -

- -

-
- - - {this.renderDirectories(FeatureCatalogueCategory.ADMIN)} - -
+
- +
+
); } @@ -260,13 +296,23 @@ Home.propTypes = { path: PropTypes.string.isRequired, showOnHomePage: PropTypes.bool.isRequired, category: PropTypes.string.isRequired, + order: PropTypes.number, + }) + ), + solutions: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + subtitle: PropTypes.string.isRequired, + descriptions: PropTypes.arrayOf(PropTypes.string).isRequired, + icon: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, + order: PropTypes.number, }) ), - apmUiEnabled: PropTypes.bool.isRequired, find: PropTypes.func.isRequired, localStorage: PropTypes.object.isRequired, urlBasePath: PropTypes.string.isRequired, - mlEnabled: PropTypes.bool.isRequired, telemetry: PropTypes.shape({ telemetryService: PropTypes.any, telemetryNotifications: PropTypes.any, diff --git a/src/plugins/home/public/application/components/home.test.js b/src/plugins/home/public/application/components/home.test.js index 3bcfce513cb125..0d7596d92a5a14 100644 --- a/src/plugins/home/public/application/components/home.test.js +++ b/src/plugins/home/public/application/components/home.test.js @@ -41,6 +41,7 @@ describe('home', () => { beforeEach(() => { defaultProps = { directories: [], + solutions: [], apmUiEnabled: true, mlEnabled: true, kibanaVersion: '99.2.1', @@ -92,8 +93,96 @@ describe('home', () => { expect(component).toMatchSnapshot(); }); + describe('header', () => { + test('render', async () => { + const component = await renderHome(); + expect(component).toMatchSnapshot(); + }); + + test('should show "Manage" link if stack management is available', async () => { + const directoryEntry = { + id: 'stack-management', + title: 'Management', + description: 'Your center console for managing the Elastic Stack.', + icon: 'managementApp', + path: 'management_landing_page', + category: FeatureCatalogueCategory.ADMIN, + showOnHomePage: false, + }; + + const component = await renderHome({ + directories: [directoryEntry], + }); + + expect(component).toMatchSnapshot(); + }); + + test('should show "Dev tools" link if console is available', async () => { + const directoryEntry = { + id: 'console', + title: 'Console', + description: 'Skip cURL and use a JSON interface to work with your data in Console.', + icon: 'consoleApp', + path: 'path-to-dev-tools', + category: FeatureCatalogueCategory.ADMIN, + showOnHomePage: false, + }; + + const component = await renderHome({ + directories: [directoryEntry], + }); + + expect(component).toMatchSnapshot(); + }); + }); + describe('directories', () => { - test('should render DATA directory entry in "Explore Data" panel', async () => { + test('should render solutions in the "solution section"', async () => { + const solutionEntry1 = { + id: 'kibana', + title: 'Kibana', + subtitle: 'Visualize & analyze', + descriptions: ['Analyze data in dashboards'], + icon: 'logoKibana', + path: 'kibana_landing_page', + order: 1, + }; + const solutionEntry2 = { + id: 'solution-2', + title: 'Solution two', + subtitle: 'Subtitle for solution two', + descriptions: ['Example use case'], + icon: 'empty', + path: 'path-to-solution-two', + order: 2, + }; + const solutionEntry3 = { + id: 'solution-3', + title: 'Solution three', + subtitle: 'Subtitle for solution three', + descriptions: ['Example use case'], + icon: 'empty', + path: 'path-to-solution-three', + order: 3, + }; + const solutionEntry4 = { + id: 'solution-4', + title: 'Solution four', + subtitle: 'Subtitle for solution four', + descriptions: ['Example use case'], + icon: 'empty', + path: 'path-to-solution-four', + order: 4, + }; + + const component = await renderHome({ + solutions: [solutionEntry1, solutionEntry2, solutionEntry3, solutionEntry4], + }); + + expect(component).toMatchSnapshot(); + }); + + test('should render DATA directory entry in "Ingest your data" panel', async () => { const directoryEntry = { id: 'dashboard', title: 'Dashboard', @@ -111,7 +200,7 @@ describe('home', () => { expect(component).toMatchSnapshot(); }); - test('should render ADMIN directory entry in "Manage" panel', async () => { + test('should render ADMIN directory entry in "Manage your data" panel', async () => { const directoryEntry = { id: 'index_patterns', title: 'Index Patterns', @@ -148,6 +237,26 @@ describe('home', () => { }); }); + describe('change home route', () => { + test('should render a link to change the default route in advanced settings if advanced settings is enabled', async () => { + const component = await renderHome({ + directories: [ + { + description: 'Change your settings', + icon: 'gear', + id: 'advanced_settings', + path: 'path-to-advanced_settings', + showOnHomePage: false, + title: 'Advanced settings', + category: FeatureCatalogueCategory.ADMIN, + }, + ], + }); + + expect(component).toMatchSnapshot(); + }); + }); + describe('welcome', () => { test('should show the welcome screen if enabled, and there are no index patterns defined', async () => { defaultProps.localStorage.getItem = sinon.spy(() => 'true'); diff --git a/src/plugins/home/public/application/components/home_app.js b/src/plugins/home/public/application/components/home_app.js index 648915b6dae0c5..90e549c873436c 100644 --- a/src/plugins/home/public/application/components/home_app.js +++ b/src/plugins/home/public/application/components/home_app.js @@ -38,7 +38,7 @@ const RedirectToDefaultApp = () => { return null; }; -export function HomeApp({ directories }) { +export function HomeApp({ directories, solutions }) { const { savedObjectsClient, getBasePath, @@ -48,8 +48,6 @@ export function HomeApp({ directories }) { } = getServices(); const environment = environmentService.getEnvironment(); const isCloudEnabled = environment.cloud; - const mlEnabled = environment.ml; - const apmUiEnabled = environment.apmUi; const renderTutorialDirectory = (props) => { return ( @@ -87,8 +85,7 @@ export function HomeApp({ directories }) { +
+ ), + }} + > + onChange({ createNewCopies: false })} + > + {overwriteRadio} + + + + + onChange({ createNewCopies: true })} + /> + + ); +}; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.scss b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.scss new file mode 100644 index 00000000000000..4b46c1244e246e --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.scss @@ -0,0 +1,20 @@ +.savedObjectsManagementImportSummary__row { + margin-bottom: $euiSizeXS; +} + +.savedObjectsManagementImportSummary__title { + // Constrains title to the flex item, and allows for truncation when necessary + min-width: 0; +} + +.savedObjectsManagementImportSummary__createdCount { + color: $euiColorSuccessText; +} + +.savedObjectsManagementImportSummary__errorCount { + color: $euiColorDangerText; +} + +.savedObjectsManagementImportSummary__icon { + margin-left: $euiSizeXS; +} diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.test.tsx new file mode 100644 index 00000000000000..ed65131b0fc6b4 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.test.tsx @@ -0,0 +1,152 @@ +/* + * 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 React from 'react'; +import { ShallowWrapper } from 'enzyme'; +import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; +import { ImportSummary, ImportSummaryProps } from './import_summary'; +import { FailedImport } from '../../../lib'; + +// @ts-expect-error +import { findTestSubject } from '@elastic/eui/lib/test'; + +describe('ImportSummary', () => { + const errorUnsupportedType: FailedImport = { + obj: { type: 'error-obj-type', id: 'error-obj-id', meta: { title: 'Error object' } }, + error: { type: 'unsupported_type' }, + }; + const successNew = { type: 'dashboard', id: 'dashboard-id', meta: { title: 'New' } }; + const successOverwritten = { + type: 'visualization', + id: 'viz-id', + meta: { title: 'Overwritten' }, + overwrite: true, + }; + + const findHeader = (wrapper: ShallowWrapper) => wrapper.find('h3'); + const findCountCreated = (wrapper: ShallowWrapper) => + wrapper.find('h4.savedObjectsManagementImportSummary__createdCount'); + const findCountOverwritten = (wrapper: ShallowWrapper) => + wrapper.find('h4.savedObjectsManagementImportSummary__overwrittenCount'); + const findCountError = (wrapper: ShallowWrapper) => + wrapper.find('h4.savedObjectsManagementImportSummary__errorCount'); + const findObjectRow = (wrapper: ShallowWrapper) => + wrapper.find('.savedObjectsManagementImportSummary__row'); + + it('should render as expected with no results', async () => { + const props: ImportSummaryProps = { failedImports: [], successfulImports: [] }; + const wrapper = shallowWithI18nProvider(); + + expect(findHeader(wrapper).childAt(0).props()).toEqual( + expect.objectContaining({ values: { importCount: 0 } }) + ); + expect(findCountCreated(wrapper)).toHaveLength(0); + expect(findCountOverwritten(wrapper)).toHaveLength(0); + expect(findCountError(wrapper)).toHaveLength(0); + expect(findObjectRow(wrapper)).toHaveLength(0); + }); + + it('should render as expected with a newly created object', async () => { + const props: ImportSummaryProps = { + failedImports: [], + successfulImports: [successNew], + }; + const wrapper = shallowWithI18nProvider(); + + expect(findHeader(wrapper).childAt(0).props()).toEqual( + expect.objectContaining({ values: { importCount: 1 } }) + ); + const countCreated = findCountCreated(wrapper); + expect(countCreated).toHaveLength(1); + expect(countCreated.childAt(0).props()).toEqual( + expect.objectContaining({ values: { createdCount: 1 } }) + ); + expect(findCountOverwritten(wrapper)).toHaveLength(0); + expect(findCountError(wrapper)).toHaveLength(0); + expect(findObjectRow(wrapper)).toHaveLength(1); + }); + + it('should render as expected with an overwritten object', async () => { + const props: ImportSummaryProps = { + failedImports: [], + successfulImports: [successOverwritten], + }; + const wrapper = shallowWithI18nProvider(); + + expect(findHeader(wrapper).childAt(0).props()).toEqual( + expect.objectContaining({ values: { importCount: 1 } }) + ); + expect(findCountCreated(wrapper)).toHaveLength(0); + const countOverwritten = findCountOverwritten(wrapper); + expect(countOverwritten).toHaveLength(1); + expect(countOverwritten.childAt(0).props()).toEqual( + expect.objectContaining({ values: { overwrittenCount: 1 } }) + ); + expect(findCountError(wrapper)).toHaveLength(0); + expect(findObjectRow(wrapper)).toHaveLength(1); + }); + + it('should render as expected with an error object', async () => { + const props: ImportSummaryProps = { + failedImports: [errorUnsupportedType], + successfulImports: [], + }; + const wrapper = shallowWithI18nProvider(); + + expect(findHeader(wrapper).childAt(0).props()).toEqual( + expect.objectContaining({ values: { importCount: 1 } }) + ); + expect(findCountCreated(wrapper)).toHaveLength(0); + expect(findCountOverwritten(wrapper)).toHaveLength(0); + const countError = findCountError(wrapper); + expect(countError).toHaveLength(1); + expect(countError.childAt(0).props()).toEqual( + expect.objectContaining({ values: { errorCount: 1 } }) + ); + expect(findObjectRow(wrapper)).toHaveLength(1); + }); + + it('should render as expected with mixed objects', async () => { + const props: ImportSummaryProps = { + failedImports: [errorUnsupportedType], + successfulImports: [successNew, successOverwritten], + }; + const wrapper = shallowWithI18nProvider(); + + expect(findHeader(wrapper).childAt(0).props()).toEqual( + expect.objectContaining({ values: { importCount: 3 } }) + ); + const countCreated = findCountCreated(wrapper); + expect(countCreated).toHaveLength(1); + expect(countCreated.childAt(0).props()).toEqual( + expect.objectContaining({ values: { createdCount: 1 } }) + ); + const countOverwritten = findCountOverwritten(wrapper); + expect(countOverwritten).toHaveLength(1); + expect(countOverwritten.childAt(0).props()).toEqual( + expect.objectContaining({ values: { overwrittenCount: 1 } }) + ); + const countError = findCountError(wrapper); + expect(countError).toHaveLength(1); + expect(countError.childAt(0).props()).toEqual( + expect.objectContaining({ values: { errorCount: 1 } }) + ); + expect(findObjectRow(wrapper)).toHaveLength(3); + }); +}); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx new file mode 100644 index 00000000000000..7949f7d18d3501 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx @@ -0,0 +1,237 @@ +/* + * 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 './import_summary.scss'; +import _ from 'lodash'; +import React, { Fragment } from 'react'; +import { + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, + EuiIcon, + EuiIconTip, + EuiHorizontalRule, + EuiTitle, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { SavedObjectsImportSuccess } from 'kibana/public'; +import { FailedImport } from '../../..'; +import { getDefaultTitle, getSavedObjectLabel } from '../../../lib'; + +const DEFAULT_ICON = 'apps'; + +export interface ImportSummaryProps { + failedImports: FailedImport[]; + successfulImports: SavedObjectsImportSuccess[]; +} + +interface ImportItem { + type: string; + id: string; + title: string; + icon: string; + outcome: 'created' | 'overwritten' | 'error'; + errorMessage?: string; +} + +const unsupportedTypeErrorMessage = i18n.translate( + 'savedObjectsManagement.objectsTable.importSummary.unsupportedTypeError', + { defaultMessage: 'Unsupported object type' } +); + +const getErrorMessage = ({ error }: FailedImport) => { + if (error.type === 'unknown') { + return error.message; + } else if (error.type === 'unsupported_type') { + return unsupportedTypeErrorMessage; + } +}; + +const mapFailedImport = (failure: FailedImport): ImportItem => { + const { obj } = failure; + const { type, id, meta } = obj; + const title = meta.title || getDefaultTitle(obj); + const icon = meta.icon || DEFAULT_ICON; + const errorMessage = getErrorMessage(failure); + return { type, id, title, icon, outcome: 'error', errorMessage }; +}; + +const mapImportSuccess = (obj: SavedObjectsImportSuccess): ImportItem => { + const { type, id, meta, overwrite } = obj; + const title = meta.title || getDefaultTitle(obj); + const icon = meta.icon || DEFAULT_ICON; + const outcome = overwrite ? 'overwritten' : 'created'; + return { type, id, title, icon, outcome }; +}; + +const getCountIndicators = (importItems: ImportItem[]) => { + if (!importItems.length) { + return null; + } + + const outcomeCounts = importItems.reduce( + (acc, { outcome }) => acc.set(outcome, (acc.get(outcome) ?? 0) + 1), + new Map() + ); + const createdCount = outcomeCounts.get('created'); + const overwrittenCount = outcomeCounts.get('overwritten'); + const errorCount = outcomeCounts.get('error'); + + return ( + + {createdCount && ( + + +

+ +

+
+
+ )} + {overwrittenCount && ( + + +

+ +

+
+
+ )} + {errorCount && ( + + +

+ +

+
+
+ )} +
+ ); +}; + +const getStatusIndicator = ({ outcome, errorMessage }: ImportItem) => { + switch (outcome) { + case 'created': + return ( + + ); + case 'overwritten': + return ( + + ); + case 'error': + return ( + + ); + } +}; + +export const ImportSummary = ({ failedImports, successfulImports }: ImportSummaryProps) => { + const importItems: ImportItem[] = _.sortBy( + [ + ...failedImports.map((x) => mapFailedImport(x)), + ...successfulImports.map((x) => mapImportSuccess(x)), + ], + ['type', 'title'] + ); + + return ( + + +

+ +

+
+ + {getCountIndicators(importItems)} + + {importItems.map((item, index) => { + const { type, title, icon } = item; + return ( + + + + + + + + +

+ {title} +

+
+
+ +
{getStatusIndicator(item)}
+
+
+ ); + })} +
+ ); +}; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.test.tsx new file mode 100644 index 00000000000000..c93bc9e5038df2 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.test.tsx @@ -0,0 +1,107 @@ +/* + * 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 React from 'react'; +import { shallowWithI18nProvider, mountWithIntl } from 'test_utils/enzyme_helpers'; +import { OverwriteModalProps, OverwriteModal } from './overwrite_modal'; +import { findTestSubject } from '@elastic/eui/lib/test'; + +describe('OverwriteModal', () => { + const obj = { type: 'foo', id: 'bar', meta: { title: 'baz' } }; + const onFinish = jest.fn(); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('with a regular conflict', () => { + const props: OverwriteModalProps = { + conflict: { obj, error: { type: 'conflict', destinationId: 'qux' } }, + onFinish, + }; + + it('should render as expected', async () => { + const wrapper = shallowWithI18nProvider(); + + expect(wrapper.find('p').text()).toMatchInlineSnapshot( + `"\\"baz\\" conflicts with an existing object, are you sure you want to overwrite it?"` + ); + expect(wrapper.find('EuiSuperSelect')).toHaveLength(0); + }); + + it('should call onFinish with expected args when Skip is clicked', async () => { + const wrapper = mountWithIntl(); + + expect(onFinish).not.toHaveBeenCalled(); + findTestSubject(wrapper, 'confirmModalCancelButton').simulate('click'); + expect(onFinish).toHaveBeenCalledWith(false); + }); + + it('should call onFinish with expected args when Overwrite is clicked', async () => { + const wrapper = mountWithIntl(); + + expect(onFinish).not.toHaveBeenCalled(); + findTestSubject(wrapper, 'confirmModalConfirmButton').simulate('click'); + expect(onFinish).toHaveBeenCalledWith(true, 'qux'); + }); + }); + + describe('with an ambiguous conflict', () => { + const props: OverwriteModalProps = { + conflict: { + obj, + error: { + type: 'ambiguous_conflict', + destinations: [ + // TODO: change one of these to have an actual `updatedAt` date string, and mock Moment for the snapshot below + { id: 'qux', title: 'some title', updatedAt: undefined }, + { id: 'quux', title: 'another title', updatedAt: undefined }, + ], + }, + }, + onFinish, + }; + + it('should render as expected', async () => { + const wrapper = shallowWithI18nProvider(); + + expect(wrapper.find('p').text()).toMatchInlineSnapshot( + `"\\"baz\\" conflicts with multiple existing objects, do you want to overwrite one of them?"` + ); + expect(wrapper.find('EuiSuperSelect')).toHaveLength(1); + }); + + it('should call onFinish with expected args when Skip is clicked', async () => { + const wrapper = mountWithIntl(); + + expect(onFinish).not.toHaveBeenCalled(); + findTestSubject(wrapper, 'confirmModalCancelButton').simulate('click'); + expect(onFinish).toHaveBeenCalledWith(false); + }); + + it('should call onFinish with expected args when Overwrite is clicked', async () => { + const wrapper = mountWithIntl(); + + expect(onFinish).not.toHaveBeenCalled(); + findTestSubject(wrapper, 'confirmModalConfirmButton').simulate('click'); + // first destination is selected by default + expect(onFinish).toHaveBeenCalledWith(true, 'qux'); + }); + }); +}); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.tsx new file mode 100644 index 00000000000000..dbe95161cbeae1 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.tsx @@ -0,0 +1,139 @@ +/* + * 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 React, { useState, Fragment, ReactNode } from 'react'; +import { + EuiOverlayMask, + EuiConfirmModal, + EUI_MODAL_CONFIRM_BUTTON, + EuiText, + EuiSuperSelect, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { FailedImportConflict } from '../../../lib/resolve_import_errors'; +import { getDefaultTitle } from '../../../lib'; + +export interface OverwriteModalProps { + conflict: FailedImportConflict; + onFinish: (overwrite: boolean, destinationId?: string) => void; +} + +export const OverwriteModal = ({ conflict, onFinish }: OverwriteModalProps) => { + const { obj, error } = conflict; + let initialDestinationId: string | undefined; + let selectControl: ReactNode = null; + if (error.type === 'conflict') { + initialDestinationId = error.destinationId; + } else { + // ambiguous conflict must have at least two destinations; default to the first one + initialDestinationId = error.destinations[0].id; + } + const [destinationId, setDestinationId] = useState(initialDestinationId); + + if (error.type === 'ambiguous_conflict') { + const selectProps = { + options: error.destinations.map((destination) => { + const header = destination.title ?? `${type} [id=${destination.id}]`; + const lastUpdated = destination.updatedAt + ? moment(destination.updatedAt).fromNow() + : 'never'; + const idText = `ID: ${destination.id}`; + const lastUpdatedText = `Last updated: ${lastUpdated}`; + return { + value: destination.id, + inputDisplay: destination.id, + dropdownDisplay: ( + + {header} + +

+ {idText} +
+ {lastUpdatedText} +

+
+
+ ), + }; + }), + onChange: (value: string) => { + setDestinationId(value); + }, + }; + selectControl = ( + + ); + } + + const { type, meta } = obj; + const title = meta.title || getDefaultTitle(obj); + const bodyText = + error.type === 'conflict' + ? i18n.translate('savedObjectsManagement.objectsTable.overwriteModal.body.conflict', { + defaultMessage: + '"{title}" conflicts with an existing object, are you sure you want to overwrite it?', + values: { title }, + }) + : i18n.translate( + 'savedObjectsManagement.objectsTable.overwriteModal.body.ambiguousConflict', + { + defaultMessage: + '"{title}" conflicts with multiple existing objects, do you want to overwrite one of them?', + values: { title }, + } + ); + return ( + + onFinish(false)} + onConfirm={() => onFinish(true, destinationId)} + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + maxWidth="500px" + > +

{bodyText}

+ {selectControl} +
+
+ ); +}; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx index 2e545b372f781b..ead27389730746 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx @@ -87,7 +87,7 @@ describe('Relationships', () => { const component = shallowWithI18nProvider(); // Make sure we are showing loading - expect(component.find('EuiLoadingKibana').length).toBe(1); + expect(component.find('EuiLoadingElastic').length).toBe(1); // Ensure all promises resolve await new Promise((resolve) => process.nextTick(resolve)); @@ -154,7 +154,7 @@ describe('Relationships', () => { const component = shallowWithI18nProvider(); // Make sure we are showing loading - expect(component.find('EuiLoadingKibana').length).toBe(1); + expect(component.find('EuiLoadingElastic').length).toBe(1); // Ensure all promises resolve await new Promise((resolve) => process.nextTick(resolve)); @@ -221,7 +221,7 @@ describe('Relationships', () => { const component = shallowWithI18nProvider(); // Make sure we are showing loading - expect(component.find('EuiLoadingKibana').length).toBe(1); + expect(component.find('EuiLoadingElastic').length).toBe(1); // Ensure all promises resolve await new Promise((resolve) => process.nextTick(resolve)); @@ -288,7 +288,7 @@ describe('Relationships', () => { const component = shallowWithI18nProvider(); // Make sure we are showing loading - expect(component.find('EuiLoadingKibana').length).toBe(1); + expect(component.find('EuiLoadingElastic').length).toBe(1); // Ensure all promises resolve await new Promise((resolve) => process.nextTick(resolve)); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx index cc654f9717bd64..194733433ce295 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx @@ -26,7 +26,7 @@ import { EuiLink, EuiIcon, EuiCallOut, - EuiLoadingKibana, + EuiLoadingElastic, EuiInMemoryTable, EuiToolTip, EuiText, @@ -119,7 +119,7 @@ export class Relationships extends Component; + return ; } const columns = [ diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx index 0c7bf64ca011d4..7733a587ca9a79 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx @@ -23,11 +23,14 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import { keys } from '@elastic/eui'; import { httpServiceMock } from '../../../../../../core/public/mocks'; import { actionServiceMock } from '../../../services/action_service.mock'; +import { columnServiceMock } from '../../../services/column_service.mock'; +import { SavedObjectsManagementAction } from '../../..'; import { Table, TableProps } from './table'; const defaultProps: TableProps = { basePath: httpServiceMock.createSetupContract().basePath, actionRegistry: actionServiceMock.createStart(), + columnRegistry: columnServiceMock.createStart(), selectedSavedObjects: [ { id: '1', @@ -50,6 +53,7 @@ const defaultProps: TableProps = { }, filterOptions: [{ value: 2 }], onDelete: () => {}, + onActionRefresh: () => {}, onExport: () => {}, goInspectObject: () => {}, canGoInApp: () => true, @@ -122,4 +126,32 @@ describe('Table', () => { expect(component).toMatchSnapshot(); }); + + it(`allows for automatic refreshing after an action`, () => { + const actionRegistry = actionServiceMock.createStart(); + actionRegistry.getAll.mockReturnValue([ + { + // minimal action mock to exercise this test case + id: 'someAction', + render: () =>
action!
, + refreshOnFinish: () => true, + euiAction: { name: 'foo', description: 'bar', icon: 'beaker', type: 'icon' }, + registerOnFinishCallback: (callback: Function) => callback(), // call the callback immediately for this test + } as SavedObjectsManagementAction, + ]); + const onActionRefresh = jest.fn(); + const customizedProps = { ...defaultProps, actionRegistry, onActionRefresh }; + const component = shallowWithI18nProvider(); + + const table = component.find('EuiBasicTable'); + const columns = table.prop('columns') as any[]; + const actionColumn = columns.find((x) => x.hasOwnProperty('actions')) as { actions: any[] }; + const someAction = actionColumn.actions.find( + (x) => x['data-test-subj'] === 'savedObjectsTableAction-someAction' + ); + + expect(onActionRefresh).not.toHaveBeenCalled(); + someAction.onClick(); + expect(onActionRefresh).toHaveBeenCalled(); + }); }); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx index 719729cee26025..0ce7e6e38962a7 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx @@ -42,11 +42,13 @@ import { SavedObjectWithMetadata } from '../../../types'; import { SavedObjectsManagementActionServiceStart, SavedObjectsManagementAction, + SavedObjectsManagementColumnServiceStart, } from '../../../services'; export interface TableProps { basePath: IBasePath; actionRegistry: SavedObjectsManagementActionServiceStart; + columnRegistry: SavedObjectsManagementColumnServiceStart; selectedSavedObjects: SavedObjectWithMetadata[]; selectionConfig: { onSelectionChange: (selection: SavedObjectWithMetadata[]) => void; @@ -54,6 +56,7 @@ export interface TableProps { filterOptions: any[]; canDelete: boolean; onDelete: () => void; + onActionRefresh: (object: SavedObjectWithMetadata) => void; onExport: (includeReferencesDeep: boolean) => void; goInspectObject: (obj: SavedObjectWithMetadata) => void; pageIndex: number; @@ -74,6 +77,7 @@ interface TableState { isExportPopoverOpen: boolean; isIncludeReferencesDeepChecked: boolean; activeAction?: SavedObjectsManagementAction; + isColumnDataLoaded: boolean; } export class Table extends PureComponent { @@ -83,12 +87,22 @@ export class Table extends PureComponent { isExportPopoverOpen: false, isIncludeReferencesDeepChecked: true, activeAction: undefined, + isColumnDataLoaded: false, }; constructor(props: TableProps) { super(props); } + componentDidMount() { + this.loadColumnData(); + } + + loadColumnData = async () => { + await Promise.all(this.props.columnRegistry.getAll().map((column) => column.loadData())); + this.setState({ isColumnDataLoaded: true }); + }; + onChange = ({ query, error }: any) => { if (error) { this.setState({ @@ -139,12 +153,14 @@ export class Table extends PureComponent { filterOptions, selectionConfig: selection, onDelete, + onActionRefresh, selectedSavedObjects, onTableChange, goInspectObject, onShowRelationships, basePath, actionRegistry, + columnRegistry, } = this.props; const pagination = { @@ -224,10 +240,18 @@ export class Table extends PureComponent { ); }, } as EuiTableFieldDataColumnType>, + ...columnRegistry.getAll().map((column) => { + return { + ...column.euiColumn, + sortable: false, + 'data-test-subj': `savedObjectsTableColumn-${column.id}`, + }; + }), { name: i18n.translate('savedObjectsManagement.objectsTable.table.columnActionsName', { defaultMessage: 'Actions', }), + width: '80px', actions: [ { name: i18n.translate( @@ -274,6 +298,10 @@ export class Table extends PureComponent { this.setState({ activeAction: undefined, }); + const { refreshOnFinish = () => false } = action; + if (refreshOnFinish()) { + onActionRefresh(object); + } }); if (action.euiAction.onClick) { diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index 3719dac24e6e70..1bc3dc80665202 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -41,6 +41,7 @@ import { import { dataPluginMock } from '../../../../data/public/mocks'; import { serviceRegistryMock } from '../../services/service_registry.mock'; import { actionServiceMock } from '../../services/action_service.mock'; +import { columnServiceMock } from '../../services/column_service.mock'; import { SavedObjectsTable, SavedObjectsTableProps, @@ -134,6 +135,7 @@ describe('SavedObjectsTable', () => { allowedTypes, serviceRegistry: serviceRegistryMock.create(), actionRegistry: actionServiceMock.createStart(), + columnRegistry: columnServiceMock.createStart(), savedObjectsClient: savedObjects.client, indexPatterns: dataPluginMock.createStartContract().indexPatterns, http, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index 340c0e3237f91f..d879a71cc22699 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -27,7 +27,7 @@ import { EuiInMemoryTable, EuiIcon, EuiConfirmModal, - EuiLoadingKibana, + EuiLoadingElastic, EuiOverlayMask, EUI_MODAL_CONFIRM_BUTTON, EuiCheckboxGroup, @@ -65,6 +65,7 @@ import { fetchExportObjects, fetchExportByTypeAndSearch, findObjects, + findObject, extractExportDetails, SavedObjectsExportResultDetails, } from '../../lib'; @@ -72,6 +73,7 @@ import { SavedObjectWithMetadata } from '../../types'; import { ISavedObjectsManagementServiceRegistry, SavedObjectsManagementActionServiceStart, + SavedObjectsManagementColumnServiceStart, } from '../../services'; import { Header, Table, Flyout, Relationships } from './components'; import { DataPublicPluginStart } from '../../../../../plugins/data/public'; @@ -85,6 +87,7 @@ export interface SavedObjectsTableProps { allowedTypes: string[]; serviceRegistry: ISavedObjectsManagementServiceRegistry; actionRegistry: SavedObjectsManagementActionServiceStart; + columnRegistry: SavedObjectsManagementColumnServiceStart; savedObjectsClient: SavedObjectsClientContract; indexPatterns: IndexPatternsContract; http: HttpStart; @@ -157,7 +160,7 @@ export class SavedObjectsTable extends Component { @@ -202,15 +205,14 @@ export class SavedObjectsTable extends Component { - this.setState( - { - isSearching: true, - }, - this.debouncedFetch - ); + this.setState({ isSearching: true }, this.debouncedFetchObjects); }; - debouncedFetch = debounce(async () => { + fetchSavedObject = (type: string, id: string) => { + this.setState({ isSearching: true }, () => this.debouncedFetchObject(type, id)); + }; + + debouncedFetchObjects = debounce(async () => { const { activeQuery: query, page, perPage } = this.state; const { notifications, http, allowedTypes } = this.props; const { queryText, visibleTypes } = parseQuery(query); @@ -261,10 +263,48 @@ export class SavedObjectsTable extends Component { + debouncedFetchObject = debounce(async (type: string, id: string) => { + const { notifications, http } = this.props; + try { + const resp = await findObject(http, type, id); + if (!this._isMounted) { + return; + } + + this.setState(({ savedObjects, filteredItemCount }) => { + const refreshedSavedObjects = savedObjects.map((object) => + object.type === type && object.id === id ? resp : object + ); + return { + savedObjects: refreshedSavedObjects, + filteredItemCount, + isSearching: false, + }; + }); + } catch (error) { + if (this._isMounted) { + this.setState({ + isSearching: false, + }); + } + notifications.toasts.addDanger({ + title: i18n.translate( + 'savedObjectsManagement.objectsTable.unableFindSavedObjectNotificationMessage', + { defaultMessage: 'Unable to find saved object' } + ), + text: `${error}`, + }); + } + }, 300); + + refreshObjects = async () => { await Promise.all([this.fetchSavedObjects(), this.fetchCounts()]); }; + refreshObject = async ({ type, id }: SavedObjectWithMetadata) => { + await this.fetchSavedObject(type, id); + }; + onSelectionChanged = (selection: SavedObjectWithMetadata[]) => { this.setState({ selectedSavedObjects: selection }); }; @@ -505,7 +545,7 @@ export class SavedObjectsTable extends Component; + modal = ; } else { const onCancel = () => { this.setState({ isShowingDeleteConfirmModal: false }); @@ -731,7 +771,7 @@ export class SavedObjectsTable extends Component this.setState({ isShowingExportAllOptionsModal: true })} onImport={this.showImportFlyout} - onRefresh={this.refreshData} + onRefresh={this.refreshObjects} filteredCount={filteredItemCount} /> @@ -740,6 +780,7 @@ export class SavedObjectsTable extends Component void; }) => { const capabilities = coreStart.application.capabilities; @@ -62,6 +65,7 @@ const SavedObjectsTablePage = ({ allowedTypes={allowedTypes} serviceRegistry={serviceRegistry} actionRegistry={actionRegistry} + columnRegistry={columnRegistry} savedObjectsClient={coreStart.savedObjects.client} indexPatterns={dataStart.indexPatterns} search={dataStart.search} diff --git a/src/plugins/saved_objects_management/public/mocks.ts b/src/plugins/saved_objects_management/public/mocks.ts index 1de3de8e853022..3bd5a70884d854 100644 --- a/src/plugins/saved_objects_management/public/mocks.ts +++ b/src/plugins/saved_objects_management/public/mocks.ts @@ -18,12 +18,14 @@ */ import { actionServiceMock } from './services/action_service.mock'; +import { columnServiceMock } from './services/column_service.mock'; import { serviceRegistryMock } from './services/service_registry.mock'; import { SavedObjectsManagementPluginSetup, SavedObjectsManagementPluginStart } from './plugin'; const createSetupContractMock = (): jest.Mocked => { const mock = { actions: actionServiceMock.createSetup(), + columns: columnServiceMock.createSetup(), serviceRegistry: serviceRegistryMock.create(), }; return mock; @@ -32,6 +34,7 @@ const createSetupContractMock = (): jest.Mocked => { const mock = { actions: actionServiceMock.createStart(), + columns: columnServiceMock.createStart(), }; return mock; }; diff --git a/src/plugins/saved_objects_management/public/plugin.ts b/src/plugins/saved_objects_management/public/plugin.ts index ac30c634097600..907352f52699e6 100644 --- a/src/plugins/saved_objects_management/public/plugin.ts +++ b/src/plugins/saved_objects_management/public/plugin.ts @@ -29,6 +29,9 @@ import { SavedObjectsManagementActionService, SavedObjectsManagementActionServiceSetup, SavedObjectsManagementActionServiceStart, + SavedObjectsManagementColumnService, + SavedObjectsManagementColumnServiceSetup, + SavedObjectsManagementColumnServiceStart, SavedObjectsManagementServiceRegistry, ISavedObjectsManagementServiceRegistry, } from './services'; @@ -36,11 +39,13 @@ import { registerServices } from './register_services'; export interface SavedObjectsManagementPluginSetup { actions: SavedObjectsManagementActionServiceSetup; + columns: SavedObjectsManagementColumnServiceSetup; serviceRegistry: ISavedObjectsManagementServiceRegistry; } export interface SavedObjectsManagementPluginStart { actions: SavedObjectsManagementActionServiceStart; + columns: SavedObjectsManagementColumnServiceStart; } export interface SetupDependencies { @@ -64,6 +69,7 @@ export class SavedObjectsManagementPlugin StartDependencies > { private actionService = new SavedObjectsManagementActionService(); + private columnService = new SavedObjectsManagementColumnService(); private serviceRegistry = new SavedObjectsManagementServiceRegistry(); public setup( @@ -71,6 +77,7 @@ export class SavedObjectsManagementPlugin { home, management }: SetupDependencies ): SavedObjectsManagementPluginSetup { const actionSetup = this.actionService.setup(); + const columnSetup = this.columnService.setup(); if (home) { home.featureCatalogue.register({ @@ -111,15 +118,18 @@ export class SavedObjectsManagementPlugin return { actions: actionSetup, + columns: columnSetup, serviceRegistry: this.serviceRegistry, }; } public start(core: CoreStart, { data }: StartDependencies) { const actionStart = this.actionService.start(); + const columnStart = this.columnService.start(); return { actions: actionStart, + columns: columnStart, }; } } diff --git a/src/plugins/saved_objects_management/public/services/column_service.mock.ts b/src/plugins/saved_objects_management/public/services/column_service.mock.ts new file mode 100644 index 00000000000000..977b2099771baa --- /dev/null +++ b/src/plugins/saved_objects_management/public/services/column_service.mock.ts @@ -0,0 +1,57 @@ +/* + * 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 { + SavedObjectsManagementColumnService, + SavedObjectsManagementColumnServiceSetup, + SavedObjectsManagementColumnServiceStart, +} from './column_service'; + +const createSetupMock = (): jest.Mocked => { + const mock = { + register: jest.fn(), + }; + return mock; +}; + +const createStartMock = (): jest.Mocked => { + const mock = { + has: jest.fn(), + getAll: jest.fn(), + }; + + mock.has.mockReturnValue(true); + mock.getAll.mockReturnValue([]); + + return mock; +}; + +const createServiceMock = (): jest.Mocked> => { + const mock = { + setup: jest.fn().mockReturnValue(createSetupMock()), + start: jest.fn().mockReturnValue(createStartMock()), + }; + return mock; +}; + +export const columnServiceMock = { + create: createServiceMock, + createSetup: createSetupMock, + createStart: createStartMock, +}; diff --git a/src/plugins/saved_objects_management/public/services/column_service.test.ts b/src/plugins/saved_objects_management/public/services/column_service.test.ts new file mode 100644 index 00000000000000..367422b0bbe117 --- /dev/null +++ b/src/plugins/saved_objects_management/public/services/column_service.test.ts @@ -0,0 +1,66 @@ +/* + * 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 { + SavedObjectsManagementColumnService, + SavedObjectsManagementColumnServiceSetup, +} from './column_service'; +import { SavedObjectsManagementColumn } from './types'; + +class DummyColumn implements SavedObjectsManagementColumn { + constructor(public id: string) {} + + public euiColumn = { + field: 'id', + name: 'name', + }; + + public loadData = async () => {}; +} + +describe('SavedObjectsManagementColumnRegistry', () => { + let service: SavedObjectsManagementColumnService; + let setup: SavedObjectsManagementColumnServiceSetup; + + const createColumn = (id: string): SavedObjectsManagementColumn => { + return new DummyColumn(id); + }; + + beforeEach(() => { + service = new SavedObjectsManagementColumnService(); + setup = service.setup(); + }); + + describe('#register', () => { + it('allows columns to be registered and retrieved', () => { + const column = createColumn('foo'); + setup.register(column); + const start = service.start(); + expect(start.getAll()).toContain(column); + }); + + it('does not allow columns with duplicate ids to be registered', () => { + const column = createColumn('my-column'); + setup.register(column); + expect(() => setup.register(column)).toThrowErrorMatchingInlineSnapshot( + `"Saved Objects Management Column with id 'my-column' already exists"` + ); + }); + }); +}); diff --git a/src/plugins/saved_objects_management/public/services/column_service.ts b/src/plugins/saved_objects_management/public/services/column_service.ts new file mode 100644 index 00000000000000..5006d9df813cf2 --- /dev/null +++ b/src/plugins/saved_objects_management/public/services/column_service.ts @@ -0,0 +1,55 @@ +/* + * 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 { SavedObjectsManagementColumn } from './types'; + +export interface SavedObjectsManagementColumnServiceSetup { + /** + * register given column in the registry. + */ + register: (column: SavedObjectsManagementColumn) => void; +} + +export interface SavedObjectsManagementColumnServiceStart { + /** + * return all {@link SavedObjectsManagementColumn | columns} currently registered. + */ + getAll: () => Array>; +} + +export class SavedObjectsManagementColumnService { + private readonly columns = new Map>(); + + setup(): SavedObjectsManagementColumnServiceSetup { + return { + register: (column) => { + if (this.columns.has(column.id)) { + throw new Error(`Saved Objects Management Column with id '${column.id}' already exists`); + } + this.columns.set(column.id, column); + }, + }; + } + + start(): SavedObjectsManagementColumnServiceStart { + return { + getAll: () => [...this.columns.values()], + }; + } +} diff --git a/src/plugins/saved_objects_management/public/services/index.ts b/src/plugins/saved_objects_management/public/services/index.ts index a59ad9012c4029..f3379a3e29702b 100644 --- a/src/plugins/saved_objects_management/public/services/index.ts +++ b/src/plugins/saved_objects_management/public/services/index.ts @@ -22,9 +22,18 @@ export { SavedObjectsManagementActionServiceStart, SavedObjectsManagementActionServiceSetup, } from './action_service'; +export { + SavedObjectsManagementColumnService, + SavedObjectsManagementColumnServiceStart, + SavedObjectsManagementColumnServiceSetup, +} from './column_service'; export { SavedObjectsManagementServiceRegistry, ISavedObjectsManagementServiceRegistry, SavedObjectsManagementServiceRegistryEntry, } from './service_registry'; -export { SavedObjectsManagementAction, SavedObjectsManagementRecord } from './types'; +export { + SavedObjectsManagementAction, + SavedObjectsManagementColumn, + SavedObjectsManagementRecord, +} from './types'; diff --git a/src/plugins/saved_objects_management/public/services/types.ts b/src/plugins/saved_objects_management/public/services/types/action.ts similarity index 86% rename from src/plugins/saved_objects_management/public/services/types.ts rename to src/plugins/saved_objects_management/public/services/types/action.ts index c2f807f63b1b9a..2ead55d1f43383 100644 --- a/src/plugins/saved_objects_management/public/services/types.ts +++ b/src/plugins/saved_objects_management/public/services/types/action.ts @@ -17,18 +17,8 @@ * under the License. */ -import { ReactNode } from 'react'; -import { SavedObjectReference } from 'src/core/public'; - -export interface SavedObjectsManagementRecord { - type: string; - id: string; - meta: { - icon: string; - title: string; - }; - references: SavedObjectReference[]; -} +import { ReactNode } from '@elastic/eui/node_modules/@types/react'; +import { SavedObjectsManagementRecord } from '.'; export abstract class SavedObjectsManagementAction { public abstract render: () => ReactNode; @@ -43,6 +33,7 @@ export abstract class SavedObjectsManagementAction { onClick?: (item: SavedObjectsManagementRecord) => void; render?: (item: SavedObjectsManagementRecord) => any; }; + public refreshOnFinish?: () => boolean; private callbacks: Function[] = []; diff --git a/src/plugins/saved_objects_management/public/services/types/column.ts b/src/plugins/saved_objects_management/public/services/types/column.ts new file mode 100644 index 00000000000000..79ee4d649177f4 --- /dev/null +++ b/src/plugins/saved_objects_management/public/services/types/column.ts @@ -0,0 +1,29 @@ +/* + * 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 { EuiTableFieldDataColumnType } from '@elastic/eui'; +import { SavedObjectsManagementRecord } from '.'; + +export interface SavedObjectsManagementColumn { + id: string; + euiColumn: Omit, 'sortable'>; + + data?: T; + loadData: () => Promise; +} diff --git a/src/plugins/saved_objects_management/public/services/types/index.ts b/src/plugins/saved_objects_management/public/services/types/index.ts new file mode 100644 index 00000000000000..667ba8a683d8d1 --- /dev/null +++ b/src/plugins/saved_objects_management/public/services/types/index.ts @@ -0,0 +1,22 @@ +/* + * 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 { SavedObjectsManagementAction } from './action'; +export { SavedObjectsManagementColumn } from './column'; +export { SavedObjectsManagementRecord } from './record'; diff --git a/src/plugins/saved_objects_management/public/services/types/record.ts b/src/plugins/saved_objects_management/public/services/types/record.ts new file mode 100644 index 00000000000000..9e00935e674ad7 --- /dev/null +++ b/src/plugins/saved_objects_management/public/services/types/record.ts @@ -0,0 +1,32 @@ +/* + * 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 { SavedObjectReference, SavedObjectsNamespaceType } from 'src/core/public'; + +export interface SavedObjectsManagementRecord { + type: string; + id: string; + meta: { + icon: string; + title: string; + namespaceType: SavedObjectsNamespaceType; + }; + references: SavedObjectReference[]; + namespaces?: string[]; +} diff --git a/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.test.ts b/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.test.ts index 0c0f9d8feb506c..11e685bd198e4d 100644 --- a/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.test.ts +++ b/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.test.ts @@ -34,6 +34,7 @@ describe('injectMetaAttributes', () => { path: 'path', uiCapabilitiesPath: 'uiCapabilitiesPath', }); + managementService.getNamespaceType.mockReturnValue('single'); }); it('inject the metadata to the obj', () => { @@ -58,6 +59,7 @@ describe('injectMetaAttributes', () => { path: 'path', uiCapabilitiesPath: 'uiCapabilitiesPath', }, + namespaceType: 'single', }, }); }); diff --git a/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.ts b/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.ts index 615caffd3b60b1..54cad2d54e60ae 100644 --- a/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.ts +++ b/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.ts @@ -35,6 +35,7 @@ export function injectMetaAttributes( result.meta.title = savedObjectsManagement.getTitle(savedObject); result.meta.editUrl = savedObjectsManagement.getEditUrl(savedObject); result.meta.inAppUrl = savedObjectsManagement.getInAppUrl(savedObject); + result.meta.namespaceType = savedObjectsManagement.getNamespaceType(savedObject); return result; } diff --git a/src/plugins/saved_objects_management/server/routes/get.ts b/src/plugins/saved_objects_management/server/routes/get.ts new file mode 100644 index 00000000000000..a2c12a39705236 --- /dev/null +++ b/src/plugins/saved_objects_management/server/routes/get.ts @@ -0,0 +1,51 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; +import { injectMetaAttributes } from '../lib'; +import { ISavedObjectsManagement } from '../services'; + +export const registerGetRoute = ( + router: IRouter, + managementServicePromise: Promise +) => { + router.get( + { + path: '/api/kibana/management/saved_objects/{type}/{id}', + validate: { + params: schema.object({ + type: schema.string(), + id: schema.string(), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const managementService = await managementServicePromise; + const { client } = context.core.savedObjects; + + const { type, id } = req.params; + const findResponse = await client.get(type, id); + + const enhancedSavedObject = injectMetaAttributes(findResponse, managementService); + + return res.ok({ body: enhancedSavedObject }); + }) + ); +}; diff --git a/src/plugins/saved_objects_management/server/routes/index.test.ts b/src/plugins/saved_objects_management/server/routes/index.test.ts index 237760444f04eb..b39262f0c8b3c8 100644 --- a/src/plugins/saved_objects_management/server/routes/index.test.ts +++ b/src/plugins/saved_objects_management/server/routes/index.test.ts @@ -34,7 +34,7 @@ describe('registerRoutes', () => { }); expect(httpSetup.createRouter).toHaveBeenCalledTimes(1); - expect(router.get).toHaveBeenCalledTimes(3); + expect(router.get).toHaveBeenCalledTimes(4); expect(router.post).toHaveBeenCalledTimes(2); expect(router.get).toHaveBeenCalledWith( @@ -43,6 +43,12 @@ describe('registerRoutes', () => { }), expect.any(Function) ); + expect(router.get).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/api/kibana/management/saved_objects/{type}/{id}', + }), + expect.any(Function) + ); expect(router.get).toHaveBeenCalledWith( expect.objectContaining({ path: '/api/kibana/management/saved_objects/relationships/{type}/{id}', diff --git a/src/plugins/saved_objects_management/server/routes/index.ts b/src/plugins/saved_objects_management/server/routes/index.ts index 0929de56b215e4..e074a0d5cbee2e 100644 --- a/src/plugins/saved_objects_management/server/routes/index.ts +++ b/src/plugins/saved_objects_management/server/routes/index.ts @@ -20,6 +20,7 @@ import { HttpServiceSetup } from 'src/core/server'; import { ISavedObjectsManagement } from '../services'; import { registerFindRoute } from './find'; +import { registerGetRoute } from './get'; import { registerScrollForCountRoute } from './scroll_count'; import { registerScrollForExportRoute } from './scroll_export'; import { registerRelationshipsRoute } from './relationships'; @@ -33,6 +34,7 @@ interface RegisterRouteOptions { export function registerRoutes({ http, managementServicePromise }: RegisterRouteOptions) { const router = http.createRouter(); registerFindRoute(router, managementServicePromise); + registerGetRoute(router, managementServicePromise); registerScrollForCountRoute(router); registerScrollForExportRoute(router); registerRelationshipsRoute(router, managementServicePromise); diff --git a/src/plugins/saved_objects_management/server/services/management.mock.ts b/src/plugins/saved_objects_management/server/services/management.mock.ts index 2099cc0f77bcc7..85c2d3e4b08d97 100644 --- a/src/plugins/saved_objects_management/server/services/management.mock.ts +++ b/src/plugins/saved_objects_management/server/services/management.mock.ts @@ -28,6 +28,7 @@ const createManagementMock = () => { getTitle: jest.fn(), getEditUrl: jest.fn(), getInAppUrl: jest.fn(), + getNamespaceType: jest.fn(), }; return mocked; }; diff --git a/src/plugins/saved_objects_management/server/services/management.test.ts b/src/plugins/saved_objects_management/server/services/management.test.ts index 3625a3f9134448..7ddde312767ded 100644 --- a/src/plugins/saved_objects_management/server/services/management.test.ts +++ b/src/plugins/saved_objects_management/server/services/management.test.ts @@ -198,4 +198,28 @@ describe('SavedObjectsManagement', () => { expect(result).toEqual({ path: 'called', uiCapabilitiesPath: 'my.path' }); }); }); + + describe('getNamespaceType()', () => { + it('returns empty for unknown type', () => { + const result = management.getNamespaceType({ + id: '1', + type: 'foo', + attributes: {}, + references: [], + }); + expect(result).toEqual(undefined); + }); + + it('returns explicit value', () => { + registerType({ name: 'foo', namespaceType: 'single' }); + + const result = management.getNamespaceType({ + id: '1', + type: 'foo', + attributes: {}, + references: [], + }); + expect(result).toEqual('single'); + }); + }); }); diff --git a/src/plugins/saved_objects_management/server/services/management.ts b/src/plugins/saved_objects_management/server/services/management.ts index 7aee974182497a..499f37990c346f 100644 --- a/src/plugins/saved_objects_management/server/services/management.ts +++ b/src/plugins/saved_objects_management/server/services/management.ts @@ -50,4 +50,8 @@ export class SavedObjectsManagement { const getInAppUrl = this.registry.getType(savedObject.type)?.management?.getInAppUrl; return getInAppUrl ? getInAppUrl(savedObject) : undefined; } + + public getNamespaceType(savedObject: SavedObject) { + return this.registry.getType(savedObject.type)?.namespaceType; + } } diff --git a/test/api_integration/apis/saved_objects/import.js b/test/api_integration/apis/saved_objects/import.js index fbacfe458d9766..1666df2c83e5a3 100644 --- a/test/api_integration/apis/saved_objects/import.js +++ b/test/api_integration/apis/saved_objects/import.js @@ -25,25 +25,33 @@ export default function ({ getService }) { const esArchiver = getService('esArchiver'); describe('import', () => { + // mock success results including metadata + const indexPattern = { + type: 'index-pattern', + id: '91200a00-9efd-11e7-acb3-3dab96693fab', + meta: { title: 'logstash-*', icon: 'indexPatternApp' }, + }; + const visualization = { + type: 'visualization', + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + meta: { title: 'Count of requests', icon: 'visualizeApp' }, + }; + const dashboard = { + type: 'dashboard', + id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', + meta: { title: 'Requests', icon: 'dashboardApp' }, + }; + const createError = (object, type) => ({ + ...object, + title: object.meta.title, + error: { type }, + }); + describe('with kibana index', () => { describe('with basic data existing', () => { before(() => esArchiver.load('saved_objects/basic')); after(() => esArchiver.unload('saved_objects/basic')); - it('should return 200', async () => { - await supertest - .post('/api/saved_objects/_import') - .query({ overwrite: true }) - .attach('file', join(__dirname, '../../fixtures/import.ndjson')) - .expect(200) - .then((resp) => { - expect(resp.body).to.eql({ - success: true, - successCount: 3, - }); - }); - }); - it('should return 415 when no file passed in', async () => { await supertest .post('/api/saved_objects/_import') @@ -67,30 +75,9 @@ export default function ({ getService }) { success: false, successCount: 0, errors: [ - { - id: '91200a00-9efd-11e7-acb3-3dab96693fab', - type: 'index-pattern', - title: 'logstash-*', - error: { - type: 'conflict', - }, - }, - { - id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - type: 'visualization', - title: 'Count of requests', - error: { - type: 'conflict', - }, - }, - { - id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', - type: 'dashboard', - title: 'Requests', - error: { - type: 'conflict', - }, - }, + createError(indexPattern, 'conflict'), + createError(visualization, 'conflict'), + createError(dashboard, 'conflict'), ], }); }); @@ -99,15 +86,18 @@ export default function ({ getService }) { it('should return 200 when conflicts exist but overwrite is passed in', async () => { await supertest .post('/api/saved_objects/_import') - .query({ - overwrite: true, - }) + .query({ overwrite: true }) .attach('file', join(__dirname, '../../fixtures/import.ndjson')) .expect(200) .then((resp) => { expect(resp.body).to.eql({ success: true, successCount: 3, + successResults: [ + { ...indexPattern, overwrite: true }, + { ...visualization, overwrite: true }, + { ...dashboard, overwrite: true }, + ], }); }); }); @@ -130,9 +120,8 @@ export default function ({ getService }) { id: '1', type: 'wigwags', title: 'my title', - error: { - type: 'unsupported_type', - }, + meta: { title: 'my title' }, + error: { type: 'unsupported_type' }, }, ], }); @@ -162,7 +151,7 @@ export default function ({ getService }) { JSON.stringify({ type: 'visualization', id: '1', - attributes: {}, + attributes: { title: 'My visualization' }, references: [ { name: 'ref_0', @@ -189,9 +178,10 @@ export default function ({ getService }) { { type: 'visualization', id: '1', + title: 'My visualization', + meta: { title: 'My visualization', icon: 'visualizeApp' }, error: { type: 'missing_references', - blocking: [], references: [ { type: 'index-pattern', diff --git a/test/api_integration/apis/saved_objects/resolve_import_errors.js b/test/api_integration/apis/saved_objects/resolve_import_errors.js index aacfcd4382fac9..5380e9c3d11d8a 100644 --- a/test/api_integration/apis/saved_objects/resolve_import_errors.js +++ b/test/api_integration/apis/saved_objects/resolve_import_errors.js @@ -25,6 +25,23 @@ export default function ({ getService }) { const esArchiver = getService('esArchiver'); describe('resolve_import_errors', () => { + // mock success results including metadata + const indexPattern = { + type: 'index-pattern', + id: '91200a00-9efd-11e7-acb3-3dab96693fab', + meta: { title: 'logstash-*', icon: 'indexPatternApp' }, + }; + const visualization = { + type: 'visualization', + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + meta: { title: 'Count of requests', icon: 'visualizeApp' }, + }; + const dashboard = { + type: 'dashboard', + id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', + meta: { title: 'Requests', icon: 'dashboardApp' }, + }; + describe('without kibana index', () => { // Cleanup data that got created in import after(() => esArchiver.unload('saved_objects/basic')); @@ -72,6 +89,11 @@ export default function ({ getService }) { expect(resp.body).to.eql({ success: true, successCount: 3, + successResults: [ + { ...indexPattern, overwrite: true }, + { ...visualization, overwrite: true }, + { ...dashboard, overwrite: true }, + ], }); }); }); @@ -109,9 +131,8 @@ export default function ({ getService }) { id: '1', type: 'wigwags', title: 'my title', - error: { - type: 'unsupported_type', - }, + meta: { title: 'my title' }, + error: { type: 'unsupported_type' }, }, ], }); @@ -175,9 +196,9 @@ export default function ({ getService }) { id: '1', type: 'visualization', title: 'My favorite vis', + meta: { title: 'My favorite vis', icon: 'visualizeApp' }, error: { type: 'missing_references', - blocking: [], references: [ { type: 'index-pattern', @@ -234,7 +255,15 @@ export default function ({ getService }) { .attach('file', join(__dirname, '../../fixtures/import.ndjson')) .expect(200) .then((resp) => { - expect(resp.body).to.eql({ success: true, successCount: 3 }); + expect(resp.body).to.eql({ + success: true, + successCount: 3, + successResults: [ + { ...indexPattern, overwrite: true }, + { ...visualization, overwrite: true }, + { ...dashboard, overwrite: true }, + ], + }); }); }); @@ -254,7 +283,11 @@ export default function ({ getService }) { .attach('file', join(__dirname, '../../fixtures/import.ndjson')) .expect(200) .then((resp) => { - expect(resp.body).to.eql({ success: true, successCount: 1 }); + expect(resp.body).to.eql({ + success: true, + successCount: 1, + successResults: [{ ...visualization, overwrite: true }], + }); }); }); @@ -298,6 +331,13 @@ export default function ({ getService }) { expect(resp.body).to.eql({ success: true, successCount: 1, + successResults: [ + { + type: 'visualization', + id: '1', + meta: { title: 'My favorite vis', icon: 'visualizeApp' }, + }, + ], }); }); await supertest diff --git a/test/api_integration/apis/saved_objects_management/find.ts b/test/api_integration/apis/saved_objects_management/find.ts index 08c4327d7c0c46..c1c78570d8fe16 100644 --- a/test/api_integration/apis/saved_objects_management/find.ts +++ b/test/api_integration/apis/saved_objects_management/find.ts @@ -68,6 +68,7 @@ export default function ({ getService }: FtrProviderContext) { uiCapabilitiesPath: 'visualize.show', }, title: 'Count of requests', + namespaceType: 'single', }, }, ], @@ -225,6 +226,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/discover#/view/960372e0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'discover.show', }, + namespaceType: 'single', }); })); @@ -243,6 +245,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/dashboards#/view/b70c7ae0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'dashboard.show', }, + namespaceType: 'single', }); })); @@ -261,6 +264,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', }, + namespaceType: 'single', }); expect(resp.body.saved_objects[1].meta).to.eql({ icon: 'visualizeApp', @@ -271,6 +275,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', }, + namespaceType: 'single', }); })); @@ -290,6 +295,7 @@ export default function ({ getService }: FtrProviderContext) { '/app/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'management.kibana.indexPatterns', }, + namespaceType: 'single', }); })); }); diff --git a/test/api_integration/apis/saved_objects_management/get.ts b/test/api_integration/apis/saved_objects_management/get.ts new file mode 100644 index 00000000000000..8eb4cd7ab9a430 --- /dev/null +++ b/test/api_integration/apis/saved_objects_management/get.ts @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { Response } from 'supertest'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const es = getService('legacyEs'); + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('get', () => { + const existingObject = 'visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab'; + const nonexistentObject = 'wigwags/foo'; + + describe('with kibana index', () => { + before(() => esArchiver.load('saved_objects/basic')); + after(() => esArchiver.unload('saved_objects/basic')); + + it('should return 200 for object that exists and inject metadata', async () => + await supertest + .get(`/api/kibana/management/saved_objects/${existingObject}`) + .expect(200) + .then((resp: Response) => { + const { body } = resp; + const { type, id, meta } = body; + expect(type).to.eql('visualization'); + expect(id).to.eql('dd7caf20-9efd-11e7-acb3-3dab96693fab'); + expect(meta).to.not.equal(undefined); + })); + + it('should return 404 for object that does not exist', async () => + await supertest + .get(`/api/kibana/management/saved_objects/${nonexistentObject}`) + .expect(404)); + }); + + describe('without kibana index', () => { + before( + async () => + // just in case the kibana server has recreated it + await es.indices.delete({ + index: '.kibana', + ignore: [404], + }) + ); + + it('should return 404 for object that no longer exists', async () => + await supertest.get(`/api/kibana/management/saved_objects/${existingObject}`).expect(404)); + }); + }); +} diff --git a/test/api_integration/apis/saved_objects_management/index.ts b/test/api_integration/apis/saved_objects_management/index.ts index 9f13e4fc5975db..a5db29a6200f33 100644 --- a/test/api_integration/apis/saved_objects_management/index.ts +++ b/test/api_integration/apis/saved_objects_management/index.ts @@ -22,6 +22,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('saved objects management apis', () => { loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./relationships')); loadTestFile(require.resolve('./scroll_count')); }); diff --git a/test/api_integration/apis/saved_objects_management/relationships.ts b/test/api_integration/apis/saved_objects_management/relationships.ts index a1ea65645c13fb..8b7837f80ee44c 100644 --- a/test/api_integration/apis/saved_objects_management/relationships.ts +++ b/test/api_integration/apis/saved_objects_management/relationships.ts @@ -38,6 +38,7 @@ export default function ({ getService }: FtrProviderContext) { path: schema.string(), uiCapabilitiesPath: schema.string(), }), + namespaceType: schema.string(), }), }) ); @@ -89,6 +90,7 @@ export default function ({ getService }: FtrProviderContext) { '/app/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'management.kibana.indexPatterns', }, + namespaceType: 'single', }, }, { @@ -104,6 +106,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', }, + namespaceType: 'single', }, }, ]); @@ -130,6 +133,7 @@ export default function ({ getService }: FtrProviderContext) { '/app/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'management.kibana.indexPatterns', }, + namespaceType: 'single', }, relationship: 'child', }, @@ -145,6 +149,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', }, + namespaceType: 'single', }, relationship: 'parent', }, @@ -189,6 +194,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', }, + namespaceType: 'single', }, }, { @@ -204,6 +210,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', }, + namespaceType: 'single', }, }, ]); @@ -227,6 +234,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', }, + namespaceType: 'single', }, relationship: 'child', }, @@ -242,6 +250,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/visualize#/edit/a42c0580-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', }, + namespaceType: 'single', }, relationship: 'child', }, @@ -286,6 +295,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/discover#/view/960372e0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'discover.show', }, + namespaceType: 'single', }, }, { @@ -301,6 +311,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/dashboards#/view/b70c7ae0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'dashboard.show', }, + namespaceType: 'single', }, }, ]); @@ -326,6 +337,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/discover#/view/960372e0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'discover.show', }, + namespaceType: 'single', }, relationship: 'child', }, @@ -369,6 +381,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/discover#/view/960372e0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'discover.show', }, + namespaceType: 'single', }, }, { @@ -384,6 +397,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'visualize.show', }, + namespaceType: 'single', }, }, ]); @@ -409,6 +423,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/discover#/view/960372e0-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'discover.show', }, + namespaceType: 'single', }, relationship: 'parent', }, diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts index ad82ea9b6fbc14..e165341dbd63dc 100644 --- a/test/functional/page_objects/management/saved_objects_page.ts +++ b/test/functional/page_objects/management/saved_objects_page.ts @@ -48,7 +48,13 @@ export function SavedObjectsPageProvider({ getService, getPageObjects }: FtrProv if (!overwriteAll) { log.debug(`Toggling overwriteAll`); - await testSubjects.click('importSavedObjectsOverwriteToggle'); + const radio = await testSubjects.find( + 'savedObjectsManagement-importModeControl-overwriteRadioGroup' + ); + // a radio button consists of a div tag that contains an input, a div, and a label + // we can't click the input directly, need to go up one level and click the parent div + const div = await radio.findByXpath("//div[input[@id='overwriteDisabled']]"); + await div.click(); } else { log.debug(`Leaving overwriteAll alone`); } diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts index 5d4ea5a6370e4a..f8d66b8ecac27d 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts @@ -42,6 +42,19 @@ beforeEach(() => { afterEach(() => jest.clearAllMocks()); +describe('#checkConflicts', () => { + it('redirects request to underlying base client', async () => { + const objects = [{ type: 'foo', id: 'bar' }]; + const options = { namespace: 'some-namespace' }; + const mockedResponse = { errors: [] }; + mockBaseClient.checkConflicts.mockResolvedValue(mockedResponse); + + await expect(wrapper.checkConflicts(objects, options)).resolves.toEqual(mockedResponse); + expect(mockBaseClient.checkConflicts).toHaveBeenCalledTimes(1); + expect(mockBaseClient.checkConflicts).toHaveBeenCalledWith(objects, options); + }); +}); + describe('#create', () => { it('redirects request to underlying base client if type is not registered', async () => { const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts index 3246457179f68e..a2725cbc6a2740 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts @@ -13,6 +13,7 @@ import { SavedObjectsBulkUpdateObject, SavedObjectsBulkResponse, SavedObjectsBulkUpdateResponse, + SavedObjectsCheckConflictsObject, SavedObjectsClientContract, SavedObjectsCreateOptions, SavedObjectsFindOptions, @@ -48,6 +49,13 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon public readonly errors = options.baseClient.errors ) {} + public async checkConflicts( + objects: SavedObjectsCheckConflictsObject[] = [], + options?: SavedObjectsBaseOptions + ) { + return await this.options.baseClient.checkConflicts(objects, options); + } + public async create( type: string, attributes: T = {} as T, diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap index bfbc8b68c3d2cc..e4014cf49778cf 100644 --- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap @@ -323,6 +323,7 @@ Array [ "edit", "delete", "copyIntoSpace", + "shareIntoSpace", ], }, "privilegeId": "all", diff --git a/x-pack/plugins/features/server/oss_features.ts b/x-pack/plugins/features/server/oss_features.ts index 9df042b45a32e3..e37c7491de5dcc 100644 --- a/x-pack/plugins/features/server/oss_features.ts +++ b/x-pack/plugins/features/server/oss_features.ts @@ -349,7 +349,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS all: [...savedObjectTypes], read: [], }, - ui: ['read', 'edit', 'delete', 'copyIntoSpace'], + ui: ['read', 'edit', 'delete', 'copyIntoSpace', 'shareIntoSpace'], }, read: { app: ['kibana'], diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts index ff1a91b00d84f6..201003629e5ea3 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts @@ -14,7 +14,7 @@ import * as Registry from '../../registry'; import { AssetType, KibanaAssetType, AssetReference } from '../../../../types'; import { savedObjectTypes } from '../../packages'; -type SavedObjectToBe = Required> & { +type SavedObjectToBe = Required> & { type: AssetType; }; export type ArchiveAsset = Pick< diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index ca191602dcf446..7f7f969e8b4801 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -62,7 +62,7 @@ const expectGeneralError = async (fn: Function, args: Record) => { * Requires that function args are passed in as key/value pairs * The argument properties must be in the correct order to be spread properly */ -const expectForbiddenError = async (fn: Function, args: Record) => { +const expectForbiddenError = async (fn: Function, args: Record, action?: string) => { clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( getMockCheckPrivilegesFailure ); @@ -87,7 +87,7 @@ const expectForbiddenError = async (fn: Function, args: Record) => expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( USERNAME, - ACTION, + action ?? ACTION, types, spaceIds, missing, @@ -96,7 +96,7 @@ const expectForbiddenError = async (fn: Function, args: Record) => expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }; -const expectSuccess = async (fn: Function, args: Record) => { +const expectSuccess = async (fn: Function, args: Record, action?: string) => { const result = await fn.bind(client)(...Object.values(args)); const getCalls = (clientOpts.actions.savedObject.get as jest.MockedFunction< SavedObjectActions['get'] @@ -109,7 +109,7 @@ const expectSuccess = async (fn: Function, args: Record) => { expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( USERNAME, - ACTION, + action ?? ACTION, types, spaceIds, args @@ -492,6 +492,40 @@ describe('#bulkUpdate', () => { }); }); +describe('#checkConflicts', () => { + const obj1 = Object.freeze({ type: 'foo', id: 'foo-id' }); + const obj2 = Object.freeze({ type: 'bar', id: 'bar-id' }); + const options = Object.freeze({ namespace: 'some-ns' }); + + test(`throws decorated GeneralError when checkPrivileges.globally rejects promise`, async () => { + const objects = [obj1, obj2]; + await expectGeneralError(client.checkConflicts, { objects }); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const objects = [obj1, obj2]; + await expectForbiddenError(client.checkConflicts, { objects, options }, 'checkConflicts'); + }); + + test(`returns result of baseClient.create when authorized`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.checkConflicts.mockResolvedValue(apiCallReturnValue as any); + + const objects = [obj1, obj2]; + const result = await expectSuccess( + client.checkConflicts, + { objects, options }, + 'checkConflicts' + ); + expect(result).toBe(apiCallReturnValue); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + const objects = [obj1, obj2]; + await expectPrivilegeCheck(client.checkConflicts, { objects, options }); + }); +}); + describe('#create', () => { const type = 'foo'; const attributes = { some_attr: 's' }; diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index 9fd8a732c4eaba..68fe65d204d6de 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -9,6 +9,7 @@ import { SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, SavedObjectsBulkUpdateObject, + SavedObjectsCheckConflictsObject, SavedObjectsClientContract, SavedObjectsCreateOptions, SavedObjectsFindOptions, @@ -77,6 +78,18 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return await this.redactSavedObjectNamespaces(savedObject); } + public async checkConflicts( + objects: SavedObjectsCheckConflictsObject[] = [], + options: SavedObjectsBaseOptions = {} + ) { + const types = this.getUniqueObjectTypes(objects); + const args = { objects, options }; + await this.ensureAuthorized(types, 'bulk_create', options.namespace, args, 'checkConflicts'); + + const response = await this.baseClient.checkConflicts(objects, options); + return response; + } + public async bulkCreate( objects: Array>, options: SavedObjectsBaseOptions = {} diff --git a/x-pack/plugins/spaces/common/model/types.ts b/x-pack/plugins/spaces/common/model/types.ts index 30004c739ee7a5..aad77f2bbcef9e 100644 --- a/x-pack/plugins/spaces/common/model/types.ts +++ b/x-pack/plugins/spaces/common/model/types.ts @@ -4,4 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export type GetSpacePurpose = 'any' | 'copySavedObjectsIntoSpace' | 'findSavedObjects'; +export type GetSpacePurpose = + | 'any' + | 'copySavedObjectsIntoSpace' + | 'findSavedObjects' + | 'shareSavedObjectsIntoSpace'; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_mode_control.test.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_mode_control.test.tsx new file mode 100644 index 00000000000000..4e49a2da3e5343 --- /dev/null +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_mode_control.test.tsx @@ -0,0 +1,66 @@ +/* + * 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 { ReactWrapper } from 'enzyme'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { CopyModeControl, CopyModeControlProps } from './copy_mode_control'; + +describe('CopyModeControl', () => { + const initialValues = { createNewCopies: false, overwrite: true }; // some test cases below make assumptions based on these initial values + const updateSelection = jest.fn(); + + const getOverwriteRadio = (wrapper: ReactWrapper) => + wrapper.find('EuiRadioGroup[data-test-subj="cts-copyModeControl-overwriteRadioGroup"]'); + const getOverwriteEnabled = (wrapper: ReactWrapper) => + wrapper.find('input[id="overwriteEnabled"]'); + const getOverwriteDisabled = (wrapper: ReactWrapper) => + wrapper.find('input[id="overwriteDisabled"]'); + const getCreateNewCopiesDisabled = (wrapper: ReactWrapper) => + wrapper.find('input[id="createNewCopiesDisabled"]'); + const getCreateNewCopiesEnabled = (wrapper: ReactWrapper) => + wrapper.find('input[id="createNewCopiesEnabled"]'); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + const props: CopyModeControlProps = { initialValues, updateSelection }; + + it('should allow the user to toggle `overwrite`', async () => { + const wrapper = mountWithIntl(); + + expect(updateSelection).not.toHaveBeenCalled(); + const { createNewCopies } = initialValues; + + getOverwriteDisabled(wrapper).simulate('change'); + expect(updateSelection).toHaveBeenNthCalledWith(1, { createNewCopies, overwrite: false }); + + getOverwriteEnabled(wrapper).simulate('change'); + expect(updateSelection).toHaveBeenNthCalledWith(2, { createNewCopies, overwrite: true }); + }); + + it('should disable the Overwrite switch when `createNewCopies` is enabled', async () => { + const wrapper = mountWithIntl(); + + expect(getOverwriteRadio(wrapper).prop('disabled')).toBe(false); + getCreateNewCopiesEnabled(wrapper).simulate('change'); + expect(getOverwriteRadio(wrapper).prop('disabled')).toBe(true); + }); + + it('should allow the user to toggle `createNewCopies`', async () => { + const wrapper = mountWithIntl(); + + expect(updateSelection).not.toHaveBeenCalled(); + const { overwrite } = initialValues; + + getCreateNewCopiesEnabled(wrapper).simulate('change'); + expect(updateSelection).toHaveBeenNthCalledWith(1, { createNewCopies: true, overwrite }); + + getCreateNewCopiesDisabled(wrapper).simulate('change'); + expect(updateSelection).toHaveBeenNthCalledWith(2, { createNewCopies: false, overwrite }); + }); +}); diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_mode_control.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_mode_control.tsx new file mode 100644 index 00000000000000..42fbf8954396ee --- /dev/null +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_mode_control.tsx @@ -0,0 +1,174 @@ +/* + * 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 { + EuiFormFieldset, + EuiTitle, + EuiCheckableCard, + EuiRadioGroup, + EuiText, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiIconTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export interface CopyModeControlProps { + initialValues: CopyMode; + updateSelection: (result: CopyMode) => void; +} + +export interface CopyMode { + createNewCopies: boolean; + overwrite: boolean; +} + +const createNewCopiesDisabled = { + id: 'createNewCopiesDisabled', + text: i18n.translate( + 'xpack.spaces.management.copyToSpace.copyModeControl.createNewCopies.disabledTitle', + { defaultMessage: 'Check for existing objects' } + ), + tooltip: i18n.translate( + 'xpack.spaces.management.copyToSpace.copyModeControl.createNewCopies.disabledText', + { + defaultMessage: + 'Check if each object was previously copied or imported into the destination space.', + } + ), +}; +const createNewCopiesEnabled = { + id: 'createNewCopiesEnabled', + text: i18n.translate( + 'xpack.spaces.management.copyToSpace.copyModeControl.createNewCopies.enabledTitle', + { defaultMessage: 'Create new objects with random IDs' } + ), + tooltip: i18n.translate( + 'xpack.spaces.management.copyToSpace.copyModeControl.createNewCopies.enabledText', + { defaultMessage: 'All copied objects will be created with new random IDs.' } + ), +}; +const overwriteEnabled = { + id: 'overwriteEnabled', + label: i18n.translate( + 'xpack.spaces.management.copyToSpace.copyModeControl.overwrite.enabledLabel', + { defaultMessage: 'Automatically try to overwrite conflicts' } + ), +}; +const overwriteDisabled = { + id: 'overwriteDisabled', + label: i18n.translate( + 'xpack.spaces.management.copyToSpace.copyModeControl.overwrite.disabledLabel', + { defaultMessage: 'Request action when conflict occurs' } + ), +}; +const includeRelated = { + id: 'includeRelated', + text: i18n.translate('xpack.spaces.management.copyToSpace.copyModeControl.includeRelated.title', { + defaultMessage: 'Include related saved objects', + }), + tooltip: i18n.translate( + 'xpack.spaces.management.copyToSpace.copyModeControl.includeRelated.text', + { + defaultMessage: + 'This will copy any other objects this has references to -- for example, a dashboard may have references to multiple visualizations.', + } + ), +}; +const copyOptionsTitle = i18n.translate( + 'xpack.spaces.management.copyToSpace.copyModeControl.copyOptionsTitle', + { defaultMessage: 'Copy options' } +); +const relationshipOptionsTitle = i18n.translate( + 'xpack.spaces.management.copyToSpace.copyModeControl.relationshipOptionsTitle', + { defaultMessage: 'Relationship options' } +); + +const createLabel = ({ text, tooltip }: { text: string; tooltip: string }) => ( + + + {text} + + + + + +); + +export const CopyModeControl = ({ initialValues, updateSelection }: CopyModeControlProps) => { + const [createNewCopies, setCreateNewCopies] = useState(initialValues.createNewCopies); + const [overwrite, setOverwrite] = useState(initialValues.overwrite); + + const onChange = (partial: Partial) => { + if (partial.createNewCopies !== undefined) { + setCreateNewCopies(partial.createNewCopies); + } else if (partial.overwrite !== undefined) { + setOverwrite(partial.overwrite); + } + updateSelection({ createNewCopies, overwrite, ...partial }); + }; + + return ( + <> + + {copyOptionsTitle} + + ), + }} + > + onChange({ createNewCopies: false })} + > + onChange({ overwrite: id === overwriteEnabled.id })} + disabled={createNewCopies} + data-test-subj={'cts-copyModeControl-overwriteRadioGroup'} + /> + + + + + onChange({ createNewCopies: true })} + /> + + + + + + {relationshipOptionsTitle} + + ), + }} + > + {}} // noop + disabled + /> + + + ); +}; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_indicator.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_indicator.tsx index 62f9503443951b..158d7a9a43ef62 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_indicator.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_indicator.tsx @@ -4,20 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { EuiLoadingSpinner, EuiText, EuiIconTip } from '@elastic/eui'; +import React, { Fragment } from 'react'; +import { EuiLoadingSpinner, EuiIconTip, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { ImportRetry } from '../types'; import { SummarizedCopyToSpaceResult, SummarizedSavedObjectResult } from '..'; interface Props { summarizedCopyResult: SummarizedCopyToSpaceResult; object: { type: string; id: string }; - overwritePending: boolean; + pendingObjectRetry?: ImportRetry; conflictResolutionInProgress: boolean; } export const CopyStatusIndicator = (props: Props) => { - const { summarizedCopyResult, conflictResolutionInProgress } = props; + const { summarizedCopyResult, conflictResolutionInProgress, pendingObjectRetry } = props; if (summarizedCopyResult.processing || conflictResolutionInProgress) { return ; } @@ -25,32 +26,55 @@ export const CopyStatusIndicator = (props: Props) => { const objectResult = summarizedCopyResult.objects.find( (o) => o.type === props.object!.type && o.id === props.object!.id ) as SummarizedSavedObjectResult; + const { conflict, hasMissingReferences, hasUnresolvableErrors, overwrite } = objectResult; + const hasConflicts = conflict && !pendingObjectRetry?.overwrite; + const successful = !hasMissingReferences && !hasUnresolvableErrors && !hasConflicts; - const successful = - !objectResult.hasUnresolvableErrors && - (objectResult.conflicts.length === 0 || props.overwritePending === true); - const successColor = props.overwritePending ? 'warning' : 'success'; - const hasConflicts = objectResult.conflicts.length > 0; - const hasUnresolvableErrors = objectResult.hasUnresolvableErrors; - - if (successful) { - const message = props.overwritePending ? ( + if (successful && !pendingObjectRetry) { + // there is no retry pending, so this object was actually copied + const message = overwrite ? ( + // the object was overwritten ) : ( + // the object was not overwritten ); - return ; + return ; } + + if (successful && pendingObjectRetry) { + const message = overwrite ? ( + // this is an "automatic overwrite", e.g., the "Overwrite all conflicts" option was selected + + ) : pendingObjectRetry?.overwrite ? ( + // this is a manual overwrite, e.g., the individual "Overwrite?" switch was enabled + + ) : ( + // this object is pending success, but it will not result in an overwrite + + ); + return ; + } + if (hasUnresolvableErrors) { return ( { /> ); } + if (hasConflicts) { return ( -

- -

-

- -

- + + + + + } /> ); } - return null; + + return hasMissingReferences ? ( + + ) : conflict ? ( + + ) : ( + + ) + } + /> + ) : null; }; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.scss b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.scss new file mode 100644 index 00000000000000..d1c3cbbd2b6af1 --- /dev/null +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.scss @@ -0,0 +1,7 @@ +.spcCopyToSpace__summaryCountBadge { + margin-left: $euiSizeXS; +} + +.spcCopyToSpace__missingReferencesIcon { + margin-left: $euiSizeXS; +} diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.tsx index 9d73c216c73cec..4bc7e5cfaf31ae 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.tsx @@ -4,30 +4,50 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { EuiLoadingSpinner, EuiIconTip } from '@elastic/eui'; +import './copy_status_summary_indicator.scss'; +import React, { Fragment } from 'react'; +import { EuiLoadingSpinner, EuiIconTip, EuiBadge } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Space } from '../../../common/model/space'; +import { ImportRetry } from '../types'; +import { ResolveAllConflicts } from './resolve_all_conflicts'; import { SummarizedCopyToSpaceResult } from '..'; interface Props { space: Space; summarizedCopyResult: SummarizedCopyToSpaceResult; conflictResolutionInProgress: boolean; + retries: ImportRetry[]; + onRetriesChange: (retries: ImportRetry[]) => void; + onDestinationMapChange: (value?: Map) => void; } -export const CopyStatusSummaryIndicator = (props: Props) => { - const { summarizedCopyResult } = props; - const getDataTestSubj = (status: string) => `cts-summary-indicator-${status}-${props.space.id}`; +const renderIcon = (props: Props) => { + const { + space, + summarizedCopyResult, + conflictResolutionInProgress, + retries, + onRetriesChange, + onDestinationMapChange, + } = props; + const getDataTestSubj = (status: string) => `cts-summary-indicator-${status}-${space.id}`; - if (summarizedCopyResult.processing || props.conflictResolutionInProgress) { + if (summarizedCopyResult.processing || conflictResolutionInProgress) { return ; } - if (summarizedCopyResult.successful) { + const { + successful, + hasUnresolvableErrors, + hasMissingReferences, + hasConflicts, + } = summarizedCopyResult; + + if (successful) { return ( { } /> ); } - if (summarizedCopyResult.hasUnresolvableErrors) { + + if (hasUnresolvableErrors) { return ( { } /> ); } - if (summarizedCopyResult.hasConflicts) { - return ( + + const missingReferences = hasMissingReferences ? ( + } /> + + ) : null; + + if (hasConflicts) { + return ( + + + + } + /> + {missingReferences} + ); } - return null; + + return missingReferences; +}; + +export const CopyStatusSummaryIndicator = (props: Props) => { + const { summarizedCopyResult } = props; + + return ( + + {renderIcon(props)} + + {summarizedCopyResult.objects.length} + + + ); }; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx index 99b4e184c071a6..dfc908d81887a7 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx @@ -17,6 +17,7 @@ import { ProcessingCopyToSpace } from './processing_copy_to_space'; import { spacesManagerMock } from '../../spaces_manager/mocks'; import { SpacesManager } from '../../spaces_manager'; import { ToastsApi } from 'src/core/public'; +import { SavedObjectsManagementRecord } from 'src/plugins/saved_objects_management/public'; interface SetupOpts { mockSpaces?: Space[]; @@ -73,8 +74,8 @@ const setup = async (opts: SetupOpts = {}) => { name: 'My Viz', }, ], - meta: { icon: 'dashboard', title: 'foo' }, - }; + meta: { icon: 'dashboard', title: 'foo', namespaceType: 'single' }, + } as SavedObjectsManagementRecord; const wrapper = mountWithIntl( { type: 'index-pattern', id: 'conflicting-ip', error: { type: 'conflict' }, + meta: {}, }, { type: 'visualization', id: 'my-viz', error: { type: 'conflict' }, + meta: {}, }, ], }, @@ -223,8 +226,12 @@ describe('CopyToSpaceFlyout', () => { const spaceResult = findTestSubject(wrapper, `cts-space-result-space-2`); spaceResult.simulate('click'); - const overwriteButton = findTestSubject(wrapper, `cts-overwrite-conflict-conflicting-ip`); - overwriteButton.simulate('click'); + const overwriteSwitch = findTestSubject( + wrapper, + `cts-overwrite-conflict-index-pattern:conflicting-ip` + ); + expect(overwriteSwitch.props()['aria-checked']).toEqual(false); + overwriteSwitch.simulate('click'); const finishButton = findTestSubject(wrapper, 'cts-finish-button'); @@ -282,6 +289,7 @@ describe('CopyToSpaceFlyout', () => { [{ type: savedObjectToCopy.type, id: savedObjectToCopy.id }], ['space-1', 'space-2'], true, + false, true ); @@ -309,21 +317,45 @@ describe('CopyToSpaceFlyout', () => { mockSpacesManager.copySavedObjects.mockResolvedValue({ 'space-1': { success: true, - successCount: 3, + successCount: 5, }, 'space-2': { success: false, successCount: 1, errors: [ + // regular conflict without destinationId { type: 'index-pattern', id: 'conflicting-ip', error: { type: 'conflict' }, + meta: {}, + }, + // regular conflict with destinationId + { + type: 'search', + id: 'conflicting-search', + error: { type: 'conflict', destinationId: 'another-search' }, + meta: {}, + }, + // ambiguous conflict + { + type: 'canvas-workpad', + id: 'conflicting-canvas', + error: { + type: 'ambiguous_conflict', + destinations: [ + { id: 'another-canvas', title: 'foo', updatedAt: undefined }, + { id: 'yet-another-canvas', title: 'bar', updatedAt: undefined }, + ], + }, + meta: {}, }, + // negative test case (skip) { type: 'visualization', id: 'my-viz', error: { type: 'conflict' }, + meta: {}, }, ], }, @@ -358,8 +390,15 @@ describe('CopyToSpaceFlyout', () => { const spaceResult = findTestSubject(wrapper, `cts-space-result-space-2`); spaceResult.simulate('click'); - const overwriteButton = findTestSubject(wrapper, `cts-overwrite-conflict-conflicting-ip`); - overwriteButton.simulate('click'); + [ + 'index-pattern:conflicting-ip', + 'search:conflicting-search', + 'canvas-workpad:conflicting-canvas', + ].forEach((id) => { + const overwriteSwitch = findTestSubject(wrapper, `cts-overwrite-conflict-${id}`); + expect(overwriteSwitch.props()['aria-checked']).toEqual(false); + overwriteSwitch.simulate('click'); + }); const finishButton = findTestSubject(wrapper, 'cts-finish-button'); @@ -372,16 +411,148 @@ describe('CopyToSpaceFlyout', () => { expect(mockSpacesManager.resolveCopySavedObjectsErrors).toHaveBeenCalledWith( [{ type: savedObjectToCopy.type, id: savedObjectToCopy.id }], { - 'space-2': [{ type: 'index-pattern', id: 'conflicting-ip', overwrite: true }], + 'space-1': [], + 'space-2': [ + { type: 'index-pattern', id: 'conflicting-ip', overwrite: true }, + { + type: 'search', + id: 'conflicting-search', + overwrite: true, + destinationId: 'another-search', + }, + { + type: 'canvas-workpad', + id: 'conflicting-canvas', + overwrite: true, + destinationId: 'another-canvas', + }, + ], }, - true + true, + false + ); + + expect(onClose).toHaveBeenCalledTimes(1); + expect(mockToastNotifications.addError).not.toHaveBeenCalled(); + }); + + it('displays a warning when missing references are encountered', async () => { + const { + wrapper, + onClose, + mockSpacesManager, + mockToastNotifications, + savedObjectToCopy, + } = await setup(); + + mockSpacesManager.copySavedObjects.mockResolvedValue({ + 'space-1': { + success: false, + successCount: 1, + errors: [ + // my-viz-1 just has a missing_references error + { + type: 'visualization', + id: 'my-viz-1', + error: { + type: 'missing_references', + references: [{ type: 'index-pattern', id: 'missing-index-pattern' }], + }, + meta: {}, + }, + // my-viz-2 has both a missing_references error and a conflict error + { + type: 'visualization', + id: 'my-viz-2', + error: { + type: 'missing_references', + references: [{ type: 'index-pattern', id: 'missing-index-pattern' }], + }, + meta: {}, + }, + { + type: 'visualization', + id: 'my-viz-2', + error: { type: 'conflict' }, + meta: {}, + }, + ], + successResults: [{ type: savedObjectToCopy.type, id: savedObjectToCopy.id, meta: {} }], + }, + }); + + // Using props callback instead of simulating clicks, + // because EuiSelectable uses a virtualized list, which isn't easily testable via test subjects + const spaceSelector = wrapper.find(SelectableSpacesControl); + + act(() => { + spaceSelector.props().onChange(['space-1']); + }); + + const startButton = findTestSubject(wrapper, 'cts-initiate-button'); + + await act(async () => { + startButton.simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(CopyToSpaceForm)).toHaveLength(0); + expect(wrapper.find(ProcessingCopyToSpace)).toHaveLength(1); + + const spaceResult = findTestSubject(wrapper, `cts-space-result-space-1`); + spaceResult.simulate('click'); + + const errorIconTip1 = spaceResult.find( + 'EuiIconTip[data-test-subj="cts-object-result-missing-references-my-viz-1"]' + ); + expect(errorIconTip1.props()).toMatchInlineSnapshot(` + Object { + "color": "warning", + "content": , + "data-test-subj": "cts-object-result-missing-references-my-viz-1", + "type": "link", + } + `); + + const myViz2Icon = 'EuiIconTip[data-test-subj="cts-object-result-missing-references-my-viz-2"]'; + expect(spaceResult.find(myViz2Icon)).toHaveLength(0); + + // TODO: test for a missing references icon by selecting overwrite for the my-viz-2 conflict + + const finishButton = findTestSubject(wrapper, 'cts-finish-button'); + await act(async () => { + finishButton.simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(mockSpacesManager.resolveCopySavedObjectsErrors).toHaveBeenCalledWith( + [{ type: savedObjectToCopy.type, id: savedObjectToCopy.id }], + { + 'space-1': [ + { type: 'dashboard', id: 'my-dash', overwrite: false }, + { + type: 'visualization', + id: 'my-viz-1', + overwrite: false, + ignoreMissingReferences: true, + }, + ], + }, + true, + false ); expect(onClose).toHaveBeenCalledTimes(1); expect(mockToastNotifications.addError).not.toHaveBeenCalled(); }); - it('displays an error when missing references are encountered', async () => { + it('displays an error when an unresolvable error is encountered', async () => { const { wrapper, onClose, mockSpacesManager, mockToastNotifications } = await setup(); mockSpacesManager.copySavedObjects.mockResolvedValue({ @@ -396,11 +567,8 @@ describe('CopyToSpaceFlyout', () => { { type: 'visualization', id: 'my-viz', - error: { - type: 'missing_references', - blocking: [], - references: [{ type: 'index-pattern', id: 'missing-index-pattern' }], - }, + error: { type: 'unknown', message: 'some error message', statusCode: 400 }, + meta: {}, }, ], }, @@ -441,7 +609,7 @@ describe('CopyToSpaceFlyout', () => { values={Object {}} />, "data-test-subj": "cts-object-result-error-my-viz", - "type": "cross", + "type": "alert", } `); diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx index 47fc603ee46e82..f9b81be2d6b4b1 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx @@ -22,17 +22,17 @@ import { mapValues } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { ToastsStart } from 'src/core/public'; -import { SavedObjectsManagementRecord } from '../../../../../../src/plugins/saved_objects_management/public'; +import { + ProcessedImportResponse, + processImportResponse, + SavedObjectsManagementRecord, +} from '../../../../../../src/plugins/saved_objects_management/public'; import { Space } from '../../../common/model/space'; import { SpacesManager } from '../../spaces_manager'; import { ProcessingCopyToSpace } from './processing_copy_to_space'; import { CopyToSpaceFlyoutFooter } from './copy_to_space_flyout_footer'; import { CopyToSpaceForm } from './copy_to_space_form'; import { CopyOptions, ImportRetry } from '../types'; -import { - ProcessedImportResponse, - processImportResponse, -} from '../../../../../../src/plugins/saved_objects_management/public'; interface Props { onClose: () => void; @@ -41,11 +41,16 @@ interface Props { toastNotifications: ToastsStart; } +const INCLUDE_RELATED_DEFAULT = true; +const CREATE_NEW_COPIES_DEFAULT = false; +const OVERWRITE_ALL_DEFAULT = true; + export const CopySavedObjectsToSpaceFlyout = (props: Props) => { const { onClose, savedObject, spacesManager, toastNotifications } = props; const [copyOptions, setCopyOptions] = useState({ - includeRelated: true, - overwrite: true, + includeRelated: INCLUDE_RELATED_DEFAULT, + createNewCopies: CREATE_NEW_COPIES_DEFAULT, + overwrite: OVERWRITE_ALL_DEFAULT, selectedSpaceIds: [], }); @@ -90,18 +95,48 @@ export const CopySavedObjectsToSpaceFlyout = (props: Props) => { setCopyResult({}); try { const copySavedObjectsResult = await spacesManager.copySavedObjects( - [ - { - type: savedObject.type, - id: savedObject.id, - }, - ], + [{ type: savedObject.type, id: savedObject.id }], copyOptions.selectedSpaceIds, copyOptions.includeRelated, + copyOptions.createNewCopies, copyOptions.overwrite ); const processedResult = mapValues(copySavedObjectsResult, processImportResponse); setCopyResult(processedResult); + + // retry all successful imports + const getAutomaticRetries = (response: ProcessedImportResponse): ImportRetry[] => { + const { failedImports, successfulImports } = response; + if (!failedImports.length) { + // if no imports failed for this space, return an empty array + return []; + } + + // get missing references failures that do not also have a conflict + const nonMissingReferencesFailures = failedImports + .filter(({ error }) => error.type !== 'missing_references') + .reduce((acc, { obj: { type, id } }) => acc.add(`${type}:${id}`), new Set()); + const missingReferencesToRetry = failedImports.filter( + ({ obj: { type, id }, error }) => + error.type === 'missing_references' && + !nonMissingReferencesFailures.has(`${type}:${id}`) + ); + + // otherwise, some imports failed for this space, so retry any successful imports (if any) + return [ + ...successfulImports.map(({ type, id, overwrite, destinationId, createNewCopy }) => { + return { type, id, overwrite: overwrite === true, destinationId, createNewCopy }; + }), + ...missingReferencesToRetry.map(({ obj: { type, id } }) => ({ + type, + id, + overwrite: false, + ignoreMissingReferences: true, + })), + ]; + }; + const automaticRetries = mapValues(processedResult, getAutomaticRetries); + setRetries(automaticRetries); } catch (e) { setCopyInProgress(false); toastNotifications.addError(e, { @@ -113,27 +148,22 @@ export const CopySavedObjectsToSpaceFlyout = (props: Props) => { } async function finishCopy() { - const needsConflictResolution = Object.values(retries).some((spaceRetry) => - spaceRetry.some((retry) => retry.overwrite) - ); + // if any retries are present, attempt to resolve errors again + const needsErrorResolution = Object.values(retries).some((spaceRetry) => spaceRetry.length); - if (needsConflictResolution) { + if (needsErrorResolution) { setConflictResolutionInProgress(true); try { await spacesManager.resolveCopySavedObjectsErrors( - [ - { - type: savedObject.type, - id: savedObject.id, - }, - ], + [{ type: savedObject.type, id: savedObject.id }], retries, - copyOptions.includeRelated + copyOptions.includeRelated, + copyOptions.createNewCopies ); toastNotifications.addSuccess( i18n.translate('xpack.spaces.management.copyToSpace.resolveCopySuccessTitle', { - defaultMessage: 'Overwrite successful', + defaultMessage: 'Copy successful', }) ); @@ -184,7 +214,12 @@ export const CopySavedObjectsToSpaceFlyout = (props: Props) => { // Step 2: Copy has not been initiated yet; User must fill out form to continue. if (!copyInProgress) { return ( - + ); } @@ -208,14 +243,14 @@ export const CopySavedObjectsToSpaceFlyout = (props: Props) => { - +

@@ -247,6 +282,7 @@ export const CopySavedObjectsToSpaceFlyout = (props: Props) => { copyResult={copyResult} numberOfSelectedSpaces={copyOptions.selectedSpaceIds.length} retries={retries} + onClose={onClose} onCopyStart={startCopy} onCopyFinish={finishCopy} /> diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx index d7ded819771fc4..524361bf6ef1d0 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx @@ -5,11 +5,18 @@ */ import React, { Fragment } from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiStat, EuiHorizontalRule } from '@elastic/eui'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiStat, + EuiHorizontalRule, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { ProcessedImportResponse, FailedImport } from 'src/plugins/saved_objects_management/public'; import { ImportRetry } from '../types'; -import { ProcessedImportResponse } from '../../../../../../src/plugins/saved_objects_management/public'; interface Props { copyInProgress: boolean; @@ -18,33 +25,54 @@ interface Props { copyResult: Record; retries: Record; numberOfSelectedSpaces: number; + onClose: () => void; onCopyStart: () => void; onCopyFinish: () => void; } + +const isResolvableError = ({ error: { type } }: FailedImport) => + ['conflict', 'ambiguous_conflict', 'missing_references'].includes(type); +const isUnresolvableError = (failure: FailedImport) => !isResolvableError(failure); + export const CopyToSpaceFlyoutFooter = (props: Props) => { - const { copyInProgress, initialCopyFinished, copyResult, retries } = props; + const { + copyInProgress, + conflictResolutionInProgress, + initialCopyFinished, + copyResult, + retries, + } = props; let summarizedResults = { successCount: 0, - overwriteConflictCount: 0, - conflictCount: 0, - unresolvableErrorCount: 0, + pendingCount: 0, + skippedCount: 0, + errorCount: 0, }; if (copyResult) { summarizedResults = Object.entries(copyResult).reduce((acc, result) => { const [spaceId, spaceResult] = result; - const overwriteCount = (retries[spaceId] || []).filter((c) => c.overwrite).length; + let successCount = 0; + let pendingCount = 0; + let skippedCount = 0; + let errorCount = 0; + if (spaceResult.status === 'success') { + successCount = spaceResult.importCount; + } else { + const uniqueResolvableErrors = spaceResult.failedImports + .filter(isResolvableError) + .reduce((set, { obj: { type, id } }) => set.add(`${type}:${id}`), new Set()); + pendingCount = (retries[spaceId] || []).length; + skippedCount = + uniqueResolvableErrors.size + spaceResult.successfulImports.length - pendingCount; + errorCount = spaceResult.failedImports.filter(isUnresolvableError).length; + } return { loading: false, - successCount: acc.successCount + spaceResult.importCount, - overwriteConflictCount: acc.overwriteConflictCount + overwriteCount, - conflictCount: - acc.conflictCount + - spaceResult.failedImports.filter((i) => i.error.type === 'conflict').length - - overwriteCount, - unresolvableErrorCount: - acc.unresolvableErrorCount + - spaceResult.failedImports.filter((i) => i.error.type !== 'conflict').length, + successCount: acc.successCount + successCount, + pendingCount: acc.pendingCount + pendingCount, + skippedCount: acc.skippedCount + skippedCount, + errorCount: acc.errorCount + errorCount, }; }, summarizedResults); } @@ -52,13 +80,13 @@ export const CopyToSpaceFlyoutFooter = (props: Props) => { const getButton = () => { let actionButton; if (initialCopyFinished) { - const hasPendingOverwrites = summarizedResults.overwriteConflictCount > 0; + const hasPendingRetries = summarizedResults.pendingCount > 0; - const buttonText = hasPendingOverwrites ? ( + const buttonText = hasPendingRetries ? ( ) : ( { actionButton = ( { } return ( - + + + props.onClose()} + data-test-subj="cts-cancel-button" + disabled={ + // Cannot cancel while the operation is in progress, or after some objects have already been created + (copyInProgress && !initialCopyFinished) || + conflictResolutionInProgress || + summarizedResults.successCount > 0 + } + > + + + {actionButton} ); @@ -141,35 +186,33 @@ export const CopyToSpaceFlyoutFooter = (props: Props) => { } />
- {summarizedResults.overwriteConflictCount > 0 && ( - - 0 ? 'primary' : 'subdued'} - isLoading={!initialCopyFinished} - textAlign="center" - description={ - - } - /> - - )} 0 ? 'primary' : 'subdued'} + isLoading={!initialCopyFinished} + textAlign="center" + description={ + + } + /> + + + 0 ? 'primary' : 'subdued'} + titleColor={summarizedResults.skippedCount > 0 ? 'primary' : 'subdued'} isLoading={!initialCopyFinished} textAlign="center" description={ } @@ -178,9 +221,9 @@ export const CopyToSpaceFlyoutFooter = (props: Props) => { 0 ? 'danger' : 'subdued'} + titleColor={summarizedResults.errorCount > 0 ? 'danger' : 'subdued'} isLoading={!initialCopyFinished} textAlign="center" description={ diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx index 0df2a7720e587f..fdc8d8c73e324f 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx @@ -4,78 +4,62 @@ * you may not use this file except in compliance with the Elastic License. */ -import './copy_to_space_form.scss'; import React from 'react'; -import { - EuiSwitch, - EuiSpacer, - EuiHorizontalRule, - EuiFormRow, - EuiListGroup, - EuiListGroupItem, -} from '@elastic/eui'; +import { EuiSpacer, EuiFormRow } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { CopyOptions } from '../types'; +import { SavedObjectsManagementRecord } from '../../../../../../src/plugins/saved_objects_management/public'; import { Space } from '../../../common/model/space'; import { SelectableSpacesControl } from './selectable_spaces_control'; +import { CopyModeControl, CopyMode } from './copy_mode_control'; interface Props { + savedObject: SavedObjectsManagementRecord; spaces: Space[]; onUpdate: (copyOptions: CopyOptions) => void; copyOptions: CopyOptions; } export const CopyToSpaceForm = (props: Props) => { - const setOverwrite = (overwrite: boolean) => props.onUpdate({ ...props.copyOptions, overwrite }); + const { savedObject, spaces, onUpdate, copyOptions } = props; + + // if the user is not creating new copies, prevent them from copying objects an object into a space where it already exists + const getDisabledSpaceIds = (createNewCopies: boolean) => + createNewCopies + ? new Set() + : (savedObject.namespaces ?? []).reduce((acc, cur) => acc.add(cur), new Set()); + + const changeCopyMode = ({ createNewCopies, overwrite }: CopyMode) => { + const disabled = getDisabledSpaceIds(createNewCopies); + const selectedSpaceIds = copyOptions.selectedSpaceIds.filter((x) => !disabled.has(x)); + onUpdate({ ...copyOptions, createNewCopies, overwrite, selectedSpaceIds }); + }; const setSelectedSpaceIds = (selectedSpaceIds: string[]) => - props.onUpdate({ ...props.copyOptions, selectedSpaceIds }); + onUpdate({ ...copyOptions, selectedSpaceIds }); return (
- - - - - } - /> - - - - - - } - checked={props.copyOptions.overwrite} - onChange={(e) => setOverwrite(e.target.checked)} + changeCopyMode(newValues)} /> - + } fullWidth > setSelectedSpaceIds(selection)} /> diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx index 255268d388eb8d..ceaa1dc9f5e21e 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx @@ -19,7 +19,7 @@ import { } from 'src/plugins/saved_objects_management/public'; import { Space } from '../../../common/model/space'; import { CopyOptions, ImportRetry } from '../types'; -import { SpaceResult } from './space_result'; +import { SpaceResult, SpaceResultProcessing } from './space_result'; import { summarizeCopyResult } from '..'; interface Props { @@ -33,6 +33,52 @@ interface Props { copyOptions: CopyOptions; } +const renderCopyOptions = ({ createNewCopies, overwrite, includeRelated }: CopyOptions) => { + const createNewCopiesLabel = createNewCopies ? ( + + ) : ( + + ); + const overwriteLabel = overwrite ? ( + + ) : ( + + ); + const includeRelatedLabel = includeRelated ? ( + + ) : ( + + ); + + return ( + + + {!createNewCopies && ( + + )} + + + ); +}; + export const ProcessingCopyToSpace = (props: Props) => { function updateRetries(spaceId: string, updatedRetries: ImportRetry[]) { props.onRetriesChange({ @@ -43,46 +89,13 @@ export const ProcessingCopyToSpace = (props: Props) => { return (
- - - ) : ( - - ) - } - /> - - ) : ( - - ) - } - /> - + {renderCopyOptions(props.copyOptions)}
@@ -90,22 +103,22 @@ export const ProcessingCopyToSpace = (props: Props) => { {props.copyOptions.selectedSpaceIds.map((id) => { const space = props.spaces.find((s) => s.id === id) as Space; const spaceCopyResult = props.copyResult[space.id]; - const summarizedSpaceCopyResult = summarizeCopyResult( - props.savedObject, - spaceCopyResult, - props.copyOptions.includeRelated - ); + const summarizedSpaceCopyResult = summarizeCopyResult(props.savedObject, spaceCopyResult); return ( - updateRetries(space.id, retries)} - conflictResolutionInProgress={props.conflictResolutionInProgress} - /> + {summarizedSpaceCopyResult.processing ? ( + + ) : ( + updateRetries(space.id, retries)} + conflictResolutionInProgress={props.conflictResolutionInProgress} + /> + )} ); diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/resolve_all_conflicts.scss b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/resolve_all_conflicts.scss new file mode 100644 index 00000000000000..ce019d17ceaf75 --- /dev/null +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/resolve_all_conflicts.scss @@ -0,0 +1,4 @@ +.spcCopyToSpace__resolveAllConflictsLink { + font-size: $euiFontSizeS; + margin-right: $euiSizeS; +} diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/resolve_all_conflicts.test.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/resolve_all_conflicts.test.tsx new file mode 100644 index 00000000000000..7da265d8f9958b --- /dev/null +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/resolve_all_conflicts.test.tsx @@ -0,0 +1,158 @@ +/* + * 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 { ReactWrapper } from 'enzyme'; +import { act } from '@testing-library/react'; +import { shallowWithIntl, mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { ResolveAllConflicts, ResolveAllConflictsProps } from './resolve_all_conflicts'; +import { SummarizedCopyToSpaceResult } from '..'; +import { ImportRetry } from '../types'; +describe('ResolveAllConflicts', () => { + const summarizedCopyResult = ({ + objects: [ + // these objects have minimal attributes to exercise test scenarios; these are not fully realistic results + { type: 'type-1', id: 'id-1', conflict: undefined }, // not a conflict + { type: 'type-2', id: 'id-2', conflict: { error: { type: 'conflict' } } }, // conflict without a destinationId + { + // conflict with a destinationId + type: 'type-3', + id: 'id-3', + conflict: { error: { type: 'conflict', destinationId: 'dest-3' } }, + }, + { + // ambiguous conflict with two destinations + type: 'type-4', + id: 'id-4', + conflict: { + error: { + type: 'ambiguous_conflict', + destinations: [{ id: 'dest-4a' }, { id: 'dest-4b' }], + }, + }, + }, + { + // ambiguous conflict with two destinations (a retry already exists for dest-5b) + type: 'type-5', + id: 'id-5', + conflict: { + error: { + type: 'ambiguous_conflict', + destinations: [{ id: 'dest-5a' }, { id: 'dest-5b' }], + }, + }, + }, + ], + } as unknown) as SummarizedCopyToSpaceResult; + const retries: ImportRetry[] = [ + { type: 'type-1', id: 'id-1', overwrite: false }, + { type: 'type-5', id: 'id-5', overwrite: true, destinationId: 'dest-5b' }, + ]; + const onRetriesChange = jest.fn(); + const onDestinationMapChange = jest.fn(); + + const getOverwriteOption = (wrapper: ReactWrapper) => + findTestSubject(wrapper, 'cts-resolve-all-conflicts-overwrite'); + const getSkipOption = (wrapper: ReactWrapper) => + findTestSubject(wrapper, 'cts-resolve-all-conflicts-skip'); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + const props: ResolveAllConflictsProps = { + summarizedCopyResult, + retries, + onRetriesChange, + onDestinationMapChange, + }; + const openPopover = async (wrapper: ReactWrapper) => { + await act(async () => { + wrapper.setState({ isPopoverOpen: true }); + await nextTick(); + wrapper.update(); + }); + }; + + it('should render as expected', async () => { + const wrapper = shallowWithIntl(); + + expect(wrapper).toMatchInlineSnapshot(` + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="resolveAllConflictsVisibilityPopover" + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + > + + Overwrite all + , + + Skip all + , + ] + } + /> + + `); + }); + + it('should add overwrite retries when "Overwrite all" is selected', async () => { + const wrapper = mountWithIntl(); + await openPopover(wrapper); + expect(onRetriesChange).not.toHaveBeenCalled(); + + getOverwriteOption(wrapper).simulate('click'); + expect(onRetriesChange).toHaveBeenCalledWith([ + { type: 'type-1', id: 'id-1', overwrite: false }, // unchanged + { type: 'type-5', id: 'id-5', overwrite: true, destinationId: 'dest-5b' }, // unchanged + { type: 'type-2', id: 'id-2', overwrite: true }, // added without a destinationId + { type: 'type-3', id: 'id-3', overwrite: true, destinationId: 'dest-3' }, // added with the destinationId + { type: 'type-4', id: 'id-4', overwrite: true, destinationId: 'dest-4a' }, // added with the first destinationId + ]); + expect(onDestinationMapChange).not.toHaveBeenCalled(); + }); + + it('should remove overwrite retries when "Skip all" is selected', async () => { + const wrapper = mountWithIntl(); + await openPopover(wrapper); + expect(onRetriesChange).not.toHaveBeenCalled(); + expect(onDestinationMapChange).not.toHaveBeenCalled(); + + getSkipOption(wrapper).simulate('click'); + expect(onRetriesChange).toHaveBeenCalledWith([ + { type: 'type-1', id: 'id-1', overwrite: false }, // unchanged + ]); + expect(onDestinationMapChange).toHaveBeenCalledWith(undefined); + }); +}); diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/resolve_all_conflicts.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/resolve_all_conflicts.tsx new file mode 100644 index 00000000000000..a4ded022debe8b --- /dev/null +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/resolve_all_conflicts.tsx @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import './resolve_all_conflicts.scss'; + +import { EuiContextMenuItem, EuiContextMenuPanel, EuiLink, EuiPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { Component } from 'react'; +import { ImportRetry } from '../types'; +import { SummarizedCopyToSpaceResult } from '..'; + +export interface ResolveAllConflictsProps { + summarizedCopyResult: SummarizedCopyToSpaceResult; + retries: ImportRetry[]; + onRetriesChange: (retries: ImportRetry[]) => void; + onDestinationMapChange: (value?: Map) => void; +} + +interface State { + isPopoverOpen: boolean; +} + +interface ResolveOption { + id: 'overwrite' | 'skip'; + text: string; +} + +const options: ResolveOption[] = [ + { + id: 'overwrite', + text: i18n.translate('xpack.spaces.management.copyToSpace.overwriteAllConflictsText', { + defaultMessage: 'Overwrite all', + }), + }, + { + id: 'skip', + text: i18n.translate('xpack.spaces.management.copyToSpace.skipAllConflictsText', { + defaultMessage: 'Skip all', + }), + }, +]; + +export class ResolveAllConflicts extends Component { + public state = { + isPopoverOpen: false, + }; + + public render() { + const button = ( + + + + ); + + const items = options.map((item) => { + return ( + { + this.onSelect(item.id); + }} + > + {item.text} + + ); + }); + + return ( + + + + ); + } + + private onSelect = (selection: ResolveOption['id']) => { + const { summarizedCopyResult, retries, onRetriesChange, onDestinationMapChange } = this.props; + const overwrite = selection === 'overwrite'; + + if (overwrite) { + const existingOverwrites = retries.filter((retry) => retry.overwrite === true); + const newOverwrites = summarizedCopyResult.objects.reduce((acc, { type, id, conflict }) => { + if ( + conflict && + !existingOverwrites.some((retry) => retry.type === type && retry.id === id) + ) { + const { error } = conflict; + // if this is a regular conflict, use its destinationId if it has one; + // otherwise, this is an ambiguous conflict, so use the first destinationId available + const destinationId = + error.type === 'conflict' ? error.destinationId : error.destinations[0].id; + return [...acc, { type, id, overwrite, ...(destinationId && { destinationId }) }]; + } + return acc; + }, new Array()); + onRetriesChange([...retries, ...newOverwrites]); + } else { + const objectsToSkip = summarizedCopyResult.objects.reduce( + (acc, { type, id, conflict }) => (conflict ? acc.add(`${type}:${id}`) : acc), + new Set() + ); + const filtered = retries.filter(({ type, id }) => !objectsToSkip.has(`${type}:${id}`)); + onRetriesChange(filtered); + onDestinationMapChange(undefined); + } + + this.setState({ isPopoverOpen: false }); + }; + + private onButtonClick = () => { + this.setState({ + isPopoverOpen: !this.state.isPopoverOpen, + }); + }; + + private closePopover = () => { + this.setState({ + isPopoverOpen: false, + }); + }; +} diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx index 9db045f4f068af..2a8b5e660f38c0 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx @@ -5,42 +5,53 @@ */ import './selectable_spaces_control.scss'; -import React, { Fragment, useState } from 'react'; -import { EuiSelectable, EuiLoadingSpinner } from '@elastic/eui'; +import React, { Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSelectable, EuiSelectableOption, EuiLoadingSpinner, EuiIconTip } from '@elastic/eui'; import { SpaceAvatar } from '../../space_avatar'; import { Space } from '../../../common/model/space'; interface Props { spaces: Space[]; selectedSpaceIds: string[]; + disabledSpaceIds: Set; onChange: (selectedSpaceIds: string[]) => void; disabled?: boolean; } -interface SpaceOption { - label: string; - prepend?: any; - checked: 'on' | 'off' | null; - ['data-space-id']: string; - disabled?: boolean; -} +type SpaceOption = EuiSelectableOption & { ['data-space-id']: string }; export const SelectableSpacesControl = (props: Props) => { - const [options, setOptions] = useState([]); - - // TODO: update once https://github.com/elastic/eui/issues/2071 is fixed - if (options.length === 0) { - setOptions( - props.spaces.map((space) => ({ - label: space.name, - prepend: , - checked: props.selectedSpaceIds.includes(space.id) ? 'on' : null, - ['data-space-id']: space.id, - ['data-test-subj']: `cts-space-selector-row-${space.id}`, - })) - ); + if (props.spaces.length === 0) { + return ; } + const disabledIndicator = ( + + } + position="left" + type="iInCircle" + /> + ); + + const options = props.spaces.map((space) => { + const disabled = props.disabledSpaceIds.has(space.id); + return { + label: space.name, + prepend: , + append: disabled ? disabledIndicator : null, + checked: props.selectedSpaceIds.includes(space.id) ? 'on' : undefined, + disabled, + ['data-space-id']: space.id, + ['data-test-subj']: `cts-space-selector-row-${space.id}`, + }; + }); + function updateSelectedSpaces(selectedOptions: SpaceOption[]) { if (props.disabled) return; @@ -49,17 +60,11 @@ export const SelectableSpacesControl = (props: Props) => { .map((opt) => opt['data-space-id']); props.onChange(selectedSpaceIds); - // TODO: remove once https://github.com/elastic/eui/issues/2071 is fixed - setOptions(selectedOptions); - } - - if (options.length === 0) { - return ; } return ( updateSelectedSpaces(newOptions as SpaceOption[])} listProps={{ bordered: true, diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result.tsx index f1a8f64a614491..eefd9f8ea2467d 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result.tsx @@ -5,8 +5,15 @@ */ import './space_result.scss'; -import React from 'react'; -import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer } from '@elastic/eui'; +import React, { useState } from 'react'; +import { + EuiAccordion, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiSpacer, + EuiLoadingSpinner, +} from '@elastic/eui'; import { SavedObjectsManagementRecord } from '../../../../../../src/plugins/saved_objects_management/public'; import { SummarizedCopyToSpaceResult } from '../index'; import { SpaceAvatar } from '../../space_avatar'; @@ -24,6 +31,39 @@ interface Props { conflictResolutionInProgress: boolean; } +const getInitialDestinationMap = (objects: SummarizedCopyToSpaceResult['objects']) => + objects.reduce((acc, { type, id, conflict }) => { + if (conflict?.error.type === 'ambiguous_conflict') { + acc.set(`${type}:${id}`, conflict.error.destinations[0].id); + } + return acc; + }, new Map()); + +export const SpaceResultProcessing = (props: Pick) => { + const { space } = props; + return ( + + + + + + {space.name} + + + } + extraAction={} + > + + + + ); +}; + export const SpaceResult = (props: Props) => { const { space, @@ -33,7 +73,12 @@ export const SpaceResult = (props: Props) => { savedObject, conflictResolutionInProgress, } = props; + const { objects } = summarizedCopyResult; const spaceHasPendingOverwrites = retries.some((r) => r.overwrite); + const [destinationMap, setDestinationMap] = useState(getInitialDestinationMap(objects)); + const onDestinationMapChange = (value?: Map) => { + setDestinationMap(value || getInitialDestinationMap(objects)); + }; return ( { extraAction={ @@ -65,6 +113,8 @@ export const SpaceResult = (props: Props) => { space={space} retries={retries} onRetriesChange={onRetriesChange} + destinationMap={destinationMap} + onDestinationMapChange={onDestinationMapChange} conflictResolutionInProgress={conflictResolutionInProgress && spaceHasPendingOverwrites} /> diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result_details.scss b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result_details.scss index 77029872202825..bca07da9eae421 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result_details.scss +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result_details.scss @@ -11,3 +11,28 @@ // Constrains name to the flex item, and allows for truncation when necessary min-width: 0; } + +.spcCopyToSpaceResultDetails__selectControl { + margin-left: $euiSizeL; +} + +.spcCopyToSpaceResultDetails__selectControl__childWrapper { + // Derived from euiAccordion + visibility: hidden; + opacity: 0; + height: 0; + overflow: hidden; + transform: translatez(0); + // sass-lint:disable-block indentation + transition: + height $euiAnimSpeedNormal $euiAnimSlightResistance, + opacity $euiAnimSpeedNormal $euiAnimSlightResistance; +} + +.spcCopyToSpaceResultDetails__selectControl.spcCopyToSpaceResultDetails__selectControl-isOpen { + .spcCopyToSpaceResultDetails__selectControl__childWrapper { + visibility: visible; + opacity: 1; + height: auto; + } +} diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result_details.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result_details.tsx index ef7931260e6436..776ed99c411206 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result_details.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result_details.tsx @@ -5,9 +5,23 @@ */ import './space_result_details.scss'; -import React from 'react'; -import { EuiText, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import React, { Fragment } from 'react'; +import { + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiSwitch, + EuiSwitchEvent, + EuiToolTip, + EuiIcon, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { + SavedObjectsImportConflictError, + SavedObjectsImportAmbiguousConflictError, +} from 'kibana/public'; +import { EuiSuperSelect } from '@elastic/eui'; +import moment from 'moment'; import { SummarizedCopyToSpaceResult } from '../index'; import { SavedObjectsManagementRecord } from '../../../../../../src/plugins/saved_objects_management/public'; import { Space } from '../../../common/model/space'; @@ -20,104 +34,161 @@ interface Props { space: Space; retries: ImportRetry[]; onRetriesChange: (retries: ImportRetry[]) => void; + destinationMap: Map; + onDestinationMapChange: (value?: Map) => void; conflictResolutionInProgress: boolean; } -export const SpaceCopyResultDetails = (props: Props) => { - const onOverwriteClick = (object: { type: string; id: string }) => { - const retry = props.retries.find((r) => r.type === object.type && r.id === object.id); - - props.onRetriesChange([ - ...props.retries.filter((r) => r !== retry), - { - type: object.type, - id: object.id, - overwrite: retry ? !retry.overwrite : true, - }, - ]); - }; - - const hasPendingOverwrite = (object: { type: string; id: string }) => { - const retry = props.retries.find((r) => r.type === object.type && r.id === object.id); +function getSavedObjectLabel(type: string) { + switch (type) { + case 'index-pattern': + case 'index-patterns': + case 'indexPatterns': + return 'index patterns'; + default: + return type; + } +} - return Boolean(retry && retry.overwrite); - }; +const isAmbiguousConflictError = ( + error: SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError +): error is SavedObjectsImportAmbiguousConflictError => error.type === 'ambiguous_conflict'; - const { objects } = props.summarizedCopyResult; +export const SpaceCopyResultDetails = (props: Props) => { + const { destinationMap, onDestinationMapChange, summarizedCopyResult } = props; + const { objects } = summarizedCopyResult; return (
{objects.map((object, index) => { - const objectOverwritePending = hasPendingOverwrite(object); + const { type, id, name, icon, conflict } = object; + const pendingObjectRetry = props.retries.find((r) => r.type === type && r.id === id); + const isOverwritePending = Boolean(pendingObjectRetry?.overwrite); + const switchProps = { + show: conflict && !props.conflictResolutionInProgress, + label: i18n.translate('xpack.spaces.management.copyToSpace.copyDetail.overwriteSwitch', { + defaultMessage: 'Overwrite?', + }), + onChange: ({ target: { checked } }: EuiSwitchEvent) => { + const filtered = props.retries.filter((r) => r.type !== type || r.id !== id); + const { error } = conflict!; - const showOverwriteButton = - object.conflicts.length > 0 && - !objectOverwritePending && - !props.conflictResolutionInProgress; - - const showSkipButton = - !showOverwriteButton && objectOverwritePending && !props.conflictResolutionInProgress; + if (!checked) { + props.onRetriesChange(filtered); + if (isAmbiguousConflictError(error)) { + // reset the selection to the first entry + const value = error.destinations[0].id; + onDestinationMapChange(new Map(destinationMap.set(`${type}:${id}`, value))); + } + } else { + const destinationId = isAmbiguousConflictError(error) + ? destinationMap.get(`${type}:${id}`) + : error.destinationId; + const retry = { type, id, overwrite: true, ...(destinationId && { destinationId }) }; + props.onRetriesChange([...filtered, retry]); + } + }, + }; + const selectProps = { + options: + conflict?.error && isAmbiguousConflictError(conflict.error) + ? conflict.error.destinations.map((destination) => { + const header = destination.title ?? `${type} [id=${destination.id}]`; + const lastUpdated = destination.updatedAt + ? moment(destination.updatedAt).fromNow() + : 'never'; + return { + value: destination.id, + inputDisplay: destination.id, + dropdownDisplay: ( + + {header} + +

+ ID: {destination.id} +
+ Last updated: {lastUpdated} +

+
+
+ ), + }; + }) + : [], + onChange: (value: string) => { + onDestinationMapChange(new Map(destinationMap.set(`${type}:${id}`, value))); + const filtered = props.retries.filter((r) => r.type !== type || r.id !== id); + const retry = { type, id, overwrite: true, destinationId: value }; + props.onRetriesChange([...filtered, retry]); + }, + }; + const selectContainerClass = + selectProps.options.length > 0 && isOverwritePending + ? ' spcCopyToSpaceResultDetails__selectControl-isOpen' + : ''; return ( - - - -

- {object.type}: {object.name || object.id} -

-
-
- {showOverwriteButton && ( - - - onOverwriteClick(object)} - size="xs" - data-test-subj={`cts-overwrite-conflict-${object.id}`} - > - - - + + + + + + - )} - {showSkipButton && ( - + - onOverwriteClick(object)} - size="xs" - data-test-subj={`cts-skip-conflict-${object.id}`} - > - - +

+ {name} +

- )} - -
- + + + )} + +
+ +
+
+ +
+
+
- - +
+ ); })}
diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx index 28b48044a17837..9bbde31ff6feab 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx @@ -21,10 +21,13 @@ export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagem defaultMessage: 'Copy to space', }), description: i18n.translate('xpack.spaces.management.copyToSpace.actionDescription', { - defaultMessage: 'Copy this saved object to one or more spaces', + defaultMessage: 'Make a copy of this saved object in one or more spaces', }), - icon: 'spacesApp', + icon: 'copy', type: 'icon', + available: (object: SavedObjectsManagementRecord) => { + return object.meta.namespaceType !== 'agnostic'; + }, onClick: (object: SavedObjectsManagementRecord) => { this.start(object); }, diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.test.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.test.ts index a8ecd7c7b9d9f5..b8fc89f47a3e08 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.test.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.test.ts @@ -5,50 +5,123 @@ */ import { summarizeCopyResult } from './summarize_copy_result'; -import { ProcessedImportResponse } from 'src/plugins/saved_objects_management/public'; +import { + ProcessedImportResponse, + FailedImport, + SavedObjectsManagementRecord, +} from 'src/plugins/saved_objects_management/public'; -const createSavedObjectsManagementRecord = () => ({ - type: 'dashboard', - id: 'foo', - meta: { icon: 'foo-icon', title: 'my-dashboard' }, - references: [ - { - type: 'visualization', - id: 'foo-viz', - name: 'Foo Viz', - }, - { - type: 'visualization', - id: 'bar-viz', - name: 'Bar Viz', - }, - ], -}); +// Sample data references: +// +// /-> Visualization bar -> Index pattern foo +// My dashboard +// \-> Visualization baz -> Index pattern bar +// +// Dashboard has references to visualizations, and transitive references to index patterns + +const OBJECTS = { + MY_DASHBOARD: { + type: 'dashboard', + id: 'foo', + meta: { title: 'my-dashboard-title', icon: 'dashboardApp', namespaceType: 'single' }, + references: [ + { type: 'visualization', id: 'foo', name: 'Visualization foo' }, + { type: 'visualization', id: 'bar', name: 'Visualization bar' }, + ], + } as SavedObjectsManagementRecord, + VISUALIZATION_FOO: { + type: 'visualization', + id: 'bar', + meta: { title: 'visualization-foo-title', icon: 'visualizeApp', namespaceType: 'single' }, + references: [{ type: 'index-pattern', id: 'foo', name: 'Index pattern foo' }], + } as SavedObjectsManagementRecord, + VISUALIZATION_BAR: { + type: 'visualization', + id: 'baz', + meta: { title: 'visualization-bar-title', icon: 'visualizeApp', namespaceType: 'single' }, + references: [{ type: 'index-pattern', id: 'bar', name: 'Index pattern bar' }], + } as SavedObjectsManagementRecord, + INDEX_PATTERN_FOO: { + type: 'index-pattern', + id: 'foo', + meta: { title: 'index-pattern-foo-title', icon: 'indexPatternApp', namespaceType: 'single' }, + references: [], + } as SavedObjectsManagementRecord, + INDEX_PATTERN_BAR: { + type: 'index-pattern', + id: 'bar', + meta: { title: 'index-pattern-bar-title', icon: 'indexPatternApp', namespaceType: 'single' }, + references: [], + } as SavedObjectsManagementRecord, +}; + +interface ObjectProperties { + type: string; + id: string; + meta: { title?: string; icon?: string }; +} +const createSuccessResult = ({ type, id, meta }: ObjectProperties) => { + return { type, id, meta }; +}; +const createFailureConflict = ({ type, id, meta }: ObjectProperties): FailedImport => { + return { obj: { type, id, meta }, error: { type: 'conflict' } }; +}; +const createFailureMissingReferences = ({ type, id, meta }: ObjectProperties): FailedImport => { + return { + obj: { type, id, meta }, + error: { type: 'missing_references', references: [] }, + }; +}; +const createFailureUnresolvable = ({ type, id, meta }: ObjectProperties): FailedImport => { + return { + obj: { type, id, meta }, + // currently, unresolvable errors are 'unsupported_type' and 'unknown'; either would work for this test case + error: { type: 'unknown', message: 'some error message', statusCode: 400 }, + }; +}; const createCopyResult = ( - opts: { withConflicts?: boolean; withUnresolvableError?: boolean } = {} + opts: { + withConflicts?: boolean; + withMissingReferencesError?: boolean; + withUnresolvableError?: boolean; + overwrite?: boolean; + } = {} ) => { - const failedImports: ProcessedImportResponse['failedImports'] = []; + let successfulImports: ProcessedImportResponse['successfulImports'] = [ + createSuccessResult(OBJECTS.MY_DASHBOARD), + ]; + let failedImports: ProcessedImportResponse['failedImports'] = []; if (opts.withConflicts) { - failedImports.push( - { - obj: { type: 'visualization', id: 'foo-viz' }, - error: { type: 'conflict' }, - }, - { - obj: { type: 'index-pattern', id: 'transient-index-pattern-conflict' }, - error: { type: 'conflict' }, - } - ); + failedImports.push(createFailureConflict(OBJECTS.VISUALIZATION_FOO)); + } else { + successfulImports.push(createSuccessResult(OBJECTS.VISUALIZATION_FOO)); } if (opts.withUnresolvableError) { - failedImports.push({ - obj: { type: 'visualization', id: 'bar-viz' }, - error: { type: 'missing_references', blocking: [], references: [] }, - }); + failedImports.push(createFailureUnresolvable(OBJECTS.INDEX_PATTERN_FOO)); + } else { + successfulImports.push(createSuccessResult(OBJECTS.INDEX_PATTERN_FOO)); + } + if (opts.withMissingReferencesError) { + failedImports.push(createFailureMissingReferences(OBJECTS.VISUALIZATION_BAR)); + // INDEX_PATTERN_BAR is not present in the source space, therefore VISUALIZATION_BAR resulted in a missing_references error + } else { + successfulImports.push( + createSuccessResult(OBJECTS.VISUALIZATION_BAR), + createSuccessResult(OBJECTS.INDEX_PATTERN_BAR) + ); + } + + if (opts.overwrite) { + failedImports = failedImports.map(({ obj, error }) => ({ + obj: { ...obj, overwrite: true }, + error, + })); + successfulImports = successfulImports.map((obj) => ({ ...obj, overwrite: true })); } const copyResult: ProcessedImportResponse = { + successfulImports, failedImports, } as ProcessedImportResponse; @@ -57,109 +130,101 @@ const createCopyResult = ( describe('summarizeCopyResult', () => { it('indicates the result is processing when not provided', () => { - const SavedObjectsManagementRecord = createSavedObjectsManagementRecord(); const copyResult = undefined; - const includeRelated = true; - - const summarizedResult = summarizeCopyResult( - SavedObjectsManagementRecord, - copyResult, - includeRelated - ); + const summarizedResult = summarizeCopyResult(OBJECTS.MY_DASHBOARD, copyResult); expect(summarizedResult).toMatchInlineSnapshot(` Object { "objects": Array [ Object { - "conflicts": Array [], + "conflict": undefined, + "hasMissingReferences": false, "hasUnresolvableErrors": false, + "icon": "dashboardApp", "id": "foo", - "name": "my-dashboard", + "name": "my-dashboard-title", + "overwrite": false, "type": "dashboard", }, - Object { - "conflicts": Array [], - "hasUnresolvableErrors": false, - "id": "foo-viz", - "name": "Foo Viz", - "type": "visualization", - }, - Object { - "conflicts": Array [], - "hasUnresolvableErrors": false, - "id": "bar-viz", - "name": "Bar Viz", - "type": "visualization", - }, ], "processing": true, } `); }); - it('processes failedImports to extract conflicts, including transient conflicts', () => { - const SavedObjectsManagementRecord = createSavedObjectsManagementRecord(); + it('processes failedImports to extract conflicts, including transitive conflicts', () => { const copyResult = createCopyResult({ withConflicts: true }); - const includeRelated = true; + const summarizedResult = summarizeCopyResult(OBJECTS.MY_DASHBOARD, copyResult); - const summarizedResult = summarizeCopyResult( - SavedObjectsManagementRecord, - copyResult, - includeRelated - ); expect(summarizedResult).toMatchInlineSnapshot(` Object { "hasConflicts": true, + "hasMissingReferences": false, "hasUnresolvableErrors": false, "objects": Array [ Object { - "conflicts": Array [], + "conflict": undefined, + "hasMissingReferences": false, "hasUnresolvableErrors": false, + "icon": "dashboardApp", "id": "foo", - "name": "my-dashboard", + "name": "my-dashboard-title", + "overwrite": false, "type": "dashboard", }, Object { - "conflicts": Array [ - Object { - "error": Object { - "type": "conflict", - }, - "obj": Object { - "id": "foo-viz", - "type": "visualization", + "conflict": Object { + "error": Object { + "type": "conflict", + }, + "obj": Object { + "id": "bar", + "meta": Object { + "icon": "visualizeApp", + "namespaceType": "single", + "title": "visualization-foo-title", }, + "type": "visualization", }, - ], + }, + "hasMissingReferences": false, "hasUnresolvableErrors": false, - "id": "foo-viz", - "name": "Foo Viz", + "icon": "visualizeApp", + "id": "bar", + "name": "visualization-foo-title", + "overwrite": false, "type": "visualization", }, Object { - "conflicts": Array [], + "conflict": undefined, + "hasMissingReferences": false, "hasUnresolvableErrors": false, - "id": "bar-viz", - "name": "Bar Viz", - "type": "visualization", + "icon": "indexPatternApp", + "id": "foo", + "name": "index-pattern-foo-title", + "overwrite": false, + "type": "index-pattern", }, Object { - "conflicts": Array [ - Object { - "error": Object { - "type": "conflict", - }, - "obj": Object { - "id": "transient-index-pattern-conflict", - "type": "index-pattern", - }, - }, - ], + "conflict": undefined, + "hasMissingReferences": false, "hasUnresolvableErrors": false, - "id": "transient-index-pattern-conflict", - "name": "transient-index-pattern-conflict", + "icon": "indexPatternApp", + "id": "bar", + "name": "index-pattern-bar-title", + "overwrite": false, "type": "index-pattern", }, + Object { + "conflict": undefined, + "hasMissingReferences": false, + "hasUnresolvableErrors": false, + "icon": "visualizeApp", + "id": "baz", + "name": "visualization-bar-title", + "overwrite": false, + "type": "visualization", + }, ], "processing": false, "successful": false, @@ -167,40 +232,54 @@ describe('summarizeCopyResult', () => { `); }); - it('processes failedImports to extract unresolvable errors', () => { - const SavedObjectsManagementRecord = createSavedObjectsManagementRecord(); - const copyResult = createCopyResult({ withUnresolvableError: true }); - const includeRelated = true; + it('processes failedImports to extract missing references errors', () => { + const copyResult = createCopyResult({ withMissingReferencesError: true }); + const summarizedResult = summarizeCopyResult(OBJECTS.MY_DASHBOARD, copyResult); - const summarizedResult = summarizeCopyResult( - SavedObjectsManagementRecord, - copyResult, - includeRelated - ); expect(summarizedResult).toMatchInlineSnapshot(` Object { "hasConflicts": false, - "hasUnresolvableErrors": true, + "hasMissingReferences": true, + "hasUnresolvableErrors": false, "objects": Array [ Object { - "conflicts": Array [], + "conflict": undefined, + "hasMissingReferences": false, "hasUnresolvableErrors": false, + "icon": "dashboardApp", "id": "foo", - "name": "my-dashboard", + "name": "my-dashboard-title", + "overwrite": false, "type": "dashboard", }, Object { - "conflicts": Array [], + "conflict": undefined, + "hasMissingReferences": true, "hasUnresolvableErrors": false, - "id": "foo-viz", - "name": "Foo Viz", + "icon": "visualizeApp", + "id": "baz", + "name": "visualization-bar-title", + "overwrite": false, "type": "visualization", }, Object { - "conflicts": Array [], - "hasUnresolvableErrors": true, - "id": "bar-viz", - "name": "Bar Viz", + "conflict": undefined, + "hasMissingReferences": false, + "hasUnresolvableErrors": false, + "icon": "indexPatternApp", + "id": "foo", + "name": "index-pattern-foo-title", + "overwrite": false, + "type": "index-pattern", + }, + Object { + "conflict": undefined, + "hasMissingReferences": false, + "hasUnresolvableErrors": false, + "icon": "visualizeApp", + "id": "bar", + "name": "visualization-foo-title", + "overwrite": false, "type": "visualization", }, ], @@ -210,75 +289,147 @@ describe('summarizeCopyResult', () => { `); }); - it('processes a result without errors', () => { - const SavedObjectsManagementRecord = createSavedObjectsManagementRecord(); - const copyResult = createCopyResult(); - const includeRelated = true; + it('processes failedImports to extract unresolvable errors', () => { + const copyResult = createCopyResult({ withUnresolvableError: true }); + const summarizedResult = summarizeCopyResult(OBJECTS.MY_DASHBOARD, copyResult); - const summarizedResult = summarizeCopyResult( - SavedObjectsManagementRecord, - copyResult, - includeRelated - ); expect(summarizedResult).toMatchInlineSnapshot(` Object { "hasConflicts": false, - "hasUnresolvableErrors": false, + "hasMissingReferences": false, + "hasUnresolvableErrors": true, "objects": Array [ Object { - "conflicts": Array [], + "conflict": undefined, + "hasMissingReferences": false, "hasUnresolvableErrors": false, + "icon": "dashboardApp", "id": "foo", - "name": "my-dashboard", + "name": "my-dashboard-title", + "overwrite": false, "type": "dashboard", }, Object { - "conflicts": Array [], + "conflict": undefined, + "hasMissingReferences": false, + "hasUnresolvableErrors": true, + "icon": "indexPatternApp", + "id": "foo", + "name": "index-pattern-foo-title", + "overwrite": false, + "type": "index-pattern", + }, + Object { + "conflict": undefined, + "hasMissingReferences": false, "hasUnresolvableErrors": false, - "id": "foo-viz", - "name": "Foo Viz", + "icon": "indexPatternApp", + "id": "bar", + "name": "index-pattern-bar-title", + "overwrite": false, + "type": "index-pattern", + }, + Object { + "conflict": undefined, + "hasMissingReferences": false, + "hasUnresolvableErrors": false, + "icon": "visualizeApp", + "id": "bar", + "name": "visualization-foo-title", + "overwrite": false, "type": "visualization", }, Object { - "conflicts": Array [], + "conflict": undefined, + "hasMissingReferences": false, "hasUnresolvableErrors": false, - "id": "bar-viz", - "name": "Bar Viz", + "icon": "visualizeApp", + "id": "baz", + "name": "visualization-bar-title", + "overwrite": false, "type": "visualization", }, ], "processing": false, - "successful": true, + "successful": false, } `); }); - it('does not include references unless requested', () => { - const SavedObjectsManagementRecord = createSavedObjectsManagementRecord(); + it('processes a result without errors', () => { const copyResult = createCopyResult(); - const includeRelated = false; + const summarizedResult = summarizeCopyResult(OBJECTS.MY_DASHBOARD, copyResult); - const summarizedResult = summarizeCopyResult( - SavedObjectsManagementRecord, - copyResult, - includeRelated - ); expect(summarizedResult).toMatchInlineSnapshot(` Object { "hasConflicts": false, + "hasMissingReferences": false, "hasUnresolvableErrors": false, "objects": Array [ Object { - "conflicts": Array [], + "conflict": undefined, + "hasMissingReferences": false, "hasUnresolvableErrors": false, + "icon": "dashboardApp", "id": "foo", - "name": "my-dashboard", + "name": "my-dashboard-title", + "overwrite": false, "type": "dashboard", }, + Object { + "conflict": undefined, + "hasMissingReferences": false, + "hasUnresolvableErrors": false, + "icon": "indexPatternApp", + "id": "foo", + "name": "index-pattern-foo-title", + "overwrite": false, + "type": "index-pattern", + }, + Object { + "conflict": undefined, + "hasMissingReferences": false, + "hasUnresolvableErrors": false, + "icon": "indexPatternApp", + "id": "bar", + "name": "index-pattern-bar-title", + "overwrite": false, + "type": "index-pattern", + }, + Object { + "conflict": undefined, + "hasMissingReferences": false, + "hasUnresolvableErrors": false, + "icon": "visualizeApp", + "id": "bar", + "name": "visualization-foo-title", + "overwrite": false, + "type": "visualization", + }, + Object { + "conflict": undefined, + "hasMissingReferences": false, + "hasUnresolvableErrors": false, + "icon": "visualizeApp", + "id": "baz", + "name": "visualization-bar-title", + "overwrite": false, + "type": "visualization", + }, ], "processing": false, "successful": true, } `); }); + + it('indicates when successes and failures have been overwritten', () => { + const copyResult = createCopyResult({ withMissingReferencesError: true, overwrite: true }); + const summarizedResult = summarizeCopyResult(OBJECTS.MY_DASHBOARD, copyResult); + + expect(summarizedResult.objects).toHaveLength(4); + for (const obj of summarizedResult.objects) { + expect(obj.overwrite).toBe(true); + } + }); }); diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.ts index 518e89df579a64..0c07d1a5da7eb5 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.ts @@ -7,19 +7,28 @@ import { SavedObjectsManagementRecord, ProcessedImportResponse, + FailedImport, } from 'src/plugins/saved_objects_management/public'; +import { + SavedObjectsImportConflictError, + SavedObjectsImportAmbiguousConflictError, +} from 'kibana/public'; export interface SummarizedSavedObjectResult { type: string; id: string; name: string; - conflicts: ProcessedImportResponse['failedImports']; + icon: string; + conflict?: FailedImportConflict; + hasMissingReferences: boolean; hasUnresolvableErrors: boolean; + overwrite: boolean; } interface SuccessfulResponse { successful: true; hasConflicts: false; + hasMissingReferences: false; hasUnresolvableErrors: false; objects: SummarizedSavedObjectResult[]; processing: false; @@ -27,6 +36,7 @@ interface SuccessfulResponse { interface UnsuccessfulResponse { successful: false; hasConflicts: boolean; + hasMissingReferences: boolean; hasUnresolvableErrors: boolean; objects: SummarizedSavedObjectResult[]; processing: false; @@ -37,6 +47,19 @@ interface ProcessingResponse { processing: true; } +interface FailedImportConflict { + obj: FailedImport['obj']; + error: SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError; +} + +const isAnyConflict = (failure: FailedImport): failure is FailedImportConflict => + failure.error.type === 'conflict' || failure.error.type === 'ambiguous_conflict'; +const isMissingReferences = (failure: FailedImport) => failure.error.type === 'missing_references'; +const isUnresolvableError = (failure: FailedImport) => + !isAnyConflict(failure) && !isMissingReferences(failure); +const typeComparator = (a: { type: string }, b: { type: string }) => + a.type > b.type ? 1 : a.type < b.type ? -1 : 0; + export type SummarizedCopyToSpaceResult = | SuccessfulResponse | UnsuccessfulResponse @@ -44,69 +67,61 @@ export type SummarizedCopyToSpaceResult = export function summarizeCopyResult( savedObject: SavedObjectsManagementRecord, - copyResult: ProcessedImportResponse | undefined, - includeRelated: boolean + copyResult: ProcessedImportResponse | undefined ): SummarizedCopyToSpaceResult { - const successful = Boolean(copyResult && copyResult.failedImports.length === 0); - - const conflicts = copyResult - ? copyResult.failedImports.filter((failed) => failed.error.type === 'conflict') - : []; - - const unresolvableErrors = copyResult - ? copyResult.failedImports.filter((failed) => failed.error.type !== 'conflict') - : []; - - const hasConflicts = conflicts.length > 0; - - const hasUnresolvableErrors = Boolean( - copyResult && copyResult.failedImports.some((failed) => failed.error.type !== 'conflict') - ); + const conflicts = copyResult?.failedImports.filter(isAnyConflict) ?? []; + const missingReferences = copyResult?.failedImports.filter(isMissingReferences) ?? []; + const unresolvableErrors = + copyResult?.failedImports.filter((failed) => isUnresolvableError(failed)) ?? []; + const getExtraFields = ({ type, id }: { type: string; id: string }) => { + const conflict = conflicts.find(({ obj }) => obj.type === type && obj.id === id); + const missingReference = missingReferences.find( + ({ obj }) => obj.type === type && obj.id === id + ); + const hasMissingReferences = missingReference !== undefined; + const hasUnresolvableErrors = unresolvableErrors.some( + ({ obj }) => obj.type === type && obj.id === id + ); + const overwrite = conflict + ? false + : missingReference + ? missingReference.obj.overwrite === true + : copyResult?.successfulImports.some( + (obj) => obj.type === type && obj.id === id && obj.overwrite + ) === true; + + return { conflict, hasMissingReferences, hasUnresolvableErrors, overwrite }; + }; - const objectMap = new Map(); + const objectMap = new Map(); objectMap.set(`${savedObject.type}:${savedObject.id}`, { type: savedObject.type, id: savedObject.id, name: savedObject.meta.title, - conflicts: conflicts.filter( - (c) => c.obj.type === savedObject.type && c.obj.id === savedObject.id - ), - hasUnresolvableErrors: unresolvableErrors.some( - (e) => e.obj.type === savedObject.type && e.obj.id === savedObject.id - ), + icon: savedObject.meta.icon, + ...getExtraFields(savedObject), }); - if (includeRelated) { - savedObject.references.forEach((ref) => { - objectMap.set(`${ref.type}:${ref.id}`, { - type: ref.type, - id: ref.id, - name: ref.name, - conflicts: conflicts.filter((c) => c.obj.type === ref.type && c.obj.id === ref.id), - hasUnresolvableErrors: unresolvableErrors.some( - (e) => e.obj.type === ref.type && e.obj.id === ref.id - ), - }); - }); - - // The `savedObject.references` array only includes the direct references. It does not include any references of references. - // Therefore, if there are conflicts detected in these transitive references, we need to include them here so that they are visible - // in the UI as resolvable conflicts. - const transitiveConflicts = conflicts.filter( - (c) => !objectMap.has(`${c.obj.type}:${c.obj.id}`) - ); - transitiveConflicts.forEach((conflict) => { - objectMap.set(`${conflict.obj.type}:${conflict.obj.id}`, { - type: conflict.obj.type, - id: conflict.obj.id, - name: conflict.obj.title || conflict.obj.id, - conflicts: conflicts.filter((c) => c.obj.type === conflict.obj.type && conflict.obj.id), - hasUnresolvableErrors: unresolvableErrors.some( - (e) => e.obj.type === conflict.obj.type && e.obj.id === conflict.obj.id - ), + const addObjectsToMap = ( + objects: Array<{ id: string; type: string; meta: { title?: string; icon?: string } }> + ) => { + objects.forEach((obj) => { + const { type, id, meta } = obj; + objectMap.set(`${type}:${id}`, { + type, + id, + name: meta.title || `${type} [id=${id}]`, + icon: meta.icon || 'apps', + ...getExtraFields(obj), }); }); - } + }; + const failedImports = (copyResult?.failedImports ?? []) + .map(({ obj }) => obj) + .sort(typeComparator); + addObjectsToMap(failedImports); + const successfulImports = (copyResult?.successfulImports ?? []).sort(typeComparator); + addObjectsToMap(successfulImports); if (typeof copyResult === 'undefined') { return { @@ -115,20 +130,26 @@ export function summarizeCopyResult( }; } + const successful = Boolean(copyResult && copyResult.failedImports.length === 0); if (successful) { return { successful, hasConflicts: false, objects: Array.from(objectMap.values()), + hasMissingReferences: false, hasUnresolvableErrors: false, processing: false, }; } + const hasConflicts = conflicts.length > 0; + const hasMissingReferences = missingReferences.length > 0; + const hasUnresolvableErrors = unresolvableErrors.length > 0; return { successful, hasConflicts, objects: Array.from(objectMap.values()), + hasMissingReferences, hasUnresolvableErrors, processing: false, }; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/types.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/types.ts index 9fcc5a89736cc3..2310f6c96937ce 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/types.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/types.ts @@ -8,6 +8,7 @@ import { SavedObjectsImportRetry, SavedObjectsImportResponse } from 'src/core/pu export interface CopyOptions { includeRelated: boolean; + createNewCopies: boolean; overwrite: boolean; selectedSpaceIds: string[]; } diff --git a/x-pack/plugins/spaces/public/plugin.tsx b/x-pack/plugins/spaces/public/plugin.tsx index 8589993a97e024..cd31a4aa17fc38 100644 --- a/x-pack/plugins/spaces/public/plugin.tsx +++ b/x-pack/plugins/spaces/public/plugin.tsx @@ -15,6 +15,7 @@ import { SpacesManager } from './spaces_manager'; import { initSpacesNavControl } from './nav_control'; import { createSpacesFeatureCatalogueEntry } from './create_feature_catalogue_entry'; import { CopySavedObjectsToSpaceService } from './copy_saved_objects_to_space'; +import { ShareSavedObjectsToSpaceService } from './share_saved_objects_to_space'; import { AdvancedSettingsService } from './advanced_settings'; import { ManagementService } from './management'; import { spaceSelectorApp } from './space_selector'; @@ -67,6 +68,12 @@ export class SpacesPlugin implements Plugin void; + disabled?: boolean; +} + +type SpaceOption = EuiSelectableOption & { ['data-space-id']: string }; + +const activeSpaceProps = { + append: Current, + disabled: true, + checked: 'on' as 'on', +}; + +export const SelectableSpacesControl = (props: Props) => { + if (props.spaces.length === 0) { + return ; + } + + const options = props.spaces + .sort((a, b) => (a.isActiveSpace ? -1 : b.isActiveSpace ? 1 : 0)) + .map((space) => ({ + label: space.name, + prepend: , + checked: props.selectedSpaceIds.includes(space.id) ? 'on' : undefined, + ['data-space-id']: space.id, + ['data-test-subj']: `sts-space-selector-row-${space.id}`, + ...(space.isActiveSpace ? activeSpaceProps : {}), + })); + + function updateSelectedSpaces(selectedOptions: SpaceOption[]) { + if (props.disabled) return; + + const selectedSpaceIds = selectedOptions + .filter((opt) => opt.checked && !opt.disabled) + .map((opt) => opt['data-space-id']); + + props.onChange(selectedSpaceIds); + } + + return ( + updateSelectedSpaces(newOptions as SpaceOption[])} + listProps={{ + bordered: true, + rowHeight: 40, + className: 'spcShareToSpace__spacesList', + 'data-test-subj': 'sts-form-space-selector', + }} + searchable + > + {(list, search) => { + return ( + + {search} + {list} + + ); + }} + + ); +}; diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout.test.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout.test.tsx new file mode 100644 index 00000000000000..c17a2dcb1a831d --- /dev/null +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout.test.tsx @@ -0,0 +1,371 @@ +/* + * 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 Boom from 'boom'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { ShareSavedObjectsToSpaceFlyout } from './share_to_space_flyout'; +import { ShareToSpaceForm } from './share_to_space_form'; +import { EuiLoadingSpinner, EuiEmptyPrompt } from '@elastic/eui'; +import { Space } from '../../../common/model/space'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { SelectableSpacesControl } from './selectable_spaces_control'; +import { act } from '@testing-library/react'; +import { spacesManagerMock } from '../../spaces_manager/mocks'; +import { SpacesManager } from '../../spaces_manager'; +import { ToastsApi } from 'src/core/public'; +import { EuiCallOut } from '@elastic/eui'; +import { CopySavedObjectsToSpaceFlyout } from '../../copy_saved_objects_to_space/components'; +import { SavedObjectsManagementRecord } from 'src/plugins/saved_objects_management/public'; + +interface SetupOpts { + mockSpaces?: Space[]; + namespaces?: string[]; + returnBeforeSpacesLoad?: boolean; +} + +const setup = async (opts: SetupOpts = {}) => { + const onClose = jest.fn(); + const onObjectUpdated = jest.fn(); + + const mockSpacesManager = spacesManagerMock.create(); + + mockSpacesManager.getActiveSpace.mockResolvedValue({ + id: 'my-active-space', + name: 'my active space', + disabledFeatures: [], + }); + + mockSpacesManager.getSpaces.mockResolvedValue( + opts.mockSpaces || [ + { + id: 'space-1', + name: 'Space 1', + disabledFeatures: [], + }, + { + id: 'space-2', + name: 'Space 2', + disabledFeatures: [], + }, + { + id: 'space-3', + name: 'Space 3', + disabledFeatures: [], + }, + { + id: 'my-active-space', + name: 'my active space', + disabledFeatures: [], + }, + ] + ); + + const mockToastNotifications = { + addError: jest.fn(), + addSuccess: jest.fn(), + }; + const savedObjectToShare = { + type: 'dashboard', + id: 'my-dash', + references: [ + { + type: 'visualization', + id: 'my-viz', + name: 'My Viz', + }, + ], + meta: { icon: 'dashboard', title: 'foo' }, + namespaces: opts.namespaces || ['my-active-space', 'space-1'], + } as SavedObjectsManagementRecord; + + const wrapper = mountWithIntl( + + ); + + if (!opts.returnBeforeSpacesLoad) { + // Wait for spaces manager to complete and flyout to rerender + await act(async () => { + await nextTick(); + wrapper.update(); + }); + } + + return { wrapper, onClose, mockSpacesManager, mockToastNotifications, savedObjectToShare }; +}; + +describe('ShareToSpaceFlyout', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + it('waits for spaces to load', async () => { + const { wrapper } = await setup({ returnBeforeSpacesLoad: true }); + + expect(wrapper.find(ShareToSpaceForm)).toHaveLength(0); + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(0); + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(ShareToSpaceForm)).toHaveLength(1); + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(0); + }); + + it('shows a message within an EuiEmptyPrompt when no spaces are available', async () => { + const { wrapper, onClose } = await setup({ mockSpaces: [] }); + + expect(wrapper.find(ShareToSpaceForm)).toHaveLength(0); + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + expect(onClose).toHaveBeenCalledTimes(0); + }); + + it('shows a message within an EuiEmptyPrompt when only the active space is available', async () => { + const { wrapper, onClose } = await setup({ + mockSpaces: [{ id: 'my-active-space', name: '', disabledFeatures: [] }], + }); + + expect(wrapper.find(ShareToSpaceForm)).toHaveLength(0); + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + expect(onClose).toHaveBeenCalledTimes(0); + }); + + it('does not show a warning callout when the saved object has multiple namespaces', async () => { + const { wrapper, onClose } = await setup(); + + expect(wrapper.find(ShareToSpaceForm)).toHaveLength(1); + expect(wrapper.find(EuiCallOut)).toHaveLength(0); + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + expect(onClose).toHaveBeenCalledTimes(0); + }); + + it('shows a warning callout when the saved object only has one namespace', async () => { + const { wrapper, onClose } = await setup({ namespaces: ['my-active-space'] }); + + expect(wrapper.find(ShareToSpaceForm)).toHaveLength(1); + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + expect(onClose).toHaveBeenCalledTimes(0); + }); + + it('does not show the Copy flyout by default', async () => { + const { wrapper, onClose } = await setup({ namespaces: ['my-active-space'] }); + + expect(wrapper.find(ShareToSpaceForm)).toHaveLength(1); + expect(wrapper.find(CopySavedObjectsToSpaceFlyout)).toHaveLength(0); + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + expect(onClose).toHaveBeenCalledTimes(0); + }); + + it('shows the Copy flyout if the the "Make a copy" button is clicked', async () => { + const { wrapper, onClose } = await setup({ namespaces: ['my-active-space'] }); + + expect(wrapper.find(ShareToSpaceForm)).toHaveLength(1); + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(0); + + const copyButton = findTestSubject(wrapper, 'sts-copy-button'); // this button is only present in the warning callout + + await act(async () => { + copyButton.simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(CopySavedObjectsToSpaceFlyout)).toHaveLength(1); + expect(onClose).toHaveBeenCalledTimes(0); + }); + + it('handles errors thrown from shareSavedObjectsAdd API call', async () => { + const { wrapper, mockSpacesManager, mockToastNotifications } = await setup(); + + mockSpacesManager.shareSavedObjectAdd.mockImplementation(() => { + return Promise.reject(Boom.serverUnavailable('Something bad happened')); + }); + + expect(wrapper.find(ShareToSpaceForm)).toHaveLength(1); + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(0); + + // Using props callback instead of simulating clicks, + // because EuiSelectable uses a virtualized list, which isn't easily testable via test subjects + const spaceSelector = wrapper.find(SelectableSpacesControl); + act(() => { + spaceSelector.props().onChange(['space-2', 'space-3']); + }); + + const startButton = findTestSubject(wrapper, 'sts-initiate-button'); + + await act(async () => { + startButton.simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(mockSpacesManager.shareSavedObjectAdd).toHaveBeenCalled(); + expect(mockSpacesManager.shareSavedObjectRemove).not.toHaveBeenCalled(); + expect(mockToastNotifications.addError).toHaveBeenCalled(); + }); + + it('handles errors thrown from shareSavedObjectsRemove API call', async () => { + const { wrapper, mockSpacesManager, mockToastNotifications } = await setup(); + + mockSpacesManager.shareSavedObjectRemove.mockImplementation(() => { + return Promise.reject(Boom.serverUnavailable('Something bad happened')); + }); + + expect(wrapper.find(ShareToSpaceForm)).toHaveLength(1); + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(0); + + // Using props callback instead of simulating clicks, + // because EuiSelectable uses a virtualized list, which isn't easily testable via test subjects + const spaceSelector = wrapper.find(SelectableSpacesControl); + act(() => { + spaceSelector.props().onChange(['space-2', 'space-3']); + }); + + const startButton = findTestSubject(wrapper, 'sts-initiate-button'); + + await act(async () => { + startButton.simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(mockSpacesManager.shareSavedObjectAdd).toHaveBeenCalled(); + expect(mockSpacesManager.shareSavedObjectRemove).toHaveBeenCalled(); + expect(mockToastNotifications.addError).toHaveBeenCalled(); + }); + + it('allows the form to be filled out to add a space', async () => { + const { + wrapper, + onClose, + mockSpacesManager, + mockToastNotifications, + savedObjectToShare, + } = await setup(); + + expect(wrapper.find(ShareToSpaceForm)).toHaveLength(1); + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(0); + + // Using props callback instead of simulating clicks, + // because EuiSelectable uses a virtualized list, which isn't easily testable via test subjects + const spaceSelector = wrapper.find(SelectableSpacesControl); + + act(() => { + spaceSelector.props().onChange(['space-1', 'space-2', 'space-3']); + }); + + const startButton = findTestSubject(wrapper, 'sts-initiate-button'); + + await act(async () => { + startButton.simulate('click'); + await nextTick(); + wrapper.update(); + }); + + const { type, id } = savedObjectToShare; + const { shareSavedObjectAdd, shareSavedObjectRemove } = mockSpacesManager; + expect(shareSavedObjectAdd).toHaveBeenCalledWith({ type, id }, ['space-2', 'space-3']); + expect(shareSavedObjectRemove).not.toHaveBeenCalled(); + + expect(mockToastNotifications.addSuccess).toHaveBeenCalledTimes(1); + expect(mockToastNotifications.addError).not.toHaveBeenCalled(); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it('allows the form to be filled out to remove a space', async () => { + const { + wrapper, + onClose, + mockSpacesManager, + mockToastNotifications, + savedObjectToShare, + } = await setup(); + + expect(wrapper.find(ShareToSpaceForm)).toHaveLength(1); + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(0); + + // Using props callback instead of simulating clicks, + // because EuiSelectable uses a virtualized list, which isn't easily testable via test subjects + const spaceSelector = wrapper.find(SelectableSpacesControl); + + act(() => { + spaceSelector.props().onChange([]); + }); + + const startButton = findTestSubject(wrapper, 'sts-initiate-button'); + + await act(async () => { + startButton.simulate('click'); + await nextTick(); + wrapper.update(); + }); + + const { type, id } = savedObjectToShare; + const { shareSavedObjectAdd, shareSavedObjectRemove } = mockSpacesManager; + expect(shareSavedObjectAdd).not.toHaveBeenCalled(); + expect(shareSavedObjectRemove).toHaveBeenCalledWith({ type, id }, ['space-1']); + + expect(mockToastNotifications.addSuccess).toHaveBeenCalledTimes(1); + expect(mockToastNotifications.addError).not.toHaveBeenCalled(); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it('allows the form to be filled out to add and remove a space', async () => { + const { + wrapper, + onClose, + mockSpacesManager, + mockToastNotifications, + savedObjectToShare, + } = await setup(); + + expect(wrapper.find(ShareToSpaceForm)).toHaveLength(1); + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(0); + + // Using props callback instead of simulating clicks, + // because EuiSelectable uses a virtualized list, which isn't easily testable via test subjects + const spaceSelector = wrapper.find(SelectableSpacesControl); + + act(() => { + spaceSelector.props().onChange(['space-2', 'space-3']); + }); + + const startButton = findTestSubject(wrapper, 'sts-initiate-button'); + + await act(async () => { + startButton.simulate('click'); + await nextTick(); + wrapper.update(); + }); + + const { type, id } = savedObjectToShare; + const { shareSavedObjectAdd, shareSavedObjectRemove } = mockSpacesManager; + expect(shareSavedObjectAdd).toHaveBeenCalledWith({ type, id }, ['space-2', 'space-3']); + expect(shareSavedObjectRemove).toHaveBeenCalledWith({ type, id }, ['space-1']); + + expect(mockToastNotifications.addSuccess).toHaveBeenCalledTimes(2); + expect(mockToastNotifications.addError).not.toHaveBeenCalled(); + expect(onClose).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout.tsx new file mode 100644 index 00000000000000..10cc5777cdcfff --- /dev/null +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout.tsx @@ -0,0 +1,268 @@ +/* + * 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, useEffect } from 'react'; +import { + EuiFlyout, + EuiIcon, + EuiFlyoutHeader, + EuiTitle, + EuiText, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiEmptyPrompt, + EuiButton, + EuiButtonEmpty, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ToastsStart } from 'src/core/public'; +import { SavedObjectsManagementRecord } from '../../../../../../src/plugins/saved_objects_management/public'; +import { Space } from '../../../common/model/space'; +import { SpacesManager } from '../../spaces_manager'; +import { ShareToSpaceForm } from './share_to_space_form'; +import { ShareOptions, SpaceTarget } from '../types'; +import { CopySavedObjectsToSpaceFlyout } from '../../copy_saved_objects_to_space/components'; + +interface Props { + onClose: () => void; + onObjectUpdated: () => void; + savedObject: SavedObjectsManagementRecord; + spacesManager: SpacesManager; + toastNotifications: ToastsStart; +} + +const arraysAreEqual = (a: unknown[], b: unknown[]) => + a.every((x) => b.includes(x)) && b.every((x) => a.includes(x)); + +export const ShareSavedObjectsToSpaceFlyout = (props: Props) => { + const { onClose, onObjectUpdated, savedObject, spacesManager, toastNotifications } = props; + const { namespaces: currentNamespaces = [] } = savedObject; + const [shareOptions, setShareOptions] = useState({ selectedSpaceIds: [] }); + const [showMakeCopy, setShowMakeCopy] = useState(false); + + const [{ isLoading, spaces }, setSpacesState] = useState<{ + isLoading: boolean; + spaces: SpaceTarget[]; + }>({ isLoading: true, spaces: [] }); + useEffect(() => { + const getSpaces = spacesManager.getSpaces('shareSavedObjectsIntoSpace'); + const getActiveSpace = spacesManager.getActiveSpace(); + Promise.all([getSpaces, getActiveSpace]) + .then(([allSpaces, activeSpace]) => { + const createSpaceTarget = (space: Space): SpaceTarget => ({ + ...space, + isActiveSpace: space.id === activeSpace.id, + }); + setSpacesState({ + isLoading: false, + spaces: allSpaces.map((space) => createSpaceTarget(space)), + }); + setShareOptions({ + selectedSpaceIds: currentNamespaces.filter((spaceId) => spaceId !== activeSpace.id), + }); + }) + .catch((e) => { + toastNotifications.addError(e, { + title: i18n.translate('xpack.spaces.management.shareToSpace.spacesLoadErrorTitle', { + defaultMessage: 'Error loading available spaces', + }), + }); + }); + }, [currentNamespaces, spacesManager, toastNotifications]); + + const getSelectionChanges = () => { + const activeSpace = spaces.find((space) => space.isActiveSpace); + if (!activeSpace) { + return { changed: false, spacesToAdd: [], spacesToRemove: [] }; + } + const initialSelection = currentNamespaces.filter( + (spaceId) => spaceId !== activeSpace.id && spaceId !== '?' + ); + const { selectedSpaceIds } = shareOptions; + const changed = !arraysAreEqual(initialSelection, selectedSpaceIds); + const spacesToAdd = selectedSpaceIds.filter((spaceId) => !initialSelection.includes(spaceId)); + const spacesToRemove = initialSelection.filter( + (spaceId) => !selectedSpaceIds.includes(spaceId) + ); + return { changed, spacesToAdd, spacesToRemove }; + }; + const { changed: isSelectionChanged, spacesToAdd, spacesToRemove } = getSelectionChanges(); + + const [shareInProgress, setShareInProgress] = useState(false); + + async function startShare() { + setShareInProgress(true); + try { + const { type, id, meta } = savedObject; + const title = + currentNamespaces.length === 1 + ? i18n.translate('xpack.spaces.management.shareToSpace.shareNewSuccessTitle', { + defaultMessage: 'Saved Object is now shared!', + }) + : i18n.translate('xpack.spaces.management.shareToSpace.shareEditSuccessTitle', { + defaultMessage: 'Saved Object updated', + }); + if (spacesToAdd.length > 0) { + await spacesManager.shareSavedObjectAdd({ type, id }, spacesToAdd); + const spaceNames = spacesToAdd.map( + (spaceId) => spaces.find((space) => space.id === spaceId)!.name + ); + const text = i18n.translate('xpack.spaces.management.shareToSpace.shareAddSuccessText', { + defaultMessage: `'{object}' was added to the following spaces:\n{spaces}`, + values: { object: meta.title, spaces: spaceNames.join(', ') }, + }); + toastNotifications.addSuccess({ title, text }); + } + if (spacesToRemove.length > 0) { + await spacesManager.shareSavedObjectRemove({ type, id }, spacesToRemove); + const spaceNames = spacesToRemove.map( + (spaceId) => spaces.find((space) => space.id === spaceId)!.name + ); + const text = i18n.translate('xpack.spaces.management.shareToSpace.shareRemoveSuccessText', { + defaultMessage: `'{object}' was removed from the following spaces:\n{spaces}`, + values: { object: meta.title, spaces: spaceNames.join(', ') }, + }); + toastNotifications.addSuccess({ title, text }); + } + onObjectUpdated(); + onClose(); + } catch (e) { + setShareInProgress(false); + toastNotifications.addError(e, { + title: i18n.translate('xpack.spaces.management.shareToSpace.shareErrorTitle', { + defaultMessage: 'Error updating saved object', + }), + }); + } + } + + const getFlyoutBody = () => { + // Step 1: loading assets for main form + if (isLoading) { + return ; + } + + // Step 1a: assets loaded, but no spaces are available for share. + // The `spaces` array includes the current space, so at minimum it will have a length of 1. + if (spaces.length < 2) { + return ( + + +

+ } + title={ +

+ +

+ } + /> + ); + } + + const showShareWarning = currentNamespaces.length === 1; + // Step 2: Share has not been initiated yet; User must fill out form to continue. + return ( + setShowMakeCopy(true)} + /> + ); + }; + + if (showMakeCopy) { + return ( + + ); + } + + return ( + + + + + + + + +

+ +

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

{savedObject.meta.title}

+
+
+
+ + + + {getFlyoutBody()} +
+ + + + + onClose()} + data-test-subj="sts-cancel-button" + disabled={shareInProgress} + > + + + + + startShare()} + data-test-subj="sts-initiate-button" + disabled={!isSelectionChanged || shareInProgress} + > + + + + + +
+ ); +}; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.scss b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_form.scss similarity index 74% rename from x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.scss rename to x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_form.scss index 87af5d83629a9c..41a9c907de745c 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.scss +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_form.scss @@ -1,10 +1,10 @@ // make icon occupy the same space as an EuiSwitch // icon is size m, which is the native $euiSize value // see @elastic/eui/src/components/icon/_variables.scss -.spcCopyToSpaceIncludeRelated .euiIcon { +.spcShareToSpaceIncludeRelated .euiIcon { margin-right: $euiSwitchWidth - $euiSize; } -.spcCopyToSpaceIncludeRelated__label { +.spcShareToSpaceIncludeRelated__label { font-size: $euiFontSizeS; } diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_form.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_form.tsx new file mode 100644 index 00000000000000..24402fec8d7715 --- /dev/null +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_form.tsx @@ -0,0 +1,94 @@ +/* + * 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 './share_to_space_form.scss'; +import React, { Fragment } from 'react'; +import { EuiHorizontalRule, EuiFormRow, EuiCallOut, EuiButton, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ShareOptions, SpaceTarget } from '../types'; +import { SelectableSpacesControl } from './selectable_spaces_control'; + +interface Props { + spaces: SpaceTarget[]; + onUpdate: (shareOptions: ShareOptions) => void; + shareOptions: ShareOptions; + showShareWarning: boolean; + makeCopy: () => void; +} + +export const ShareToSpaceForm = (props: Props) => { + const setSelectedSpaceIds = (selectedSpaceIds: string[]) => + props.onUpdate({ ...props.shareOptions, selectedSpaceIds }); + + const getShareWarning = () => { + if (!props.showShareWarning) { + return null; + } + + return ( + + + } + color="warning" + > + + + props.makeCopy()} + color="warning" + data-test-subj="sts-copy-button" + size="s" + > + + + + + + + ); + }; + + return ( +
+ {getShareWarning()} + + + } + labelAppend={ + + } + fullWidth + > + setSelectedSpaceIds(selection)} + /> + +
+ ); +}; diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/index.ts b/x-pack/plugins/spaces/public/share_saved_objects_to_space/index.ts new file mode 100644 index 00000000000000..037fcb684b47d7 --- /dev/null +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/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 { ShareSavedObjectsToSpaceService } from './share_saved_objects_to_space_service'; diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_action.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_action.tsx new file mode 100644 index 00000000000000..ba9a6473999df3 --- /dev/null +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_action.tsx @@ -0,0 +1,66 @@ +/* + * 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 { NotificationsStart } from 'src/core/public'; +import { + SavedObjectsManagementAction, + SavedObjectsManagementRecord, +} from '../../../../../src/plugins/saved_objects_management/public'; +import { ShareSavedObjectsToSpaceFlyout } from './components'; +import { SpacesManager } from '../spaces_manager'; + +export class ShareToSpaceSavedObjectsManagementAction extends SavedObjectsManagementAction { + public id: string = 'share_saved_objects_to_space'; + + public euiAction = { + name: i18n.translate('xpack.spaces.management.shareToSpace.actionTitle', { + defaultMessage: 'Share to space', + }), + description: i18n.translate('xpack.spaces.management.shareToSpace.actionDescription', { + defaultMessage: 'Share this saved object to one or more spaces', + }), + icon: 'share', + type: 'icon', + available: (object: SavedObjectsManagementRecord) => { + return object.meta.namespaceType === 'multiple'; + }, + onClick: (object: SavedObjectsManagementRecord) => { + this.isDataChanged = false; + this.start(object); + }, + }; + public refreshOnFinish = () => this.isDataChanged; + + private isDataChanged: boolean = false; + + constructor( + private readonly spacesManager: SpacesManager, + private readonly notifications: NotificationsStart + ) { + super(); + } + + public render = () => { + if (!this.record) { + throw new Error('No record available! `render()` was likely called before `start()`.'); + } + + return ( + (this.isDataChanged = true)} + savedObject={this.record} + spacesManager={this.spacesManager} + toastNotifications={this.notifications.toasts} + /> + ); + }; + + private onClose = () => { + this.finish(); + }; +} diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_column.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_column.tsx new file mode 100644 index 00000000000000..e8649faa120be3 --- /dev/null +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_column.tsx @@ -0,0 +1,142 @@ +/* + * 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, ReactNode } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiBadge } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiToolTip } from '@elastic/eui'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + SavedObjectsManagementColumn, + SavedObjectsManagementRecord, +} from '../../../../../src/plugins/saved_objects_management/public'; +import { SpaceTarget } from './types'; +import { SpacesManager } from '../spaces_manager'; +import { getSpaceColor } from '..'; + +const SPACES_DISPLAY_COUNT = 5; + +type SpaceMap = Map; +interface ColumnDataProps { + namespaces?: string[]; + data?: SpaceMap; +} + +const ColumnDisplay = ({ namespaces, data }: ColumnDataProps) => { + const [isExpanded, setIsExpanded] = useState(false); + + if (!data) { + return null; + } + + const authorized = namespaces?.filter((namespace) => namespace !== '?') ?? []; + const authorizedSpaceTargets: SpaceTarget[] = []; + authorized.forEach((namespace) => { + const spaceTarget = data.get(namespace); + if (spaceTarget === undefined) { + // in the event that a new space was created after this page has loaded, fall back to displaying the space ID + authorizedSpaceTargets.push({ + id: namespace, + name: namespace, + disabledFeatures: [], + isActiveSpace: false, + }); + } else if (!spaceTarget.isActiveSpace) { + authorizedSpaceTargets.push(spaceTarget); + } + }); + const unauthorizedCount = (namespaces?.filter((namespace) => namespace === '?') ?? []).length; + const unauthorizedTooltip = i18n.translate( + 'xpack.spaces.management.shareToSpace.columnUnauthorizedLabel', + { defaultMessage: 'You do not have permission to view these spaces' } + ); + + const displayedSpaces = isExpanded + ? authorizedSpaceTargets + : authorizedSpaceTargets.slice(0, SPACES_DISPLAY_COUNT); + const showButton = authorizedSpaceTargets.length > SPACES_DISPLAY_COUNT; + + const unauthorizedCountBadge = + (isExpanded || !showButton) && unauthorizedCount > 0 ? ( + + + +{unauthorizedCount} + + + ) : null; + + let button: ReactNode = null; + if (showButton) { + button = isExpanded ? ( + setIsExpanded(false)}> + + + ) : ( + setIsExpanded(true)}> + + + ); + } + + return ( + + {displayedSpaces.map(({ id, name, color }) => ( + + {name} + + ))} + {unauthorizedCountBadge} + {button} + + ); +}; + +export class ShareToSpaceSavedObjectsManagementColumn + implements SavedObjectsManagementColumn { + public id: string = 'share_saved_objects_to_space'; + public data: Map | undefined; + + public euiColumn = { + field: 'namespaces', + name: i18n.translate('xpack.spaces.management.shareToSpace.columnTitle', { + defaultMessage: 'Shared spaces', + }), + description: i18n.translate('xpack.spaces.management.shareToSpace.columnDescription', { + defaultMessage: 'The other spaces that this object is currently shared to', + }), + render: (namespaces: string[] | undefined, _object: SavedObjectsManagementRecord) => ( + + ), + }; + + constructor(private readonly spacesManager: SpacesManager) {} + + public loadData = () => { + this.data = undefined; + return Promise.all([this.spacesManager.getSpaces(), this.spacesManager.getActiveSpace()]).then( + ([spaces, activeSpace]) => { + this.data = spaces + .map((space) => ({ + ...space, + isActiveSpace: space.id === activeSpace.id, + color: getSpaceColor(space), + })) + .reduce((acc, cur) => acc.set(cur.id, cur), new Map()); + return this.data; + } + ); + }; +} diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_service.test.ts b/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_service.test.ts new file mode 100644 index 00000000000000..0f0fa7d22214f2 --- /dev/null +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_service.test.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 { ShareToSpaceSavedObjectsManagementAction } from './share_saved_objects_to_space_action'; +// import { ShareToSpaceSavedObjectsManagementColumn } from './share_saved_objects_to_space_column'; +import { spacesManagerMock } from '../spaces_manager/mocks'; +import { ShareSavedObjectsToSpaceService } from '.'; +import { notificationServiceMock } from 'src/core/public/mocks'; +import { savedObjectsManagementPluginMock } from '../../../../../src/plugins/saved_objects_management/public/mocks'; + +describe('ShareSavedObjectsToSpaceService', () => { + describe('#setup', () => { + it('registers the ShareToSpaceSavedObjectsManagement Action and Column', () => { + const deps = { + spacesManager: spacesManagerMock.create(), + notificationsSetup: notificationServiceMock.createSetupContract(), + savedObjectsManagementSetup: savedObjectsManagementPluginMock.createSetupContract(), + }; + + const service = new ShareSavedObjectsToSpaceService(); + service.setup(deps); + + expect(deps.savedObjectsManagementSetup.actions.register).toHaveBeenCalledTimes(1); + expect(deps.savedObjectsManagementSetup.actions.register).toHaveBeenCalledWith( + expect.any(ShareToSpaceSavedObjectsManagementAction) + ); + + // expect(deps.savedObjectsManagementSetup.columns.register).toHaveBeenCalledTimes(1); + // expect(deps.savedObjectsManagementSetup.columns.register).toHaveBeenCalledWith( + // expect.any(ShareToSpaceSavedObjectsManagementColumn) + // ); + expect(deps.savedObjectsManagementSetup.columns.register).not.toHaveBeenCalled(); // ensure this test fails after column code is uncommented + }); + }); +}); diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_service.ts b/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_service.ts new file mode 100644 index 00000000000000..9f6e57c355380b --- /dev/null +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_service.ts @@ -0,0 +1,27 @@ +/* + * 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 { NotificationsSetup } from 'src/core/public'; +import { SavedObjectsManagementPluginSetup } from 'src/plugins/saved_objects_management/public'; +import { ShareToSpaceSavedObjectsManagementAction } from './share_saved_objects_to_space_action'; +// import { ShareToSpaceSavedObjectsManagementColumn } from './share_saved_objects_to_space_column'; +import { SpacesManager } from '../spaces_manager'; + +interface SetupDeps { + spacesManager: SpacesManager; + savedObjectsManagementSetup: SavedObjectsManagementPluginSetup; + notificationsSetup: NotificationsSetup; +} + +export class ShareSavedObjectsToSpaceService { + public setup({ spacesManager, savedObjectsManagementSetup, notificationsSetup }: SetupDeps) { + const action = new ShareToSpaceSavedObjectsManagementAction(spacesManager, notificationsSetup); + savedObjectsManagementSetup.actions.register(action); + // Note: this column is hidden for now because no saved objects are shareable. It should be uncommented when at least one saved object type is multi-namespace. + // const column = new ShareToSpaceSavedObjectsManagementColumn(spacesManager); + // savedObjectsManagementSetup.columns.register(column); + } +} diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/types.ts b/x-pack/plugins/spaces/public/share_saved_objects_to_space/types.ts new file mode 100644 index 00000000000000..fe41f4a5fadc8b --- /dev/null +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/types.ts @@ -0,0 +1,22 @@ +/* + * 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 { SavedObjectsImportRetry, SavedObjectsImportResponse } from 'src/core/public'; +import { Space } from '..'; + +export interface ShareOptions { + selectedSpaceIds: string[]; +} + +export type ImportRetry = Omit; + +export interface ShareSavedObjectsToSpaceResponse { + [spaceId: string]: SavedObjectsImportResponse; +} + +export interface SpaceTarget extends Space { + isActiveSpace: boolean; +} diff --git a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts index 6186ac7fd93be6..f666c823bd3654 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts @@ -18,6 +18,8 @@ function createSpacesManagerMock() { updateSpace: jest.fn().mockResolvedValue(undefined), deleteSpace: jest.fn().mockResolvedValue(undefined), copySavedObjects: jest.fn().mockResolvedValue(undefined), + shareSavedObjectAdd: jest.fn().mockResolvedValue(undefined), + shareSavedObjectRemove: jest.fn().mockResolvedValue(undefined), resolveCopySavedObjectsErrors: jest.fn().mockResolvedValue(undefined), redirectToSpaceSelector: jest.fn().mockResolvedValue(undefined), } as unknown) as jest.Mocked; diff --git a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts index ac5cb56084cfce..2daf9ab420efc5 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts @@ -11,6 +11,8 @@ import { Space } from '../../common/model/space'; import { GetSpacePurpose } from '../../common/model/types'; import { CopySavedObjectsToSpaceResponse } from '../copy_saved_objects_to_space/types'; +type SavedObject = Pick; + export class SpacesManager { private activeSpace$: BehaviorSubject = new BehaviorSubject(null); @@ -72,9 +74,10 @@ export class SpacesManager { } public async copySavedObjects( - objects: Array>, + objects: SavedObject[], spaces: string[], includeReferences: boolean, + createNewCopies: boolean, overwrite: boolean ): Promise { return this.http.post('/api/spaces/_copy_saved_objects', { @@ -82,25 +85,39 @@ export class SpacesManager { objects, spaces, includeReferences, - overwrite, + ...(createNewCopies ? { createNewCopies } : { overwrite }), }), }); } public async resolveCopySavedObjectsErrors( - objects: Array>, + objects: SavedObject[], retries: unknown, - includeReferences: boolean + includeReferences: boolean, + createNewCopies: boolean ): Promise { return this.http.post(`/api/spaces/_resolve_copy_saved_objects_errors`, { body: JSON.stringify({ objects, includeReferences, + createNewCopies, retries, }), }); } + public async shareSavedObjectAdd(object: SavedObject, spaces: string[]): Promise { + return this.http.post(`/api/spaces/_share_saved_object_add`, { + body: JSON.stringify({ object, spaces }), + }); + } + + public async shareSavedObjectRemove(object: SavedObject, spaces: string[]): Promise { + return this.http.post(`/api/spaces/_share_saved_object_remove`, { + body: JSON.stringify({ object, spaces }), + }); + } + public redirectToSpaceSelector() { window.location.href = `${this.serverBasePath}/spaces/space_selector`; } diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index 9679dd8c52523e..d49dfa2015dc6d 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -3,14 +3,20 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Readable } from 'stream'; import { SavedObjectsImportResponse, SavedObjectsImportOptions, SavedObjectsExportOptions, + SavedObjectsImportSuccess, } from 'src/core/server'; +import { + coreMock, + httpServerMock, + savedObjectsTypeRegistryMock, + savedObjectsClientMock, +} from 'src/core/server/mocks'; import { copySavedObjectsToSpacesFactory } from './copy_to_spaces'; -import { Readable } from 'stream'; -import { coreMock, savedObjectsTypeRegistryMock, httpServerMock } from 'src/core/server/mocks'; jest.mock('../../../../../../src/core/server', () => { return { @@ -31,6 +37,8 @@ interface SetupOpts { ) => Promise; } +const EXPORT_LIMIT = 1000; + const expectStreamToContainObjects = async ( stream: Readable, expectedObjects: SetupOpts['objects'] @@ -50,23 +58,29 @@ const expectStreamToContainObjects = async ( }; describe('copySavedObjectsToSpaces', () => { + const mockExportResults = [ + { type: 'dashboard', id: 'my-dashboard', attributes: {} }, + { type: 'visualization', id: 'my-viz', attributes: {} }, + { type: 'index-pattern', id: 'my-index-pattern', attributes: {} }, + { type: 'globaltype', id: 'my-globaltype', attributes: {} }, + ]; + const setup = (setupOpts: SetupOpts) => { const coreStart = coreMock.createStart(); + const savedObjectsClient = savedObjectsClientMock.create(); const typeRegistry = savedObjectsTypeRegistryMock.create(); - typeRegistry.getAllTypes.mockReturnValue([ + coreStart.savedObjects.getScopedClient.mockReturnValue(savedObjectsClient); + coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); + + typeRegistry.getImportableAndExportableTypes.mockReturnValue([ + // don't need to include all types, just need a positive case (agnostic) and a negative case (non-agnostic) { name: 'dashboard', namespaceType: 'single', hidden: false, mappings: { properties: {} }, }, - { - name: 'visualization', - namespaceType: 'single', - hidden: false, - mappings: { properties: {} }, - }, { name: 'globaltype', namespaceType: 'agnostic', @@ -74,13 +88,12 @@ describe('copySavedObjectsToSpaces', () => { mappings: { properties: {} }, }, ]); - typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) => - typeRegistry.getAllTypes().some((t) => t.name === type && t.namespaceType === 'agnostic') + typeRegistry + .getImportableAndExportableTypes() + .some((t) => t.name === type && t.namespaceType === 'agnostic') ); - coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); - (exportSavedObjectsToStream as jest.Mock).mockImplementation( async (opts: SavedObjectsExportOptions) => { return ( @@ -100,10 +113,15 @@ describe('copySavedObjectsToSpaces', () => { (importSavedObjectsFromStream as jest.Mock).mockImplementation( async (opts: SavedObjectsImportOptions) => { const defaultImpl = async () => { - await expectStreamToContainObjects(opts.readStream, setupOpts.objects); + // namespace-agnostic types should be filtered out before import + const filteredObjects = setupOpts.objects.filter(({ type }) => type !== 'globaltype'); + await expectStreamToContainObjects(opts.readStream, filteredObjects); const response: SavedObjectsImportResponse = { success: true, - successCount: setupOpts.objects.length, + successCount: filteredObjects.length, + successResults: [ + ('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess, + ], }; return Promise.resolve(response); @@ -115,261 +133,95 @@ describe('copySavedObjectsToSpaces', () => { return { savedObjects: coreStart.savedObjects, + savedObjectsClient, + typeRegistry, }; }; it('uses the Saved Objects Service to perform an export followed by a series of imports', async () => { - const { savedObjects } = setup({ - objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - attributes: {}, - }, - { - type: 'visualization', - id: 'my-viz', - attributes: {}, - }, - { - type: 'index-pattern', - id: 'my-index-pattern', - attributes: {}, - }, - ], + const { savedObjects, savedObjectsClient, typeRegistry } = setup({ + objects: mockExportResults, }); const request = httpServerMock.createKibanaRequest(); const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory( savedObjects, - () => 1000, + () => EXPORT_LIMIT, request ); - const result = await copySavedObjectsToSpaces('sourceSpace', ['destination1', 'destination2'], { + const namespace = 'sourceSpace'; + const objects = [{ type: 'dashboard', id: 'my-dashboard' }]; + const result = await copySavedObjectsToSpaces(namespace, ['destination1', 'destination2'], { includeReferences: true, overwrite: true, - objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - }, - ], + objects, + createNewCopies: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "destination1": Object { - "errors": undefined, - "success": true, - "successCount": 3, - }, - "destination2": Object { - "errors": undefined, - "success": true, - "successCount": 3, - }, - } - `); - - expect((exportSavedObjectsToStream as jest.Mock).mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "excludeExportDetails": true, - "exportSizeLimit": 1000, - "includeReferencesDeep": true, - "namespace": "sourceSpace", - "objects": Array [ - Object { - "id": "my-dashboard", - "type": "dashboard", - }, - ], - "savedObjectsClient": Object { - "addToNamespaces": [MockFunction], - "bulkCreate": [MockFunction], - "bulkGet": [MockFunction], - "bulkUpdate": [MockFunction], - "create": [MockFunction], - "delete": [MockFunction], - "deleteFromNamespaces": [MockFunction], - "errors": [Function], - "find": [MockFunction], - "get": [MockFunction], - "update": [MockFunction], - }, - }, - ], - ] + Object { + "destination1": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + "destination2": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + } `); - expect((importSavedObjectsFromStream as jest.Mock).mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "namespace": "destination1", - "objectLimit": 1000, - "overwrite": true, - "readStream": Readable { - "_events": Object { - "data": [Function], - "end": [Function], - "error": [Function], - }, - "_eventsCount": 3, - "_maxListeners": undefined, - "_read": [Function], - "_readableState": ReadableState { - "autoDestroy": false, - "awaitDrain": 0, - "buffer": BufferList { - "head": null, - "length": 0, - "tail": null, - }, - "decoder": null, - "defaultEncoding": "utf8", - "destroyed": false, - "emitClose": true, - "emittedReadable": false, - "encoding": null, - "endEmitted": true, - "ended": true, - "flowing": true, - "highWaterMark": 16, - "length": 0, - "needReadable": false, - "objectMode": true, - "paused": false, - "pipes": null, - "pipesCount": 0, - "readableListening": false, - "reading": false, - "readingMore": false, - "resumeScheduled": false, - "sync": false, - }, - "readable": false, - }, - "savedObjectsClient": Object { - "addToNamespaces": [MockFunction], - "bulkCreate": [MockFunction], - "bulkGet": [MockFunction], - "bulkUpdate": [MockFunction], - "create": [MockFunction], - "delete": [MockFunction], - "deleteFromNamespaces": [MockFunction], - "errors": [Function], - "find": [MockFunction], - "get": [MockFunction], - "update": [MockFunction], - }, - "supportedTypes": Array [ - "dashboard", - "visualization", - ], - }, - ], - Array [ - Object { - "namespace": "destination2", - "objectLimit": 1000, - "overwrite": true, - "readStream": Readable { - "_events": Object { - "data": [Function], - "end": [Function], - "error": [Function], - }, - "_eventsCount": 3, - "_maxListeners": undefined, - "_read": [Function], - "_readableState": ReadableState { - "autoDestroy": false, - "awaitDrain": 0, - "buffer": BufferList { - "head": null, - "length": 0, - "tail": null, - }, - "decoder": null, - "defaultEncoding": "utf8", - "destroyed": false, - "emitClose": true, - "emittedReadable": false, - "encoding": null, - "endEmitted": true, - "ended": true, - "flowing": true, - "highWaterMark": 16, - "length": 0, - "needReadable": false, - "objectMode": true, - "paused": false, - "pipes": null, - "pipesCount": 0, - "readableListening": false, - "reading": false, - "readingMore": false, - "resumeScheduled": false, - "sync": false, - }, - "readable": false, - }, - "savedObjectsClient": Object { - "addToNamespaces": [MockFunction], - "bulkCreate": [MockFunction], - "bulkGet": [MockFunction], - "bulkUpdate": [MockFunction], - "create": [MockFunction], - "delete": [MockFunction], - "deleteFromNamespaces": [MockFunction], - "errors": [Function], - "find": [MockFunction], - "get": [MockFunction], - "update": [MockFunction], - }, - "supportedTypes": Array [ - "dashboard", - "visualization", - ], - }, - ], - ] - `); + expect(exportSavedObjectsToStream).toHaveBeenCalledWith({ + excludeExportDetails: true, + exportSizeLimit: EXPORT_LIMIT, + includeReferencesDeep: true, + namespace, + objects, + savedObjectsClient, + }); + + const importOptions = { + createNewCopies: false, + objectLimit: EXPORT_LIMIT, + overwrite: true, + readStream: expect.any(Readable), + savedObjectsClient, + typeRegistry, + }; + expect(importSavedObjectsFromStream).toHaveBeenNthCalledWith(1, { + ...importOptions, + namespace: 'destination1', + }); + expect(importSavedObjectsFromStream).toHaveBeenNthCalledWith(2, { + ...importOptions, + namespace: 'destination2', + }); }); it(`doesn't stop copy if some spaces fail`, async () => { - const objects = [ - { - type: 'dashboard', - id: 'my-dashboard', - attributes: {}, - }, - { - type: 'visualization', - id: 'my-viz', - attributes: {}, - }, - { - type: 'index-pattern', - id: 'my-index-pattern', - attributes: {}, - }, - ]; - const { savedObjects } = setup({ - objects, + objects: mockExportResults, importSavedObjectsFromStreamImpl: async (opts) => { if (opts.namespace === 'failure-space') { throw new Error(`Some error occurred!`); } - await expectStreamToContainObjects(opts.readStream, objects); + // namespace-agnostic types should be filtered out before import + const filteredObjects = mockExportResults.filter(({ type }) => type !== 'globaltype'); + await expectStreamToContainObjects(opts.readStream, filteredObjects); return Promise.resolve({ success: true, - successCount: 3, + successCount: filteredObjects.length, + successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess], }); }, }); @@ -378,7 +230,7 @@ describe('copySavedObjectsToSpaces', () => { const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory( savedObjects, - () => 1000, + () => EXPORT_LIMIT, request ); @@ -388,58 +240,44 @@ describe('copySavedObjectsToSpaces', () => { { includeReferences: true, overwrite: true, - objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - }, - ], + objects: [{ type: 'dashboard', id: 'my-dashboard' }], + createNewCopies: false, } ); expect(result).toMatchInlineSnapshot(` - Object { - "failure-space": Object { - "errors": Array [ - [Error: Some error occurred!], - ], - "success": false, - "successCount": 0, - }, - "marketing": Object { - "errors": undefined, - "success": true, - "successCount": 3, - }, - "non-existent-space": Object { - "errors": undefined, - "success": true, - "successCount": 3, - }, - } - `); + Object { + "failure-space": Object { + "errors": Array [ + [Error: Some error occurred!], + ], + "success": false, + "successCount": 0, + }, + "marketing": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + "non-existent-space": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + } + `); }); it(`handles stream read errors`, async () => { const { savedObjects } = setup({ - objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - attributes: {}, - }, - { - type: 'visualization', - id: 'my-viz', - attributes: {}, - }, - { - type: 'index-pattern', - id: 'my-index-pattern', - attributes: {}, - }, - ], - exportSavedObjectsToStreamImpl: (opts) => { + objects: mockExportResults, + exportSavedObjectsToStreamImpl: (_opts) => { return Promise.resolve( new Readable({ objectMode: true, @@ -455,7 +293,7 @@ describe('copySavedObjectsToSpaces', () => { const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory( savedObjects, - () => 1000, + () => EXPORT_LIMIT, request ); @@ -466,12 +304,8 @@ describe('copySavedObjectsToSpaces', () => { { includeReferences: true, overwrite: true, - objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - }, - ], + objects: [{ type: 'dashboard', id: 'my-dashboard' }], + createNewCopies: false, } ) ).rejects.toThrowErrorMatchingInlineSnapshot( diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts index dca6f2a6206ab9..5575052d7bbb80 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts @@ -12,11 +12,11 @@ import { } from '../../../../../../src/core/server'; import { spaceIdToNamespace } from '../utils/namespace'; import { CopyOptions, CopyResponse } from './types'; -import { getEligibleTypes } from './lib/get_eligible_types'; import { createReadableStreamFromArray } from './lib/readable_stream_from_array'; import { createEmptyFailureResponse } from './lib/create_empty_failure_response'; import { readStreamToCompletion } from './lib/read_stream_to_completion'; import { COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS } from './lib/saved_objects_client_opts'; +import { getIneligibleTypes } from './lib/get_ineligible_types'; export function copySavedObjectsToSpacesFactory( savedObjects: CoreStart['savedObjects'], @@ -27,8 +27,6 @@ export function copySavedObjectsToSpacesFactory( const savedObjectsClient = getScopedClient(request, COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS); - const eligibleTypes = getEligibleTypes(getTypeRegistry()); - const exportRequestedObjects = async ( sourceSpaceId: string, options: Pick @@ -56,13 +54,15 @@ export function copySavedObjectsToSpacesFactory( objectLimit: getImportExportObjectLimit(), overwrite: options.overwrite, savedObjectsClient, - supportedTypes: eligibleTypes, + typeRegistry: getTypeRegistry(), readStream: objectsStream, + createNewCopies: options.createNewCopies, }); return { success: importResponse.success, successCount: importResponse.successCount, + successResults: importResponse.successResults, errors: importResponse.errors, }; } catch (error) { @@ -78,11 +78,15 @@ export function copySavedObjectsToSpacesFactory( const response: CopyResponse = {}; const exportedSavedObjects = await exportRequestedObjects(sourceSpaceId, options); + const ineligibleTypes = getIneligibleTypes(getTypeRegistry()); + const filteredObjects = exportedSavedObjects.filter( + ({ type }) => !ineligibleTypes.includes(type) + ); for (const spaceId of destinationSpaceIds) { response[spaceId] = await importObjectsToSpace( spaceId, - createReadableStreamFromArray(exportedSavedObjects), + createReadableStreamFromArray(filteredObjects), options ); } diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/get_eligible_types.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/get_eligible_types.ts deleted file mode 100644 index e5f2c5b18bd00f..00000000000000 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/get_eligible_types.ts +++ /dev/null @@ -1,16 +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 { SavedObjectTypeRegistry } from 'src/core/server'; - -export function getEligibleTypes( - typeRegistry: Pick -) { - return typeRegistry - .getAllTypes() - .filter((type) => !typeRegistry.isNamespaceAgnostic(type.name)) - .map((type) => type.name); -} diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/get_ineligible_types.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/get_ineligible_types.ts new file mode 100644 index 00000000000000..91d4cb13b98ebb --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/get_ineligible_types.ts @@ -0,0 +1,24 @@ +/* + * 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 { SavedObjectTypeRegistry } from 'src/core/server'; + +/** + * This function returns any importable/exportable saved object types that are namespace-agnostic. Even if these are eligible for + * import/export, we should not include them in the copy operation because it will result in a conflict that needs to overwrite itself to be + * resolved. + */ +export function getIneligibleTypes( + typeRegistry: Pick< + SavedObjectTypeRegistry, + 'getImportableAndExportableTypes' | 'isNamespaceAgnostic' + > +) { + return typeRegistry + .getImportableAndExportableTypes() + .filter((type) => typeRegistry.isNamespaceAgnostic(type.name)) + .map((type) => type.name); +} diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index 7bb4c61ed51a0d..6a77bf7397cb58 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -3,13 +3,19 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Readable } from 'stream'; import { SavedObjectsImportResponse, SavedObjectsResolveImportErrorsOptions, SavedObjectsExportOptions, + SavedObjectsImportSuccess, } from 'src/core/server'; -import { coreMock, savedObjectsTypeRegistryMock, httpServerMock } from 'src/core/server/mocks'; -import { Readable } from 'stream'; +import { + coreMock, + httpServerMock, + savedObjectsTypeRegistryMock, + savedObjectsClientMock, +} from 'src/core/server/mocks'; import { resolveCopySavedObjectsToSpacesConflictsFactory } from './resolve_copy_conflicts'; jest.mock('../../../../../../src/core/server', () => { @@ -31,6 +37,8 @@ interface SetupOpts { ) => Promise; } +const EXPORT_LIMIT = 1000; + const expectStreamToContainObjects = async ( stream: Readable, expectedObjects: SetupOpts['objects'] @@ -50,23 +58,28 @@ const expectStreamToContainObjects = async ( }; describe('resolveCopySavedObjectsToSpacesConflicts', () => { + const mockExportResults = [ + { type: 'dashboard', id: 'my-dashboard', attributes: {} }, + { type: 'visualization', id: 'my-viz', attributes: {} }, + { type: 'index-pattern', id: 'my-index-pattern', attributes: {} }, + ]; + const setup = (setupOpts: SetupOpts) => { const coreStart = coreMock.createStart(); + const savedObjectsClient = savedObjectsClientMock.create(); const typeRegistry = savedObjectsTypeRegistryMock.create(); - typeRegistry.getAllTypes.mockReturnValue([ + coreStart.savedObjects.getScopedClient.mockReturnValue(savedObjectsClient); + coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); + + typeRegistry.getImportableAndExportableTypes.mockReturnValue([ + // don't need to include all types, just need a positive case (agnostic) and a negative case (non-agnostic) { name: 'dashboard', namespaceType: 'single', hidden: false, mappings: { properties: {} }, }, - { - name: 'visualization', - namespaceType: 'single', - hidden: false, - mappings: { properties: {} }, - }, { name: 'globaltype', namespaceType: 'agnostic', @@ -74,13 +87,12 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { mappings: { properties: {} }, }, ]); - typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) => - typeRegistry.getAllTypes().some((t) => t.name === type && t.namespaceType === 'agnostic') + typeRegistry + .getImportableAndExportableTypes() + .some((t) => t.name === type && t.namespaceType === 'agnostic') ); - coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); - (exportSavedObjectsToStream as jest.Mock).mockImplementation( async (opts: SavedObjectsExportOptions) => { return ( @@ -100,11 +112,16 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { (resolveSavedObjectsImportErrors as jest.Mock).mockImplementation( async (opts: SavedObjectsResolveImportErrorsOptions) => { const defaultImpl = async () => { - await expectStreamToContainObjects(opts.readStream, setupOpts.objects); + // namespace-agnostic types should be filtered out before import + const filteredObjects = setupOpts.objects.filter(({ type }) => type !== 'globaltype'); + await expectStreamToContainObjects(opts.readStream, filteredObjects); const response: SavedObjectsImportResponse = { success: true, - successCount: setupOpts.objects.length, + successCount: filteredObjects.length, + successResults: [ + ('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess, + ], }; return response; @@ -116,290 +133,100 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { return { savedObjects: coreStart.savedObjects, + savedObjectsClient, + typeRegistry, }; }; it('uses the Saved Objects Service to perform an export followed by a series of conflict resolution calls', async () => { - const { savedObjects } = setup({ - objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - attributes: {}, - }, - { - type: 'visualization', - id: 'my-viz', - attributes: {}, - }, - { - type: 'index-pattern', - id: 'my-index-pattern', - attributes: {}, - }, - ], + const { savedObjects, savedObjectsClient, typeRegistry } = setup({ + objects: mockExportResults, }); const request = httpServerMock.createKibanaRequest(); const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory( savedObjects, - () => 1000, + () => EXPORT_LIMIT, request ); - const result = await resolveCopySavedObjectsToSpacesConflicts('sourceSpace', { + const namespace = 'sourceSpace'; + const objects = [{ type: 'dashboard', id: 'my-dashboard' }]; + const retries = { + destination1: [{ type: 'visualization', id: 'my-visualization', overwrite: true }], + destination2: [{ type: 'visualization', id: 'my-visualization', overwrite: false }], + }; + const result = await resolveCopySavedObjectsToSpacesConflicts(namespace, { includeReferences: true, - objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - }, - ], - retries: { - destination1: [ - { - type: 'visualization', - id: 'my-visualization', - overwrite: true, - }, - ], - destination2: [ - { - type: 'visualization', - id: 'my-visualization', - overwrite: false, - }, - ], - }, + objects, + retries, + createNewCopies: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "destination1": Object { - "errors": undefined, - "success": true, - "successCount": 3, - }, - "destination2": Object { - "errors": undefined, - "success": true, - "successCount": 3, - }, - } - `); - - expect((exportSavedObjectsToStream as jest.Mock).mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "excludeExportDetails": true, - "exportSizeLimit": 1000, - "includeReferencesDeep": true, - "namespace": "sourceSpace", - "objects": Array [ - Object { - "id": "my-dashboard", - "type": "dashboard", - }, - ], - "savedObjectsClient": Object { - "addToNamespaces": [MockFunction], - "bulkCreate": [MockFunction], - "bulkGet": [MockFunction], - "bulkUpdate": [MockFunction], - "create": [MockFunction], - "delete": [MockFunction], - "deleteFromNamespaces": [MockFunction], - "errors": [Function], - "find": [MockFunction], - "get": [MockFunction], - "update": [MockFunction], - }, - }, - ], - ] + Object { + "destination1": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + "destination2": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + } `); - expect((resolveSavedObjectsImportErrors as jest.Mock).mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "namespace": "destination1", - "objectLimit": 1000, - "readStream": Readable { - "_events": Object { - "data": [Function], - "end": [Function], - "error": [Function], - }, - "_eventsCount": 3, - "_maxListeners": undefined, - "_read": [Function], - "_readableState": ReadableState { - "autoDestroy": false, - "awaitDrain": 0, - "buffer": BufferList { - "head": null, - "length": 0, - "tail": null, - }, - "decoder": null, - "defaultEncoding": "utf8", - "destroyed": false, - "emitClose": true, - "emittedReadable": false, - "encoding": null, - "endEmitted": true, - "ended": true, - "flowing": true, - "highWaterMark": 16, - "length": 0, - "needReadable": false, - "objectMode": true, - "paused": false, - "pipes": null, - "pipesCount": 0, - "readableListening": false, - "reading": false, - "readingMore": false, - "resumeScheduled": false, - "sync": false, - }, - "readable": false, - }, - "retries": Array [ - Object { - "id": "my-visualization", - "overwrite": true, - "replaceReferences": Array [], - "type": "visualization", - }, - ], - "savedObjectsClient": Object { - "addToNamespaces": [MockFunction], - "bulkCreate": [MockFunction], - "bulkGet": [MockFunction], - "bulkUpdate": [MockFunction], - "create": [MockFunction], - "delete": [MockFunction], - "deleteFromNamespaces": [MockFunction], - "errors": [Function], - "find": [MockFunction], - "get": [MockFunction], - "update": [MockFunction], - }, - "supportedTypes": Array [ - "dashboard", - "visualization", - ], - }, - ], - Array [ - Object { - "namespace": "destination2", - "objectLimit": 1000, - "readStream": Readable { - "_events": Object { - "data": [Function], - "end": [Function], - "error": [Function], - }, - "_eventsCount": 3, - "_maxListeners": undefined, - "_read": [Function], - "_readableState": ReadableState { - "autoDestroy": false, - "awaitDrain": 0, - "buffer": BufferList { - "head": null, - "length": 0, - "tail": null, - }, - "decoder": null, - "defaultEncoding": "utf8", - "destroyed": false, - "emitClose": true, - "emittedReadable": false, - "encoding": null, - "endEmitted": true, - "ended": true, - "flowing": true, - "highWaterMark": 16, - "length": 0, - "needReadable": false, - "objectMode": true, - "paused": false, - "pipes": null, - "pipesCount": 0, - "readableListening": false, - "reading": false, - "readingMore": false, - "resumeScheduled": false, - "sync": false, - }, - "readable": false, - }, - "retries": Array [ - Object { - "id": "my-visualization", - "overwrite": false, - "replaceReferences": Array [], - "type": "visualization", - }, - ], - "savedObjectsClient": Object { - "addToNamespaces": [MockFunction], - "bulkCreate": [MockFunction], - "bulkGet": [MockFunction], - "bulkUpdate": [MockFunction], - "create": [MockFunction], - "delete": [MockFunction], - "deleteFromNamespaces": [MockFunction], - "errors": [Function], - "find": [MockFunction], - "get": [MockFunction], - "update": [MockFunction], - }, - "supportedTypes": Array [ - "dashboard", - "visualization", - ], - }, - ], - ] - `); + expect(exportSavedObjectsToStream).toHaveBeenCalledWith({ + excludeExportDetails: true, + exportSizeLimit: EXPORT_LIMIT, + includeReferencesDeep: true, + namespace, + objects, + savedObjectsClient, + }); + + const importOptions = { + createNewCopies: false, + objectLimit: EXPORT_LIMIT, + readStream: expect.any(Readable), + savedObjectsClient, + typeRegistry, + }; + expect(resolveSavedObjectsImportErrors).toHaveBeenNthCalledWith(1, { + ...importOptions, + namespace: 'destination1', + retries: [{ ...retries.destination1[0], replaceReferences: [] }], + }); + expect(resolveSavedObjectsImportErrors).toHaveBeenNthCalledWith(2, { + ...importOptions, + namespace: 'destination2', + retries: [{ ...retries.destination2[0], replaceReferences: [] }], + }); }); it(`doesn't stop resolution if some spaces fail`, async () => { - const objects = [ - { - type: 'dashboard', - id: 'my-dashboard', - attributes: {}, - }, - { - type: 'visualization', - id: 'my-viz', - attributes: {}, - }, - { - type: 'index-pattern', - id: 'my-index-pattern', - attributes: {}, - }, - ]; - const { savedObjects } = setup({ - objects, + objects: mockExportResults, resolveSavedObjectsImportErrorsImpl: async (opts) => { if (opts.namespace === 'failure-space') { throw new Error(`Some error occurred!`); } - await expectStreamToContainObjects(opts.readStream, objects); + // namespace-agnostic types should be filtered out before import + const filteredObjects = mockExportResults.filter(({ type }) => type !== 'globaltype'); + await expectStreamToContainObjects(opts.readStream, filteredObjects); return Promise.resolve({ success: true, - successCount: 3, + successCount: filteredObjects.length, + successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess], }); }, }); @@ -408,64 +235,50 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory( savedObjects, - () => 1000, + () => EXPORT_LIMIT, request ); const result = await resolveCopySavedObjectsToSpacesConflicts('sourceSpace', { includeReferences: true, - objects: [ - { - type: 'dashboard', - id: 'my-dashboard', - }, - ], + objects: [{ type: 'dashboard', id: 'my-dashboard' }], retries: { - ['failure-space']: [ - { - type: 'visualization', - id: 'my-visualization', - overwrite: true, - }, - ], + ['failure-space']: [{ type: 'visualization', id: 'my-visualization', overwrite: true }], ['non-existent-space']: [ - { - type: 'visualization', - id: 'my-visualization', - overwrite: false, - }, - ], - ['marketing']: [ - { - type: 'visualization', - id: 'my-visualization', - overwrite: true, - }, + { type: 'visualization', id: 'my-visualization', overwrite: false }, ], + marketing: [{ type: 'visualization', id: 'my-visualization', overwrite: true }], }, + createNewCopies: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "failure-space": Object { - "errors": Array [ - [Error: Some error occurred!], - ], - "success": false, - "successCount": 0, - }, - "marketing": Object { - "errors": undefined, - "success": true, - "successCount": 3, - }, - "non-existent-space": Object { - "errors": undefined, - "success": true, - "successCount": 3, - }, - } - `); + Object { + "failure-space": Object { + "errors": Array [ + [Error: Some error occurred!], + ], + "success": false, + "successCount": 0, + }, + "marketing": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + "non-existent-space": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + } + `); }); it(`handles stream read errors`, async () => { @@ -487,7 +300,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory( savedObjects, - () => 1000, + () => EXPORT_LIMIT, request ); @@ -496,6 +309,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { includeReferences: true, objects: [], retries: {}, + createNewCopies: false, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Something went wrong while reading this stream"` diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts index a355d19b305a34..d433712bb9412f 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts @@ -5,18 +5,18 @@ */ import { Readable } from 'stream'; -import { SavedObject, CoreStart, KibanaRequest } from 'src/core/server'; +import { SavedObject, CoreStart, KibanaRequest, SavedObjectsImportRetry } from 'src/core/server'; import { exportSavedObjectsToStream, resolveSavedObjectsImportErrors, } from '../../../../../../src/core/server'; import { spaceIdToNamespace } from '../utils/namespace'; import { CopyOptions, ResolveConflictsOptions, CopyResponse } from './types'; -import { getEligibleTypes } from './lib/get_eligible_types'; import { createEmptyFailureResponse } from './lib/create_empty_failure_response'; import { readStreamToCompletion } from './lib/read_stream_to_completion'; import { createReadableStreamFromArray } from './lib/readable_stream_from_array'; import { COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS } from './lib/saved_objects_client_opts'; +import { getIneligibleTypes } from './lib/get_ineligible_types'; export function resolveCopySavedObjectsToSpacesConflictsFactory( savedObjects: CoreStart['savedObjects'], @@ -27,8 +27,6 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( const savedObjectsClient = getScopedClient(request, COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS); - const eligibleTypes = getEligibleTypes(getTypeRegistry()); - const exportRequestedObjects = async ( sourceSpaceId: string, options: Pick @@ -47,26 +45,24 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( const resolveConflictsForSpace = async ( spaceId: string, objectsStream: Readable, - retries: Array<{ - type: string; - id: string; - overwrite: boolean; - replaceReferences: Array<{ type: string; from: string; to: string }>; - }> + retries: SavedObjectsImportRetry[], + createNewCopies: boolean ) => { try { const importResponse = await resolveSavedObjectsImportErrors({ namespace: spaceIdToNamespace(spaceId), objectLimit: getImportExportObjectLimit(), savedObjectsClient, - supportedTypes: eligibleTypes, + typeRegistry: getTypeRegistry(), readStream: objectsStream, retries, + createNewCopies, }); return { success: importResponse.success, successCount: importResponse.successCount, + successResults: importResponse.successResults, errors: importResponse.errors, }; } catch (error) { @@ -84,6 +80,10 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( includeReferences: options.includeReferences, objects: options.objects, }); + const ineligibleTypes = getIneligibleTypes(getTypeRegistry()); + const filteredObjects = exportedSavedObjects.filter( + ({ type }) => !ineligibleTypes.includes(type) + ); for (const entry of Object.entries(options.retries)) { const [spaceId, entryRetries] = entry; @@ -92,8 +92,9 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( response[spaceId] = await resolveConflictsForSpace( spaceId, - createReadableStreamFromArray(exportedSavedObjects), - retries + createReadableStreamFromArray(filteredObjects), + retries, + options.createNewCopies ); } diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts index 1bbe5aa6625b0a..8d4169f9727954 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts @@ -5,26 +5,33 @@ */ import { Payload } from 'boom'; -import { SavedObjectsImportError } from 'src/core/server'; +import { + SavedObjectsImportSuccess, + SavedObjectsImportError, + SavedObjectsImportRetry, +} from 'src/core/server'; export interface CopyOptions { objects: Array<{ type: string; id: string }>; overwrite: boolean; includeReferences: boolean; + createNewCopies: boolean; } export interface ResolveConflictsOptions { objects: Array<{ type: string; id: string }>; includeReferences: boolean; retries: { - [spaceId: string]: Array<{ type: string; id: string; overwrite: boolean }>; + [spaceId: string]: Array>; }; + createNewCopies: boolean; } export interface CopyResponse { [spaceId: string]: { success: boolean; successCount: number; + successResults?: SavedObjectsImportSuccess[]; errors?: Array; }; } diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/__snapshots__/spaces_client.test.ts.snap b/x-pack/plugins/spaces/server/lib/spaces_client/__snapshots__/spaces_client.test.ts.snap index c2df94a0a2936e..9544d7e8bb4817 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/__snapshots__/spaces_client.test.ts.snap +++ b/x-pack/plugins/spaces/server/lib/spaces_client/__snapshots__/spaces_client.test.ts.snap @@ -28,6 +28,8 @@ exports[`#getAll useRbacForRequest is true with purpose='copySavedObjectsIntoSpa exports[`#getAll useRbacForRequest is true with purpose='findSavedObjects' throws Boom.forbidden when user isn't authorized for any spaces 1`] = `"Forbidden"`; +exports[`#getAll useRbacForRequest is true with purpose='shareSavedObjectsIntoSpace' throws Boom.forbidden when user isn't authorized for any spaces 1`] = `"Forbidden"`; + exports[`#getAll useRbacForRequest is true with purpose='undefined' throws Boom.forbidden when user isn't authorized for any spaces 1`] = `"Forbidden"`; exports[`#update useRbacForRequest is true throws Boom.forbidden when user isn't authorized at space 1`] = `"Unauthorized to update spaces"`; diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts index 61b1985c5a0b9f..90ce2b01bfd203 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts @@ -242,6 +242,11 @@ describe('#getAll', () => { expectedPrivilege: (mockAuthorization: SecurityPluginSetup['authz']) => mockAuthorization.actions.savedObject.get('config', 'find'), }, + { + purpose: 'shareSavedObjectsIntoSpace' as GetSpacePurpose, + expectedPrivilege: (mockAuthorization: SecurityPluginSetup['authz']) => + mockAuthorization.actions.ui.get('savedObjectsManagement', 'shareIntoSpace'), + }, ].forEach((scenario) => { describe(`with purpose='${scenario.purpose}'`, () => { test(`throws Boom.forbidden when user isn't authorized for any spaces`, async () => { diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts index dd2e0d40f31ed7..b1d6e3200ab3a4 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts @@ -17,6 +17,7 @@ const SUPPORTED_GET_SPACE_PURPOSES: GetSpacePurpose[] = [ 'any', 'copySavedObjectsIntoSpace', 'findSavedObjects', + 'shareSavedObjectsIntoSpace', ]; const PURPOSE_PRIVILEGE_MAP: Record< @@ -30,6 +31,9 @@ const PURPOSE_PRIVILEGE_MAP: Record< findSavedObjects: (authorization) => { return [authorization.actions.savedObject.get('config', 'find')]; }, + shareSavedObjectsIntoSpace: (authorization) => [ + authorization.actions.ui.get('savedObjectsManagement', 'shareIntoSpace'), + ], }; export class SpacesClient { diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts index 034d212a33035d..ce93591f492f16 100644 --- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts @@ -43,41 +43,6 @@ export const createMockSavedObjectsService = (spaces: any[] = []) => { const { savedObjects } = coreMock.createStart(); const typeRegistry = savedObjectsTypeRegistryMock.create(); - typeRegistry.getAllTypes.mockReturnValue([ - { - name: 'visualization', - namespaceType: 'single', - hidden: false, - mappings: { properties: {} }, - }, - { - name: 'dashboard', - namespaceType: 'single', - hidden: false, - mappings: { properties: {} }, - }, - { - name: 'index-pattern', - namespaceType: 'single', - hidden: false, - mappings: { properties: {} }, - }, - { - name: 'globalType', - namespaceType: 'agnostic', - hidden: false, - mappings: { properties: {} }, - }, - { - name: 'space', - namespaceType: 'agnostic', - hidden: true, - mappings: { properties: {} }, - }, - ]); - typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) => - typeRegistry.getAllTypes().some((t) => t.name === type && t.namespaceType === 'agnostic') - ); savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); savedObjects.getScopedClient.mockReturnValue(mockSavedObjectsClientContract); diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index b604554cbc59ac..bec3a5dcb0b71e 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -191,54 +191,35 @@ describe('copy to space', () => { ); }); - it(`requires objects to be unique`, async () => { + it(`does not allow "overwrite" to be used with "createNewCopies"`, async () => { const payload = { spaces: ['a-space'], - objects: [ - { type: 'foo', id: 'bar' }, - { type: 'foo', id: 'bar' }, - ], + objects: [{ type: 'foo', id: 'bar' }], + overwrite: true, + createNewCopies: true, }; const { copyToSpace } = await setup(); expect(() => (copyToSpace.routeValidation.body as ObjectType).validate(payload) - ).toThrowErrorMatchingInlineSnapshot(`"[objects]: duplicate objects are not allowed"`); + ).toThrowErrorMatchingInlineSnapshot(`"cannot use [overwrite] with [createNewCopies]"`); }); - it('does not allow namespace agnostic types to be copied (via "supportedTypes" property)', async () => { + it(`requires objects to be unique`, async () => { const payload = { spaces: ['a-space'], objects: [ - { type: 'globalType', id: 'bar' }, - { type: 'visualization', id: 'bar' }, + { type: 'foo', id: 'bar' }, + { type: 'foo', id: 'bar' }, ], }; const { copyToSpace } = await setup(); - const request = httpServerMock.createKibanaRequest({ - body: payload, - method: 'post', - }); - - const response = await copyToSpace.routeHandler( - mockRouteContext, - request, - kibanaResponseFactory - ); - - const { status } = response; - - expect(status).toEqual(200); - expect(importSavedObjectsFromStream).toHaveBeenCalledTimes(1); - const [importCallOptions] = (importSavedObjectsFromStream as jest.Mock).mock.calls[0]; - - expect(importCallOptions).toMatchObject({ - namespace: 'a-space', - supportedTypes: ['visualization', 'dashboard', 'index-pattern'], - }); + expect(() => + (copyToSpace.routeValidation.body as ObjectType).validate(payload) + ).toThrowErrorMatchingInlineSnapshot(`"[objects]: duplicate objects are not allowed"`); }); it('copies to multiple spaces', async () => { @@ -365,58 +346,6 @@ describe('copy to space', () => { ); }); - it('does not allow namespace agnostic types to be copied (via "supportedTypes" property)', async () => { - const payload = { - retries: { - ['a-space']: [ - { - type: 'visualization', - id: 'bar', - overwrite: true, - }, - { - type: 'globalType', - id: 'bar', - overwrite: true, - }, - ], - }, - objects: [ - { - type: 'globalType', - id: 'bar', - }, - { type: 'visualization', id: 'bar' }, - ], - }; - - const { resolveConflicts } = await setup(); - - const request = httpServerMock.createKibanaRequest({ - body: payload, - method: 'post', - }); - - const response = await resolveConflicts.routeHandler( - mockRouteContext, - request, - kibanaResponseFactory - ); - - const { status } = response; - - expect(status).toEqual(200); - expect(resolveSavedObjectsImportErrors).toHaveBeenCalledTimes(1); - const [ - resolveImportErrorsCallOptions, - ] = (resolveSavedObjectsImportErrors as jest.Mock).mock.calls[0]; - - expect(resolveImportErrorsCallOptions).toMatchObject({ - namespace: 'a-space', - supportedTypes: ['visualization', 'dashboard', 'index-pattern'], - }); - }); - it('resolves conflicts for multiple spaces', async () => { const payload = { objects: [{ type: 'visualization', id: 'bar' }], @@ -459,19 +388,13 @@ describe('copy to space', () => { resolveImportErrorsFirstCallOptions, ] = (resolveSavedObjectsImportErrors as jest.Mock).mock.calls[0]; - expect(resolveImportErrorsFirstCallOptions).toMatchObject({ - namespace: 'a-space', - supportedTypes: ['visualization', 'dashboard', 'index-pattern'], - }); + expect(resolveImportErrorsFirstCallOptions).toMatchObject({ namespace: 'a-space' }); const [ resolveImportErrorsSecondCallOptions, ] = (resolveSavedObjectsImportErrors as jest.Mock).mock.calls[1]; - expect(resolveImportErrorsSecondCallOptions).toMatchObject({ - namespace: 'b-space', - supportedTypes: ['visualization', 'dashboard', 'index-pattern'], - }); + expect(resolveImportErrorsSecondCallOptions).toMatchObject({ namespace: 'b-space' }); }); }); }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts index 87c2fee4ea9bf1..fef1646067fde3 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts @@ -30,39 +30,49 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { tags: ['access:copySavedObjectsToSpaces'], }, validate: { - body: schema.object({ - spaces: schema.arrayOf( - schema.string({ - validate: (value) => { - if (!SPACE_ID_REGEX.test(value)) { - return `lower case, a-z, 0-9, "_", and "-" are allowed`; - } - }, - }), - { - validate: (spaceIds) => { - if (_.uniq(spaceIds).length !== spaceIds.length) { - return 'duplicate space ids are not allowed'; - } - }, - } - ), - objects: schema.arrayOf( - schema.object({ - type: schema.string(), - id: schema.string(), - }), - { - validate: (objects) => { - if (!areObjectsUnique(objects)) { - return 'duplicate objects are not allowed'; - } - }, - } - ), - includeReferences: schema.boolean({ defaultValue: false }), - overwrite: schema.boolean({ defaultValue: false }), - }), + body: schema.object( + { + spaces: schema.arrayOf( + schema.string({ + validate: (value) => { + if (!SPACE_ID_REGEX.test(value)) { + return `lower case, a-z, 0-9, "_", and "-" are allowed`; + } + }, + }), + { + validate: (spaceIds) => { + if (_.uniq(spaceIds).length !== spaceIds.length) { + return 'duplicate space ids are not allowed'; + } + }, + } + ), + objects: schema.arrayOf( + schema.object({ + type: schema.string(), + id: schema.string(), + }), + { + validate: (objects) => { + if (!areObjectsUnique(objects)) { + return 'duplicate objects are not allowed'; + } + }, + } + ), + includeReferences: schema.boolean({ defaultValue: false }), + overwrite: schema.boolean({ defaultValue: false }), + createNewCopies: schema.boolean({ defaultValue: false }), + }, + { + validate: (object) => { + if (object.overwrite && object.createNewCopies) { + return 'cannot use [overwrite] with [createNewCopies]'; + } + }, + } + ), }, }, createLicensedRouteHandler(async (context, request, response) => { @@ -73,12 +83,19 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { getImportExportObjectLimit, request ); - const { spaces: destinationSpaceIds, objects, includeReferences, overwrite } = request.body; + const { + spaces: destinationSpaceIds, + objects, + includeReferences, + overwrite, + createNewCopies, + } = request.body; const sourceSpaceId = spacesService.getSpaceId(request); const copyResponse = await copySavedObjectsToSpaces(sourceSpaceId, destinationSpaceIds, { objects, includeReferences, overwrite, + createNewCopies, }); return response.ok({ body: copyResponse }); }) @@ -105,6 +122,9 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { type: schema.string(), id: schema.string(), overwrite: schema.boolean({ defaultValue: false }), + destinationId: schema.maybe(schema.string()), + createNewCopy: schema.maybe(schema.boolean()), + ignoreMissingReferences: schema.maybe(schema.boolean()), }) ) ), @@ -122,6 +142,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { } ), includeReferences: schema.boolean({ defaultValue: false }), + createNewCopies: schema.boolean({ defaultValue: false }), }), }, }, @@ -133,7 +154,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { getImportExportObjectLimit, request ); - const { objects, includeReferences, retries } = request.body; + const { objects, includeReferences, retries, createNewCopies } = request.body; const sourceSpaceId = spacesService.getSpaceId(request); const resolveConflictsResponse = await resolveCopySavedObjectsToSpacesConflicts( sourceSpaceId, @@ -141,6 +162,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { objects, includeReferences, retries, + createNewCopies, } ); return response.ok({ body: resolveConflictsResponse }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts index ec841808f771d2..a9b701a8ea395e 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts @@ -119,6 +119,22 @@ describe('GET /spaces/space', () => { expect(response.payload).toEqual(spaces); }); + it(`returns all available spaces with the 'shareSavedObjectsIntoSpace' purpose`, async () => { + const { routeHandler } = await setup(); + + const request = httpServerMock.createKibanaRequest({ + query: { + purpose: 'shareSavedObjectsIntoSpace', + }, + method: 'get', + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toEqual(200); + expect(response.payload).toEqual(spaces); + }); + it(`returns http/403 when the license is invalid`, async () => { const { routeHandler } = await setup(); diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_all.ts b/x-pack/plugins/spaces/server/routes/api/external/get_all.ts index cd1e03eb10b0a7..088409471fa55b 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_all.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_all.ts @@ -19,7 +19,11 @@ export function initGetAllSpacesApi(deps: ExternalRouteDeps) { validate: { query: schema.object({ purpose: schema.oneOf( - [schema.literal('any'), schema.literal('copySavedObjectsIntoSpace')], + [ + schema.literal('any'), + schema.literal('copySavedObjectsIntoSpace'), + schema.literal('shareSavedObjectsIntoSpace'), + ], { defaultValue: 'any', } diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts index 51c59212bef165..c9c17d091cd55c 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts @@ -90,7 +90,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const type = Symbol(); const id = Symbol(); const options = Object.freeze({ foo: 'bar' }); - // @ts-ignore + // @ts-expect-error const actualReturnValue = await client.get(type, id, options); expect(actualReturnValue).toBe(expectedReturnValue); @@ -117,7 +117,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const objects = [{ type: 'foo' }]; const options = Object.freeze({ foo: 'bar' }); - // @ts-ignore + // @ts-expect-error const actualReturnValue = await client.bulkGet(objects, options); expect(actualReturnValue).toBe(expectedReturnValue); @@ -263,6 +263,34 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); }); + describe('#checkConflicts', () => { + test(`throws error if options.namespace is specified`, async () => { + const { client } = await createSpacesSavedObjectsClient(); + + await expect( + // @ts-expect-error + client.checkConflicts(null, { namespace: 'bar' }) + ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); + }); + + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); + const expectedReturnValue = { errors: [] }; + baseClient.checkConflicts.mockReturnValue(Promise.resolve(expectedReturnValue)); + + const objects = Symbol(); + const options = Object.freeze({ foo: 'bar' }); + // @ts-expect-error + const actualReturnValue = await client.checkConflicts(objects, options); + + expect(actualReturnValue).toBe(expectedReturnValue); + expect(baseClient.checkConflicts).toHaveBeenCalledWith(objects, { + foo: 'bar', + namespace: currentSpace.expectedNamespace, + }); + }); + }); + describe('#create', () => { test(`throws error if options.namespace is specified`, async () => { const { client } = await createSpacesSavedObjectsClient(); @@ -280,7 +308,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const type = Symbol(); const attributes = Symbol(); const options = Object.freeze({ foo: 'bar' }); - // @ts-ignore + // @ts-expect-error const actualReturnValue = await client.create(type, attributes, options); expect(actualReturnValue).toBe(expectedReturnValue); @@ -307,7 +335,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const objects = [{ type: 'foo' }]; const options = Object.freeze({ foo: 'bar' }); - // @ts-ignore + // @ts-expect-error const actualReturnValue = await client.bulkCreate(objects, options); expect(actualReturnValue).toBe(expectedReturnValue); @@ -323,7 +351,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const { client } = await createSpacesSavedObjectsClient(); await expect( - // @ts-ignore + // @ts-expect-error client.update(null, null, null, { namespace: 'bar' }) ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); }); @@ -337,7 +365,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const id = Symbol(); const attributes = Symbol(); const options = Object.freeze({ foo: 'bar' }); - // @ts-ignore + // @ts-expect-error const actualReturnValue = await client.update(type, id, attributes, options); expect(actualReturnValue).toBe(expectedReturnValue); @@ -353,7 +381,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const { client } = await createSpacesSavedObjectsClient(); await expect( - // @ts-ignore + // @ts-expect-error client.bulkUpdate(null, { namespace: 'bar' }) ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); }); @@ -387,7 +415,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const { client } = await createSpacesSavedObjectsClient(); await expect( - // @ts-ignore + // @ts-expect-error client.delete(null, null, { namespace: 'bar' }) ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); }); @@ -400,7 +428,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const type = Symbol(); const id = Symbol(); const options = Object.freeze({ foo: 'bar' }); - // @ts-ignore + // @ts-expect-error const actualReturnValue = await client.delete(type, id, options); expect(actualReturnValue).toBe(expectedReturnValue); @@ -416,7 +444,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const { client } = await createSpacesSavedObjectsClient(); await expect( - // @ts-ignore + // @ts-expect-error client.addToNamespaces(null, null, null, { namespace: 'bar' }) ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); }); @@ -430,7 +458,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const id = Symbol(); const namespaces = Symbol(); const options = Object.freeze({ foo: 'bar' }); - // @ts-ignore + // @ts-expect-error const actualReturnValue = await client.addToNamespaces(type, id, namespaces, options); expect(actualReturnValue).toBe(expectedReturnValue); @@ -446,7 +474,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const { client } = await createSpacesSavedObjectsClient(); await expect( - // @ts-ignore + // @ts-expect-error client.deleteFromNamespaces(null, null, null, { namespace: 'bar' }) ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); }); @@ -460,7 +488,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const id = Symbol(); const namespaces = Symbol(); const options = Object.freeze({ foo: 'bar' }); - // @ts-ignore + // @ts-expect-error const actualReturnValue = await client.deleteFromNamespaces(type, id, namespaces, options); expect(actualReturnValue).toBe(expectedReturnValue); diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts index 7e2b302d7cff56..4e830d6149537c 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts @@ -9,6 +9,7 @@ import { SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, SavedObjectsBulkUpdateObject, + SavedObjectsCheckConflictsObject, SavedObjectsClientContract, SavedObjectsCreateOptions, SavedObjectsFindOptions, @@ -59,6 +60,25 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { this.errors = baseClient.errors; } + /** + * Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are + * multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. + * + * @param objects + * @param options + */ + public async checkConflicts( + objects: SavedObjectsCheckConflictsObject[] = [], + options: SavedObjectsBaseOptions = {} + ) { + throwErrorIfNamespaceSpecified(options); + + return await this.client.checkConflicts(objects, { + ...options, + namespace: spaceIdToNamespace(this.spaceId), + }); + } + /** * Persists an object * diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 92cc35e9e78ca3..70e2b34d06ce6d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2924,10 +2924,6 @@ "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage": "失敗したオブジェクトを再試行中…", "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage": "保存された検索が正しくリンクされていることを確認してください…", "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage": "矛盾を保存中…", - "savedObjectsManagement.objectsTable.flyout.confirmOverwriteBody": "{title}を上書きしてよろしいですか?", - "savedObjectsManagement.objectsTable.flyout.confirmOverwriteCancelButtonText": "キャンセル", - "savedObjectsManagement.objectsTable.flyout.confirmOverwriteOverwriteButtonText": "上書き", - "savedObjectsManagement.objectsTable.flyout.confirmOverwriteTitle": "{type}を上書きしますか?", "savedObjectsManagement.objectsTable.flyout.errorCalloutTitle": "申し訳ございません、エラーが発生しました", "savedObjectsManagement.objectsTable.flyout.import.cancelButtonLabel": "キャンセル", "savedObjectsManagement.objectsTable.flyout.import.confirmButtonLabel": "インポート", @@ -2950,7 +2946,6 @@ "savedObjectsManagement.objectsTable.flyout.invalidFormatOfImportedFileErrorMessage": "保存されたオブジェクトのファイル形式が無効なため、インポートできません。", "savedObjectsManagement.objectsTable.flyout.legacyFileUsedBody": "最新のレポートでNDJSONファイルを作成すれば完了です。", "savedObjectsManagement.objectsTable.flyout.legacyFileUsedTitle": "JSONファイルのサポートが終了します", - "savedObjectsManagement.objectsTable.flyout.overwriteSavedObjectsLabel": "すべての保存されたオブジェクトを自動的に上書きしますか?", "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountDescription": "影響されるオブジェクトの数です", "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountName": "カウント", "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnIdDescription": "インデックスパターンのIDです", @@ -17868,16 +17863,8 @@ "xpack.spaces.management.confirmDeleteModal.deletingSpaceWarningMessage": "スペースを削除すると、スペースと {allContents} が永久に削除されます。この操作は元に戻すことができません。", "xpack.spaces.management.confirmDeleteModal.redirectAfterDeletingCurrentSpaceWarningMessage": "現在のスペース {name} を削除しようとしています。続行すると、別のスペースを選択する画面に移動します。", "xpack.spaces.management.confirmDeleteModal.spaceNamesDoNoMatchErrorMessage": "スペース名が一致していません。", - "xpack.spaces.management.copyToSpace.actionDescription": "この保存されたオブジェクトを1つまたは複数のスペースにコピーします。", - "xpack.spaces.management.copyToSpace.actionTitle": "スペースにコピー", - "xpack.spaces.management.copyToSpace.automaticallyOverwrite": "すべての保存されたオブジェクトを自動的に上書き", - "xpack.spaces.management.copyToSpace.copyDetail.overwriteButton": "上書き", - "xpack.spaces.management.copyToSpace.copyDetail.skipOverwriteButton": "スキップ", "xpack.spaces.management.copyToSpace.copyErrorTitle": "保存されたオブジェクトのコピー中にエラーが発生", - "xpack.spaces.management.copyToSpace.copyResultsLabel": "コピー結果", "xpack.spaces.management.copyToSpace.copyStatus.conflictsMessage": "このスペースには同じID({id})の保存されたオブジェクトが既に存在します。", - "xpack.spaces.management.copyToSpace.copyStatus.conflictsOverwriteMessage": "「上書き」をクリックしてこのバージョンをコピーされたバージョンに置き換えます。", - "xpack.spaces.management.copyToSpace.copyStatus.pendingOverwriteMessage": "保存されたオブジェクトは上書きされます。「スキップ」をクリックしてこの操作をキャンセルします。", "xpack.spaces.management.copyToSpace.copyStatus.successMessage": "保存されたオブジェクトがコピーされました。", "xpack.spaces.management.copyToSpace.copyStatus.unresolvableErrorMessage": "この保存されたオブジェクトのコピー中にエラーが発生しました。", "xpack.spaces.management.copyToSpace.copyStatusSummary.conflictsMessage": "{space}スペースに1つまたは複数の矛盾が検出されました。解決するにはこのセクションを拡張してください。", @@ -17885,26 +17872,17 @@ "xpack.spaces.management.copyToSpace.copyStatusSummary.successMessage": "{space}スペースにコピーされました。", "xpack.spaces.management.copyToSpace.copyToSpacesButton": "{spaceCount} {spaceCount, plural, one {スペース} other {スペース}}にコピー", "xpack.spaces.management.copyToSpace.disabledCopyToSpacesButton": "コピー", - "xpack.spaces.management.copyToSpace.dontIncludeRelatedLabel": "関連性のある保存されたオブジェクトを含みません", - "xpack.spaces.management.copyToSpace.dontOverwriteLabel": "保存されたオブジェクトを上書きしません", "xpack.spaces.management.copyToSpace.finishCopyToSpacesButton": "終了", "xpack.spaces.management.copyToSpace.finishedButtonLabel": "コピーが完了しました。", - "xpack.spaces.management.copyToSpace.finishPendingOverwritesCopyToSpacesButton": "{overwriteCount}件のオブジェクトを上書き", - "xpack.spaces.management.copyToSpace.includeRelatedFormLabel": "関連性のある保存されたオブジェクトを含みます", - "xpack.spaces.management.copyToSpace.includeRelatedLabel": "関連性のある保存されたオブジェクトを含みます", "xpack.spaces.management.copyToSpace.inProgressButtonLabel": "コピーが進行中です。お待ちください。", "xpack.spaces.management.copyToSpace.noSpacesBody": "コピーできるスペースがありません。", "xpack.spaces.management.copyToSpace.noSpacesTitle": "スペースがありません", "xpack.spaces.management.copyToSpace.overwriteLabel": "保存されたオブジェクトを自動的に上書きしています", "xpack.spaces.management.copyToSpace.resolveCopyErrorTitle": "保存されたオブジェクトの矛盾の解決中にエラーが発生", - "xpack.spaces.management.copyToSpace.resolveCopySuccessTitle": "上書き成功", - "xpack.spaces.management.copyToSpace.selectSpacesLabel": "コピー先のスペースを選択してください", "xpack.spaces.management.copyToSpace.spacesLoadErrorTitle": "利用可能なスペースを読み込み中にエラーが発生", - "xpack.spaces.management.copyToSpaceFlyoutFooter.conflictCount": "スキップ", "xpack.spaces.management.copyToSpaceFlyoutFooter.errorCount": "エラー", "xpack.spaces.management.copyToSpaceFlyoutFooter.pendingCount": "保留中", "xpack.spaces.management.copyToSpaceFlyoutFooter.successCount": "コピー完了", - "xpack.spaces.management.copyToSpaceFlyoutHeader": "保存されたオブジェクトのスペースへのコピー", "xpack.spaces.management.createSpaceBreadcrumb": "作成", "xpack.spaces.management.customizeSpaceAvatar.colorFormRowLabel": "色", "xpack.spaces.management.customizeSpaceAvatar.imageUrl": "カスタム画像", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 59d0e63ef2d4aa..e682a12859c472 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2925,10 +2925,6 @@ "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage": "正在重试失败的对象……", "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage": "确保已保存搜索已正确链接……", "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage": "正在保存冲突……", - "savedObjectsManagement.objectsTable.flyout.confirmOverwriteBody": "确定要覆盖“{title}”?", - "savedObjectsManagement.objectsTable.flyout.confirmOverwriteCancelButtonText": "取消", - "savedObjectsManagement.objectsTable.flyout.confirmOverwriteOverwriteButtonText": "覆盖", - "savedObjectsManagement.objectsTable.flyout.confirmOverwriteTitle": "覆盖“{type}”?", "savedObjectsManagement.objectsTable.flyout.errorCalloutTitle": "抱歉,有错误", "savedObjectsManagement.objectsTable.flyout.import.cancelButtonLabel": "取消", "savedObjectsManagement.objectsTable.flyout.import.confirmButtonLabel": "导入", @@ -2951,7 +2947,6 @@ "savedObjectsManagement.objectsTable.flyout.invalidFormatOfImportedFileErrorMessage": "已保存对象文件格式无效,无法导入。", "savedObjectsManagement.objectsTable.flyout.legacyFileUsedBody": "只需使用更新的导出功能生成 NDJSON 文件,便万事俱备。", "savedObjectsManagement.objectsTable.flyout.legacyFileUsedTitle": "将不再支持 JSON 文件", - "savedObjectsManagement.objectsTable.flyout.overwriteSavedObjectsLabel": "自动覆盖所有已保存对象?", "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountDescription": "受影响对象数目", "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountName": "计数", "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnIdDescription": "索引模式的 ID", @@ -17875,16 +17870,8 @@ "xpack.spaces.management.confirmDeleteModal.deletingSpaceWarningMessage": "删除空间会永久删除空间及其 {allContents}。此操作无法撤消。", "xpack.spaces.management.confirmDeleteModal.redirectAfterDeletingCurrentSpaceWarningMessage": "您即将删除当前空间 {name}。如果继续,系统会将您重定向到选择其他空间的位置。", "xpack.spaces.management.confirmDeleteModal.spaceNamesDoNoMatchErrorMessage": "空间名称不匹配。", - "xpack.spaces.management.copyToSpace.actionDescription": "将此已保存对象复制到一个或多个工作区", - "xpack.spaces.management.copyToSpace.actionTitle": "复制到工作区", - "xpack.spaces.management.copyToSpace.automaticallyOverwrite": "自动覆盖所有已保存对象", - "xpack.spaces.management.copyToSpace.copyDetail.overwriteButton": "覆盖", - "xpack.spaces.management.copyToSpace.copyDetail.skipOverwriteButton": "跳过", "xpack.spaces.management.copyToSpace.copyErrorTitle": "复制已保存对象时出错", - "xpack.spaces.management.copyToSpace.copyResultsLabel": "复制结果", "xpack.spaces.management.copyToSpace.copyStatus.conflictsMessage": "具有匹配 ID ({id}) 的已保存对象在此工作区中已存在。", - "xpack.spaces.management.copyToSpace.copyStatus.conflictsOverwriteMessage": "单击“覆盖”可将此版本替换为复制的版本。", - "xpack.spaces.management.copyToSpace.copyStatus.pendingOverwriteMessage": "已保存对象将被覆盖。单击“跳过”可取消此操作。", "xpack.spaces.management.copyToSpace.copyStatus.successMessage": "已保存对象成功复制。", "xpack.spaces.management.copyToSpace.copyStatus.unresolvableErrorMessage": "复制此已保存对象时出错。", "xpack.spaces.management.copyToSpace.copyStatusSummary.conflictsMessage": "在 {space} 工作区中检测到一个或多个冲突。展开此部分以进行解决。", @@ -17892,26 +17879,17 @@ "xpack.spaces.management.copyToSpace.copyStatusSummary.successMessage": "已成功复制到 {space} 工作区。", "xpack.spaces.management.copyToSpace.copyToSpacesButton": "复制到 {spaceCount} {spaceCount, plural, one {个工作区} other {个工作区}}", "xpack.spaces.management.copyToSpace.disabledCopyToSpacesButton": "复制", - "xpack.spaces.management.copyToSpace.dontIncludeRelatedLabel": "不包括相关已保存对象", - "xpack.spaces.management.copyToSpace.dontOverwriteLabel": "未覆盖已保存对象", "xpack.spaces.management.copyToSpace.finishCopyToSpacesButton": "完成", "xpack.spaces.management.copyToSpace.finishedButtonLabel": "复制已完成。", - "xpack.spaces.management.copyToSpace.finishPendingOverwritesCopyToSpacesButton": "覆盖 {overwriteCount} 个对象", - "xpack.spaces.management.copyToSpace.includeRelatedFormLabel": "包括相关已保存对象", - "xpack.spaces.management.copyToSpace.includeRelatedLabel": "包括相关已保存对象", "xpack.spaces.management.copyToSpace.inProgressButtonLabel": "复制正在进行中。请稍候。", "xpack.spaces.management.copyToSpace.noSpacesBody": "没有可向其中进行复制的合格工作区。", "xpack.spaces.management.copyToSpace.noSpacesTitle": "没有可用的工作区", "xpack.spaces.management.copyToSpace.overwriteLabel": "正在自动覆盖已保存对象", "xpack.spaces.management.copyToSpace.resolveCopyErrorTitle": "解决已保存对象冲突时出错", - "xpack.spaces.management.copyToSpace.resolveCopySuccessTitle": "覆盖成功", - "xpack.spaces.management.copyToSpace.selectSpacesLabel": "选择要向其中进行复制的工作区", "xpack.spaces.management.copyToSpace.spacesLoadErrorTitle": "加载可用工作区时出错", - "xpack.spaces.management.copyToSpaceFlyoutFooter.conflictCount": "已跳过", "xpack.spaces.management.copyToSpaceFlyoutFooter.errorCount": "错误", "xpack.spaces.management.copyToSpaceFlyoutFooter.pendingCount": "待处理", "xpack.spaces.management.copyToSpaceFlyoutFooter.successCount": "已复制", - "xpack.spaces.management.copyToSpaceFlyoutHeader": "将已保存对象复制到工作区", "xpack.spaces.management.createSpaceBreadcrumb": "创建", "xpack.spaces.management.customizeSpaceAvatar.colorFormRowLabel": "颜色", "xpack.spaces.management.customizeSpaceAvatar.imageUrl": "定制图像", diff --git a/x-pack/test/functional/apps/spaces/copy_saved_objects.ts b/x-pack/test/functional/apps/spaces/copy_saved_objects.ts index 05d497c235dadf..2ee6b903cc3a98 100644 --- a/x-pack/test/functional/apps/spaces/copy_saved_objects.ts +++ b/x-pack/test/functional/apps/spaces/copy_saved_objects.ts @@ -65,10 +65,10 @@ export default function spaceSelectorFunctonalTests({ const summaryCounts = await PageObjects.copySavedObjectsToSpace.getSummaryCounts(); expect(summaryCounts).to.eql({ - copied: 3, + success: 3, + pending: 0, skipped: 0, errors: 0, - overwrite: undefined, }); await PageObjects.copySavedObjectsToSpace.finishCopy(); @@ -93,23 +93,23 @@ export default function spaceSelectorFunctonalTests({ const summaryCounts = await PageObjects.copySavedObjectsToSpace.getSummaryCounts(); expect(summaryCounts).to.eql({ - copied: 2, + success: 0, + pending: 2, skipped: 1, errors: 0, - overwrite: undefined, }); // Mark conflict for overwrite await testSubjects.click(`cts-space-result-${destinationSpaceId}`); - await testSubjects.click(`cts-overwrite-conflict-logstash-*`); + await testSubjects.click(`cts-overwrite-conflict-index-pattern:logstash-*`); // Verify summary changed - const updatedSummaryCounts = await PageObjects.copySavedObjectsToSpace.getSummaryCounts(true); + const updatedSummaryCounts = await PageObjects.copySavedObjectsToSpace.getSummaryCounts(); expect(updatedSummaryCounts).to.eql({ - copied: 2, + success: 0, + pending: 3, skipped: 0, - overwrite: 1, errors: 0, }); diff --git a/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts b/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts index 629a86520389dd..6b8680271635be 100644 --- a/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts +++ b/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts @@ -35,7 +35,11 @@ export function CopySavedObjectsToSpacePageProvider({ destinationSpaceId: string; }) { if (!overwrite) { - await testSubjects.click('cts-form-overwrite'); + const radio = await testSubjects.find('cts-copyModeControl-overwriteRadioGroup'); + // a radio button consists of a div tag that contains an input, a div, and a label + // we can't click the input directly, need to go up one level and click the parent div + const div = await radio.findByXpath("//div[input[@id='overwriteDisabled']]"); + await div.click(); } await testSubjects.click(`cts-space-selector-row-${destinationSpaceId}`); }, @@ -49,31 +53,25 @@ export function CopySavedObjectsToSpacePageProvider({ await testSubjects.waitForDeleted('copy-to-space-flyout'); }, - async getSummaryCounts(includeOverwrite: boolean = false) { - const copied = extractCountFromSummary( + async getSummaryCounts() { + const success = extractCountFromSummary( await testSubjects.getVisibleText('cts-summary-success-count') ); + const pending = extractCountFromSummary( + await testSubjects.getVisibleText('cts-summary-pending-count') + ); const skipped = extractCountFromSummary( - await testSubjects.getVisibleText('cts-summary-conflict-count') + await testSubjects.getVisibleText('cts-summary-skipped-count') ); const errors = extractCountFromSummary( await testSubjects.getVisibleText('cts-summary-error-count') ); - let overwrite; - if (includeOverwrite) { - overwrite = extractCountFromSummary( - await testSubjects.getVisibleText('cts-summary-overwrite-count') - ); - } else { - await testSubjects.missingOrFail('cts-summary-overwrite-count', { timeout: 250 }); - } - return { - copied, + success, + pending, skipped, errors, - overwrite, }; }, }; diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json index d2c14189e2529a..4c0447c29c8f9c 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -397,3 +397,91 @@ "type": "doc" } } + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_1", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in all spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_1", "space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_2a", + "index": ".kibana", + "source": { + "originId": "conflict_2", + "sharedtype": { + "title": "A shared saved-object in all spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_1", "space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_2b", + "index": ".kibana", + "source": { + "originId": "conflict_2", + "sharedtype": { + "title": "A shared saved-object in all spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_1", "space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_3", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in all spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_1", "space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_4a", + "index": ".kibana", + "source": { + "originId": "conflict_4", + "sharedtype": { + "title": "A shared saved-object in all spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_1", "space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json index 7b5b1d86f6bcc8..73f0e536b92957 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json @@ -182,6 +182,9 @@ "namespaces": { "type": "keyword" }, + "originId": { + "type": "keyword" + }, "search": { "properties": { "columns": { diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts b/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts index 0c15ab4bd2f804..45880635586a72 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts +++ b/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts @@ -48,6 +48,7 @@ export class Plugin { name: 'sharedtype', hidden: false, namespaceType: 'multiple', + management, mappings, }); core.savedObjects.registerType({ diff --git a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts index 5d08421038d3f5..595986c08efc1d 100644 --- a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts +++ b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts @@ -168,7 +168,9 @@ export const expectResponses = { expect(actualNamespace).to.eql(spaceId); } if (isMultiNamespace(type)) { - if (id === CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1.id) { + if (['conflict_1', 'conflict_2a', 'conflict_2b', 'conflict_3', 'conflict_4a'].includes(id)) { + expect(actualNamespaces).to.eql([DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]); + } else if (id === CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1.id) { expect(actualNamespaces).to.eql([DEFAULT_SPACE_ID, SPACE_1_ID]); } else if (id === CASES.MULTI_NAMESPACE_ONLY_SPACE_1.id) { expect(actualNamespaces).to.eql([SPACE_1_ID]); diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts index bc356927cc0af8..e3163ef77d4279 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; import { SPACES } from '../lib/spaces'; import { @@ -23,6 +24,7 @@ export interface BulkCreateTestDefinition extends TestDefinition { export type BulkCreateTestSuite = TestSuite; export interface BulkCreateTestCase extends TestCase { failure?: 400 | 409; // only used for permitted response case + fail409Param?: string; } const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake @@ -56,6 +58,15 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: for (let i = 0; i < savedObjects.length; i++) { const object = savedObjects[i]; const testCase = testCaseArray[i]; + if (testCase.failure === 409 && testCase.fail409Param === 'unresolvableConflict') { + const { type, id } = testCase; + const error = SavedObjectsErrorHelpers.createConflictError(type, id); + const payload = { ...error.output.payload, metadata: { isNotOverwritable: true } }; + expect(object.type).to.eql(type); + expect(object.id).to.eql(id); + expect(object.error).to.eql(payload); + continue; + } await expectResponses.permitted(object, testCase); if (!testCase.failure) { expect(object.attributes[NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); diff --git a/x-pack/test/saved_object_api_integration/common/suites/export.ts b/x-pack/test/saved_object_api_integration/common/suites/export.ts index ff22cdaeafd061..4a8eff1fb380c0 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/export.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/export.ts @@ -8,7 +8,7 @@ import { SuperTest } from 'supertest'; import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; import { SPACES } from '../lib/spaces'; import { expectResponses, getUrlPrefix } from '../lib/saved_object_test_utils'; -import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; +import { ExpectResponseBody, TestDefinition, TestSuite } from '../lib/types'; const { DEFAULT: { spaceId: DEFAULT_SPACE_ID }, @@ -20,15 +20,28 @@ export interface ExportTestDefinition extends TestDefinition { request: ReturnType; } export type ExportTestSuite = TestSuite; +interface SuccessResult { + type: string; + id: string; + originId?: string; +} export interface ExportTestCase { title: string; type: string; id?: string; - successResult?: TestCase | TestCase[]; + successResult?: SuccessResult | SuccessResult[]; failure?: 400 | 403; } -export const getTestCases = (spaceId?: string) => ({ +// additional sharedtype objects that exist but do not have common test cases defined +const CID = 'conflict_'; +const CONFLICT_1_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}1` }); +const CONFLICT_2A_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}2a`, originId: `${CID}2` }); +const CONFLICT_2B_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}2b`, originId: `${CID}2` }); +const CONFLICT_3_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}3` }); +const CONFLICT_4A_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}4a`, originId: `${CID}4` }); + +export const getTestCases = (spaceId?: string): { [key: string]: ExportTestCase } => ({ singleNamespaceObject: { title: 'single-namespace object', ...(spaceId === SPACE_1_ID @@ -36,7 +49,7 @@ export const getTestCases = (spaceId?: string) => ({ : spaceId === SPACE_2_ID ? CASES.SINGLE_NAMESPACE_SPACE_2 : CASES.SINGLE_NAMESPACE_DEFAULT_SPACE), - } as ExportTestCase, + }, singleNamespaceType: { // this test explicitly ensures that single-namespace objects from other spaces are not returned title: 'single-namespace type', @@ -47,7 +60,7 @@ export const getTestCases = (spaceId?: string) => ({ : spaceId === SPACE_2_ID ? CASES.SINGLE_NAMESPACE_SPACE_2 : CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, - } as ExportTestCase, + }, multiNamespaceObject: { title: 'multi-namespace object', ...(spaceId === SPACE_1_ID @@ -55,30 +68,30 @@ export const getTestCases = (spaceId?: string) => ({ : spaceId === SPACE_2_ID ? CASES.MULTI_NAMESPACE_ONLY_SPACE_2 : CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1), - failure: 400, // multi-namespace types cannot be exported yet - } as ExportTestCase, + }, multiNamespaceType: { title: 'multi-namespace type', type: 'sharedtype', - // successResult: - // spaceId === SPACE_1_ID - // ? [CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, CASES.MULTI_NAMESPACE_ONLY_SPACE_1] - // : spaceId === SPACE_2_ID - // ? CASES.MULTI_NAMESPACE_ONLY_SPACE_2 - // : CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, - failure: 400, // multi-namespace types cannot be exported yet - } as ExportTestCase, + successResult: (spaceId === SPACE_1_ID + ? [CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, CASES.MULTI_NAMESPACE_ONLY_SPACE_1] + : spaceId === SPACE_2_ID + ? [CASES.MULTI_NAMESPACE_ONLY_SPACE_2] + : [CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1] + ) + .concat([CONFLICT_1_OBJ, CONFLICT_2A_OBJ, CONFLICT_2B_OBJ, CONFLICT_3_OBJ, CONFLICT_4A_OBJ]) + .flat(), + }, namespaceAgnosticObject: { title: 'namespace-agnostic object', ...CASES.NAMESPACE_AGNOSTIC, - } as ExportTestCase, + }, namespaceAgnosticType: { title: 'namespace-agnostic type', type: 'globaltype', successResult: CASES.NAMESPACE_AGNOSTIC, - } as ExportTestCase, - hiddenObject: { title: 'hidden object', ...CASES.HIDDEN, failure: 400 } as ExportTestCase, - hiddenType: { title: 'hidden type', type: 'hiddentype', failure: 400 } as ExportTestCase, + }, + hiddenObject: { title: 'hidden object', ...CASES.HIDDEN, failure: 400 }, + hiddenType: { title: 'hidden type', type: 'hiddentype', failure: 400 }, }); export const createRequest = ({ type, id }: ExportTestCase) => id ? { objects: [{ type, id }] } : { type }; @@ -98,7 +111,7 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest async ( response: Record ) => { - const { type, id, successResult = { type, id }, failure } = testCase; + const { type, id, successResult = { type, id } as SuccessResult, failure } = testCase; if (failure === 403) { // In export only, the API uses "bulk_get" or "find" depending on the parameters it receives. // The best that could be done here is to have an if statement to ensure at least one of the @@ -125,11 +138,14 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest x.id === object.id)!; + expect(expected).not.to.be(undefined); + expect(object.type).to.eql(expected.type); + if (object.originId) { + expect(object.originId).to.eql(expected.originId); + } expect(object.updated_at).to.match(/^[\d-]{10}T[\d:\.]{12}Z$/); // don't test attributes, version, or references } diff --git a/x-pack/test/saved_object_api_integration/common/suites/find.ts b/x-pack/test/saved_object_api_integration/common/suites/find.ts index 882451c28bfe46..bab4a4d88534a8 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/find.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/find.ts @@ -43,6 +43,36 @@ export interface FindTestCase { }; } +// additional sharedtype objects that exist but do not have common test cases defined +const CONFLICT_1_OBJ = Object.freeze({ + type: 'sharedtype', + id: 'conflict_1', + namespaces: ['default', 'space_1', 'space_2'], +}); +const CONFLICT_2A_OBJ = Object.freeze({ + type: 'sharedtype', + id: 'conflict_2a', + originId: 'conflict_2', + namespaces: ['default', 'space_1', 'space_2'], +}); +const CONFLICT_2B_OBJ = Object.freeze({ + type: 'sharedtype', + id: 'conflict_2b', + originId: 'conflict_2', + namespaces: ['default', 'space_1', 'space_2'], +}); +const CONFLICT_3_OBJ = Object.freeze({ + type: 'sharedtype', + id: 'conflict_3', + namespaces: ['default', 'space_1', 'space_2'], +}); +const CONFLICT_4A_OBJ = Object.freeze({ + type: 'sharedtype', + id: 'conflict_4a', + originId: 'conflict_4', + namespaces: ['default', 'space_1', 'space_2'], +}); + const TEST_CASES = [ { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, namespaces: ['default'] }, { ...CASES.SINGLE_NAMESPACE_SPACE_1, namespaces: ['space_1'] }, @@ -110,7 +140,13 @@ export const getTestCases = ( query: `type=sharedtype&fields=title${namespacesQueryParam}`, successResult: { // expected depends on which spaces the user is authorized against... - savedObjects: getExpectedSavedObjects((t) => t.type === 'sharedtype'), + savedObjects: getExpectedSavedObjects((t) => t.type === 'sharedtype').concat( + CONFLICT_1_OBJ, + CONFLICT_2A_OBJ, + CONFLICT_2B_OBJ, + CONFLICT_3_OBJ, + CONFLICT_4A_OBJ + ), }, } as FindTestCase, namespaceAgnosticType: { diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts index ed57c6eb16b9a7..5036d7b2008810 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/import.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts @@ -8,33 +8,66 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; import { SPACES } from '../lib/spaces'; -import { - createRequest, - expectResponses, - getUrlPrefix, - getTestTitle, -} from '../lib/saved_object_test_utils'; +import { expectResponses, getUrlPrefix, getTestTitle } from '../lib/saved_object_test_utils'; import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; export interface ImportTestDefinition extends TestDefinition { - request: Array<{ type: string; id: string }>; + request: Array<{ type: string; id: string; originId?: string }>; + overwrite: boolean; + createNewCopies: boolean; } export type ImportTestSuite = TestSuite; export interface ImportTestCase extends TestCase { + originId?: string; + expectedNewId?: string; + successParam?: string; failure?: 400 | 409; // only used for permitted response case + fail409Param?: string; } const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; -const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' }); -const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }); -const NEW_NAMESPACE_AGNOSTIC_OBJ = Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }); +// these five saved objects already exist in the sample data: +// * id: conflict_1 +// * id: conflict_2a, originId: conflict_2 +// * id: conflict_2b, originId: conflict_2 +// * id: conflict_3 +// * id: conflict_4a, originId: conflict_4 +// using the seven conflict test case objects below, we can exercise various permutations of exact/inexact/ambiguous conflict scenarios +const CID = 'conflict_'; export const TEST_CASES = Object.freeze({ ...CASES, - NEW_SINGLE_NAMESPACE_OBJ, - NEW_MULTI_NAMESPACE_OBJ, - NEW_NAMESPACE_AGNOSTIC_OBJ, + CONFLICT_1_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}1` }), + CONFLICT_1A_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}1a`, originId: `${CID}1` }), + CONFLICT_1B_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}1b`, originId: `${CID}1` }), + CONFLICT_2C_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}2c`, originId: `${CID}2` }), + CONFLICT_2D_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}2d`, originId: `${CID}2` }), + CONFLICT_3A_OBJ: Object.freeze({ + type: 'sharedtype', + id: `${CID}3a`, + originId: `${CID}3`, + expectedNewId: `${CID}3`, + }), + CONFLICT_4_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}4`, expectedNewId: `${CID}4a` }), + NEW_SINGLE_NAMESPACE_OBJ: Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' }), + NEW_MULTI_NAMESPACE_OBJ: Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }), + NEW_NAMESPACE_AGNOSTIC_OBJ: Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }), +}); + +/** + * Test cases have additional properties that we don't want to send in HTTP Requests + */ +const createRequest = ({ type, id, originId }: ImportTestCase) => ({ + type, + id, + ...(originId && { originId }), +}); + +const getConflictDest = (id: string) => ({ + id, + title: 'A shared saved-object in all spaces', + updatedAt: '2017-09-21T18:59:16.270Z', }); export function importTestSuiteFactory(es: any, esArchiver: any, supertest: SuperTest) { @@ -42,6 +75,9 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe const expectResponseBody = ( testCases: ImportTestCase | ImportTestCase[], statusCode: 200 | 403, + singleRequest: boolean, + overwrite: boolean, + createNewCopies: boolean, spaceId = SPACES.DEFAULT.spaceId ): ExpectResponseBody => async (response: Record) => { const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; @@ -50,7 +86,7 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe await expectForbidden(types)(response); } else { // permitted - const { success, successCount, errors } = response.body; + const { success, successCount, successResults, errors } = response.body; const expectedSuccesses = testCaseArray.filter((x) => !x.failure); const expectedFailures = testCaseArray.filter((x) => x.failure); expect(success).to.eql(expectedFailures.length === 0); @@ -61,12 +97,53 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe expect(response.body).not.to.have.property('errors'); } for (let i = 0; i < expectedSuccesses.length; i++) { - const { type, id } = expectedSuccesses[i]; - const { _source } = await expectResponses.successCreated(es, spaceId, type, id); - expect(_source[type][NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); + const { type, id, successParam, expectedNewId } = expectedSuccesses[i]; + // we don't know the order of the returned successResults; search for each one + const object = (successResults as Array>).find( + (x) => x.type === type && x.id === id + ); + expect(object).not.to.be(undefined); + const destinationId = object!.destinationId as string; + if (successParam === 'destinationId') { + // Kibana created the object with a different ID than what was specified in the import + // This can happen due to an unresolvable conflict (so the new ID will be random), or due to an inexact match (so the new ID will + // be equal to the ID or originID of the existing object that it inexactly matched) + if (expectedNewId) { + expect(destinationId).to.be(expectedNewId); + } else { + // the new ID was randomly generated + expect(destinationId).to.match(/^[0-9a-f-]{36}$/); + } + } else if (successParam === 'createNewCopies' || successParam === 'createNewCopy') { + // the new ID was randomly generated + expect(destinationId).to.match(/^[0-9a-f-]{36}$/); + } else { + expect(destinationId).to.be(undefined); + } + + // This assertion is only needed for the case where `createNewCopies` mode is disabled and ambiguous source conflicts are detected. + // When `createNewCopies` mode is permanently enabled, this field will be removed, and this assertion will be redundant and can be + // removed too. + const createNewCopy = object!.createNewCopy as boolean | undefined; + if (successParam === 'createNewCopy') { + expect(createNewCopy).to.be(true); + } else { + expect(createNewCopy).to.be(undefined); + } + + if (!singleRequest || overwrite || createNewCopies) { + // even if the object result was a "success" result, it may not have been created if other resolvable errors were returned + const { _source } = await expectResponses.successCreated( + es, + spaceId, + type, + destinationId ?? id + ); + expect(_source[type][NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); + } } for (let i = 0; i < expectedFailures.length; i++) { - const { type, id, failure } = expectedFailures[i]; + const { type, id, failure, fail409Param, expectedNewId } = expectedFailures[i]; // we don't know the order of the returned errors; search for each one const object = (errors as Array>).find( (x) => x.type === type && x.id === id @@ -76,7 +153,24 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe expect(object!.error).to.eql({ type: 'unsupported_type' }); } else { // 409 - expect(object!.error).to.eql({ type: 'conflict' }); + let error: Record = { + type: 'conflict', + ...(expectedNewId && { destinationId: expectedNewId }), + }; + if (fail409Param === 'ambiguous_conflict_1a1b') { + // "ambiguous source" conflict + error = { + type: 'ambiguous_conflict', + destinations: [getConflictDest(`${CID}1`)], + }; + } else if (fail409Param === 'ambiguous_conflict_2c') { + // "ambiguous destination" conflict + error = { + type: 'ambiguous_conflict', + destinations: [getConflictDest(`${CID}2a`), getConflictDest(`${CID}2b`)], + }; + } + expect(object!.error).to.eql(error); } } } @@ -84,7 +178,9 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe const createTestDefinitions = ( testCases: ImportTestCase | ImportTestCase[], forbidden: boolean, - options?: { + options: { + overwrite?: boolean; + createNewCopies?: boolean; spaceId?: string; singleRequest?: boolean; responseBodyOverride?: ExpectResponseBody; @@ -92,7 +188,14 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe ): ImportTestDefinition[] => { const cases = Array.isArray(testCases) ? testCases : [testCases]; const responseStatusCode = forbidden ? 403 : 200; - if (!options?.singleRequest) { + const { + overwrite = false, + createNewCopies = false, + spaceId, + singleRequest, + responseBodyOverride, + } = options; + if (!singleRequest) { // if we are testing cases that should result in a forbidden response, we can do each case individually // this ensures that multiple test cases of a single type will each result in a forbidden error return cases.map((x) => ({ @@ -100,8 +203,10 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe request: [createRequest(x)], responseStatusCode, responseBody: - options?.responseBodyOverride || - expectResponseBody(x, responseStatusCode, options?.spaceId), + responseBodyOverride || + expectResponseBody(x, responseStatusCode, false, overwrite, createNewCopies, spaceId), + overwrite, + createNewCopies, })); } // batch into a single request to save time during test execution @@ -111,8 +216,10 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe request: cases.map((x) => createRequest(x)), responseStatusCode, responseBody: - options?.responseBodyOverride || - expectResponseBody(cases, responseStatusCode, options?.spaceId), + responseBodyOverride || + expectResponseBody(cases, responseStatusCode, true, overwrite, createNewCopies, spaceId), + overwrite, + createNewCopies, }, ]; }; @@ -134,8 +241,13 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe const requestBody = test.request .map((obj) => JSON.stringify({ ...obj, ...attrs })) .join('\n'); + const query = test.overwrite + ? '?overwrite=true' + : test.createNewCopies + ? '?createNewCopies=true' + : ''; await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import`) + .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import${query}`) .auth(user?.username, user?.password) .attach('file', Buffer.from(requestBody, 'utf8'), 'export.ndjson') .expect(test.responseStatusCode) diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts index 822214cd6dc6aa..6d294aed9b4de7 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts @@ -8,34 +8,85 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; import { SPACES } from '../lib/spaces'; -import { - createRequest, - expectResponses, - getUrlPrefix, - getTestTitle, -} from '../lib/saved_object_test_utils'; +import { expectResponses, getUrlPrefix, getTestTitle } from '../lib/saved_object_test_utils'; import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; export interface ResolveImportErrorsTestDefinition extends TestDefinition { - request: Array<{ type: string; id: string }>; + request: { + objects: Array<{ type: string; id: string; originId?: string }>; + retries: Array<{ type: string; id: string; overwrite: boolean; destinationId?: string }>; + }; overwrite: boolean; + createNewCopies: boolean; } export type ResolveImportErrorsTestSuite = TestSuite; export interface ResolveImportErrorsTestCase extends TestCase { + originId?: string; + expectedNewId?: string; + successParam?: string; failure?: 400 | 409; // only used for permitted response case } const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; -const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' }); -const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }); -const NEW_NAMESPACE_AGNOSTIC_OBJ = Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }); +// these five saved objects already exist in the sample data: +// * id: conflict_1 +// * id: conflict_2a, originId: conflict_2 +// * id: conflict_2b, originId: conflict_2 +// * id: conflict_3 +// * id: conflict_4a, originId: conflict_4 +// using the five conflict test case objects below, we can exercise various permutations of exact/inexact/ambiguous conflict scenarios export const TEST_CASES = Object.freeze({ ...CASES, - NEW_SINGLE_NAMESPACE_OBJ, - NEW_MULTI_NAMESPACE_OBJ, - NEW_NAMESPACE_AGNOSTIC_OBJ, + CONFLICT_1A_OBJ: Object.freeze({ + type: 'sharedtype', + id: `conflict_1a`, + originId: `conflict_1`, + expectedNewId: 'some-random-id', + }), + CONFLICT_1B_OBJ: Object.freeze({ + type: 'sharedtype', + id: `conflict_1b`, + originId: `conflict_1`, + expectedNewId: 'another-random-id', + }), + CONFLICT_2C_OBJ: Object.freeze({ + type: 'sharedtype', + id: `conflict_2c`, + originId: `conflict_2`, + expectedNewId: `conflict_2a`, + }), + CONFLICT_3A_OBJ: Object.freeze({ + type: 'sharedtype', + id: `conflict_3a`, + originId: `conflict_3`, + expectedNewId: `conflict_3`, + }), + CONFLICT_4_OBJ: Object.freeze({ + type: 'sharedtype', + id: `conflict_4`, + expectedNewId: `conflict_4a`, + }), +}); + +/** + * Test cases have additional properties that we don't want to send in HTTP Requests + */ +const createRequest = ( + { type, id, originId, expectedNewId, successParam }: ResolveImportErrorsTestCase, + overwrite: boolean +): ResolveImportErrorsTestDefinition['request'] => ({ + objects: [{ type, id, ...(originId && { originId }) }], + retries: [ + { + type, + id, + overwrite, + ...(expectedNewId && { destinationId: expectedNewId }), + ...(successParam === 'createNewCopy' && { createNewCopy: true }), + }, + ], }); export function resolveImportErrorsTestSuiteFactory( @@ -47,6 +98,9 @@ export function resolveImportErrorsTestSuiteFactory( const expectResponseBody = ( testCases: ResolveImportErrorsTestCase | ResolveImportErrorsTestCase[], statusCode: 200 | 403, + singleRequest: boolean, + overwrite: boolean, + createNewCopies: boolean, spaceId = SPACES.DEFAULT.spaceId ): ExpectResponseBody => async (response: Record) => { const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; @@ -55,7 +109,7 @@ export function resolveImportErrorsTestSuiteFactory( await expectForbidden(types)(response); } else { // permitted - const { success, successCount, errors } = response.body; + const { success, successCount, successResults, errors } = response.body; const expectedSuccesses = testCaseArray.filter((x) => !x.failure); const expectedFailures = testCaseArray.filter((x) => x.failure); expect(success).to.eql(expectedFailures.length === 0); @@ -66,12 +120,51 @@ export function resolveImportErrorsTestSuiteFactory( expect(response.body).not.to.have.property('errors'); } for (let i = 0; i < expectedSuccesses.length; i++) { - const { type, id } = expectedSuccesses[i]; - const { _source } = await expectResponses.successCreated(es, spaceId, type, id); - expect(_source[type][NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); + const { type, id, successParam, expectedNewId } = expectedSuccesses[i]; + // we don't know the order of the returned successResults; search for each one + const object = (successResults as Array>).find( + (x) => x.type === type && x.id === id + ); + expect(object).not.to.be(undefined); + const destinationId = object!.destinationId as string; + if (successParam === 'destinationId') { + // Kibana created the object with a different ID than what was specified in the import + // This can happen due to an unresolvable conflict (so the new ID will be random), or due to an inexact match (so the new ID will + // be equal to the ID or originID of the existing object that it inexactly matched) + if (expectedNewId) { + expect(destinationId).to.be(expectedNewId); + } else { + // the new ID was randomly generated + expect(destinationId).to.match(/^[0-9a-f-]{36}$/); + } + } else if (successParam === 'createNewCopies' || successParam === 'createNewCopy') { + expect(destinationId).to.be(expectedNewId!); + } else { + expect(destinationId).to.be(undefined); + } + + // This assertion is only needed for the case where `createNewCopies` mode is disabled and ambiguous source conflicts are detected. + // When `createNewCopies` mode is permanently enabled, this field will be removed, and this assertion will be redundant and can be + // removed too. + const createNewCopy = object!.createNewCopy as boolean | undefined; + if (successParam === 'createNewCopy') { + expect(createNewCopy).to.be(true); + } else { + expect(createNewCopy).to.be(undefined); + } + + if (!singleRequest || overwrite || createNewCopies) { + const { _source } = await expectResponses.successCreated( + es, + spaceId, + type, + destinationId ?? id + ); + expect(_source[type][NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); + } } for (let i = 0; i < expectedFailures.length; i++) { - const { type, id, failure } = expectedFailures[i]; + const { type, id, failure, expectedNewId } = expectedFailures[i]; // we don't know the order of the returned errors; search for each one const object = (errors as Array>).find( (x) => x.type === type && x.id === id @@ -81,7 +174,10 @@ export function resolveImportErrorsTestSuiteFactory( expect(object!.error).to.eql({ type: 'unsupported_type' }); } else { // 409 - expect(object!.error).to.eql({ type: 'conflict' }); + expect(object!.error).to.eql({ + type: 'conflict', + ...(expectedNewId && { destinationId: expectedNewId }), + }); } } } @@ -89,8 +185,9 @@ export function resolveImportErrorsTestSuiteFactory( const createTestDefinitions = ( testCases: ResolveImportErrorsTestCase | ResolveImportErrorsTestCase[], forbidden: boolean, - overwrite: boolean, - options?: { + options: { + overwrite?: boolean; + createNewCopies?: boolean; spaceId?: string; singleRequest?: boolean; responseBodyOverride?: ExpectResponseBody; @@ -98,29 +195,43 @@ export function resolveImportErrorsTestSuiteFactory( ): ResolveImportErrorsTestDefinition[] => { const cases = Array.isArray(testCases) ? testCases : [testCases]; const responseStatusCode = forbidden ? 403 : 200; - if (!options?.singleRequest) { + const { + overwrite = false, + createNewCopies = false, + spaceId, + singleRequest, + responseBodyOverride, + } = options; + if (!singleRequest) { // if we are testing cases that should result in a forbidden response, we can do each case individually // this ensures that multiple test cases of a single type will each result in a forbidden error return cases.map((x) => ({ title: getTestTitle(x, responseStatusCode), - request: [createRequest(x)], + request: createRequest(x, overwrite), responseStatusCode, responseBody: - options?.responseBodyOverride || - expectResponseBody(x, responseStatusCode, options?.spaceId), + responseBodyOverride || + expectResponseBody(x, responseStatusCode, false, overwrite, createNewCopies, spaceId), overwrite, + createNewCopies, })); } // batch into a single request to save time during test execution return [ { title: getTestTitle(cases, responseStatusCode), - request: cases.map((x) => createRequest(x)), + request: cases + .map((x) => createRequest(x, overwrite)) + .reduce((acc, cur) => ({ + objects: [...acc.objects, ...cur.objects], + retries: [...acc.retries, ...cur.retries], + })), responseStatusCode, responseBody: - options?.responseBodyOverride || - expectResponseBody(cases, responseStatusCode, options?.spaceId), + responseBodyOverride || + expectResponseBody(cases, responseStatusCode, true, overwrite, createNewCopies, spaceId), overwrite, + createNewCopies, }, ]; }; @@ -139,17 +250,14 @@ export function resolveImportErrorsTestSuiteFactory( for (const test of tests) { it(`should return ${test.responseStatusCode} ${test.title}`, async () => { - const retryAttrs = test.overwrite ? { overwrite: true } : {}; - const retries = JSON.stringify( - test.request.map(({ type, id }) => ({ type, id, ...retryAttrs })) - ); - const requestBody = test.request + const requestBody = test.request.objects .map((obj) => JSON.stringify({ ...obj, ...attrs })) .join('\n'); + const query = test.createNewCopies ? '?createNewCopies=true' : ''; await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_errors`) + .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_errors${query}`) .auth(user?.username, user?.password) - .field('retries', retries) + .field('retries', JSON.stringify(test.request.retries)) .attach('file', Buffer.from(requestBody, 'utf8'), 'export.ndjson') .expect(test.responseStatusCode) .then(test.responseBody); diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts index d83f3449460ce1..0cc5969e2b7ab0 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts @@ -20,6 +20,8 @@ const { SPACE_2: { spaceId: SPACE_2_ID }, } = SPACES; const { fail400, fail409 } = testCaseFailures; +const unresolvableConflict = (condition?: boolean) => + condition !== false ? { fail409Param: 'unresolvableConflict' } : {}; const createTestCases = (overwrite: boolean, spaceId: string) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect @@ -34,9 +36,18 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), + ...unresolvableConflict(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, + ...fail409(!overwrite || spaceId !== SPACE_1_ID), + ...unresolvableConflict(spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, + ...fail409(!overwrite || spaceId !== SPACE_2_ID), + ...unresolvableConflict(spaceId !== SPACE_2_ID), }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, CASES.NEW_SINGLE_NAMESPACE_OBJ, CASES.NEW_MULTI_NAMESPACE_OBJ, diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts index f85cd3a36c0920..c581a1757565e7 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts @@ -18,15 +18,12 @@ const createTestCases = (spaceId: string) => { const exportableTypes = [ cases.singleNamespaceObject, cases.singleNamespaceType, - cases.namespaceAgnosticObject, - cases.namespaceAgnosticType, - ]; - const nonExportableTypes = [ cases.multiNamespaceObject, cases.multiNamespaceType, - cases.hiddenObject, - cases.hiddenType, + cases.namespaceAgnosticObject, + cases.namespaceAgnosticType, ]; + const nonExportableTypes = [cases.hiddenObject, cases.hiddenType]; const allTypes = exportableTypes.concat(nonExportableTypes); return { exportableTypes, nonExportableTypes, allTypes }; }; diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts index 6b4dfe1d05f721..0b531a3dccc1ab 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts @@ -20,27 +20,78 @@ const { SPACE_2: { spaceId: SPACE_2_ID }, } = SPACES; const { fail400, fail409 } = testCaseFailures; +const destinationId = (condition?: boolean) => + condition !== false ? { successParam: 'destinationId' } : {}; +const newCopy = () => ({ successParam: 'createNewCopy' }); +const ambiguousConflict = (suffix: string) => ({ + failure: 409 as 409, + fail409Param: `ambiguous_conflict_${suffix}`, +}); -const createTestCases = (spaceId: string) => { +const createNewCopiesTestCases = () => { + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); + const importable = cases.map(([, val]) => ({ ...val, successParam: 'createNewCopies' })); + const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; + const all = [...importable, ...nonImportable]; + return { importable, nonImportable, all }; +}; + +const createTestCases = (overwrite: boolean, spaceId: string) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result - const importableTypes = [ - { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(spaceId === DEFAULT_SPACE_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(spaceId === SPACE_1_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(spaceId === SPACE_2_ID) }, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409() }, + const group1Importable = [ + // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, CASES.NEW_SINGLE_NAMESPACE_OBJ, CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, ]; - const nonImportableTypes = [ - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, - { ...CASES.HIDDEN, ...fail400() }, - { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; + const group1All = group1Importable.concat(group1NonImportable); + const group2 = [ + // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes + CASES.NEW_MULTI_NAMESPACE_OBJ, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)), + ...destinationId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, + ...fail409(!overwrite && spaceId === SPACE_1_ID), + ...destinationId(spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, + ...fail409(!overwrite && spaceId === SPACE_2_ID), + ...destinationId(spaceId !== SPACE_2_ID), + }, + { ...CASES.CONFLICT_1A_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1B_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict + { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict + ]; + const group3 = [ + // when overwrite=true, all of the objects in this group are errors, so we cannot check the created object attributes + // grouping errors together simplifies the test suite code + { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict + ]; + const group4 = [ + // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes + { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict + CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match + CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match + { ...CASES.CONFLICT_2C_OBJ, ...newCopy() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_2D_OBJ, ...newCopy() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID ]; - const allTypes = importableTypes.concat(nonImportableTypes); - return { importableTypes, nonImportableTypes, allTypes }; + return { group1Importable, group1NonImportable, group1All, group2, group3, group4 }; }; export default function ({ getService }: FtrProviderContext) { @@ -53,27 +104,77 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (spaceId: string) => { - const { importableTypes, nonImportableTypes, allTypes } = createTestCases(spaceId); - // use singleRequest to reduce execution time and/or test combined cases + const createTests = (overwrite: boolean, createNewCopies: boolean, spaceId: string) => { + const singleRequest = true; + + if (createNewCopies) { + const { importable, nonImportable, all } = createNewCopiesTestCases(); + return { + unauthorized: [ + createTestDefinitions(importable, true, { createNewCopies, spaceId }), + createTestDefinitions(nonImportable, false, { createNewCopies, spaceId, singleRequest }), + createTestDefinitions(all, true, { + createNewCopies, + spaceId, + singleRequest, + responseBodyOverride: expectForbidden([ + 'dashboard', + 'globaltype', + 'isolatedtype', + 'sharedtype', + ]), + }), + ].flat(), + authorized: createTestDefinitions(all, false, { createNewCopies, spaceId, singleRequest }), + }; + } + + const { + group1Importable, + group1NonImportable, + group1All, + group2, + group3, + group4, + } = createTestCases(overwrite, spaceId); return { unauthorized: [ - createTestDefinitions(importableTypes, true, { spaceId }), - createTestDefinitions(nonImportableTypes, false, { spaceId, singleRequest: true }), - createTestDefinitions(allTypes, true, { + createTestDefinitions(group1Importable, true, { overwrite, spaceId }), + createTestDefinitions(group1NonImportable, false, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group1All, true, { + overwrite, spaceId, - singleRequest: true, + singleRequest, responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']), }), + createTestDefinitions(group2, true, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group3, true, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group4, true, { overwrite, spaceId, singleRequest }), + ].flat(), + authorized: [ + createTestDefinitions(group1All, false, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group2, false, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group3, false, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group4, false, { overwrite, spaceId, singleRequest }), ].flat(), - authorized: createTestDefinitions(allTypes, false, { spaceId, singleRequest: true }), }; }; describe('_import', () => { - getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { - const suffix = ` within the ${spaceId} space`; - const { unauthorized, authorized } = createTests(spaceId); + getTestScenarios([ + [false, false], + [false, true], + [true, false], + ]).securityAndSpaces.forEach(({ spaceId, users, modifier }) => { + const [overwrite, createNewCopies] = modifier!; + const suffix = ` within the ${spaceId} space${ + overwrite + ? ' with overwrite enabled' + : createNewCopies + ? ' with createNewCopies enabled' + : '' + }`; + const { unauthorized, authorized } = createTests(overwrite, createNewCopies, spaceId); const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, spaceId, tests }); }; diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts index 8c16e298c7df98..792fe63e5932d7 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { v4 as uuidv4 } from 'uuid'; import { SPACES } from '../../common/lib/spaces'; import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { TestUser } from '../../common/lib/types'; @@ -20,30 +21,65 @@ const { SPACE_2: { spaceId: SPACE_2_ID }, } = SPACES; const { fail400, fail409 } = testCaseFailures; +const destinationId = (condition?: boolean) => + condition !== false ? { successParam: 'destinationId' } : {}; +const newCopy = () => ({ successParam: 'createNewCopy' }); + +const createNewCopiesTestCases = () => { + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); + const importable = cases.map(([, val]) => ({ + ...val, + successParam: 'createNewCopies', + expectedNewId: uuidv4(), + })); + const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; + const all = [...importable, ...nonImportable]; + return { importable, nonImportable, all }; +}; const createTestCases = (overwrite: boolean, spaceId: string) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result - const importableTypes = [ - { - ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, - ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), - }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, + const singleNamespaceObject = + spaceId === DEFAULT_SPACE_ID + ? CASES.SINGLE_NAMESPACE_DEFAULT_SPACE + : spaceId === SPACE_1_ID + ? CASES.SINGLE_NAMESPACE_SPACE_1 + : CASES.SINGLE_NAMESPACE_SPACE_2; + const group1Importable = [ + { ...singleNamespaceObject, ...fail409(!overwrite) }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, ]; - const nonImportableTypes = [ - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, - { ...CASES.HIDDEN, ...fail400() }, - { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; + const group1All = [...group1Importable, ...group1NonImportable]; + const group2 = [ + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)), + ...destinationId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, + ...fail409(!overwrite && spaceId === SPACE_1_ID), + ...destinationId(spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, + ...fail409(!overwrite && spaceId === SPACE_2_ID), + ...destinationId(spaceId !== SPACE_2_ID), + }, + { ...CASES.CONFLICT_1A_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1B_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict + // if we call _resolve_import_errors and don't specify overwrite, each of these will result in a conflict because an object with that + // `expectedDestinationId` already exists + { ...CASES.CONFLICT_2C_OBJ, ...fail409(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a' + { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' + { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' ]; - const allTypes = importableTypes.concat(nonImportableTypes); - return { importableTypes, nonImportableTypes, allTypes }; + return { group1Importable, group1NonImportable, group1All, group2 }; }; export default function ({ getService }: FtrProviderContext) { @@ -56,47 +92,82 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean, spaceId: string) => { - const { importableTypes, nonImportableTypes, allTypes } = createTestCases(overwrite, spaceId); - const singleRequest = true; + const createTests = (overwrite: boolean, createNewCopies: boolean, spaceId: string) => { // use singleRequest to reduce execution time and/or test combined cases + const singleRequest = true; + + if (createNewCopies) { + const { importable, nonImportable, all } = createNewCopiesTestCases(); + return { + unauthorized: [ + createTestDefinitions(importable, true, { createNewCopies, spaceId }), + createTestDefinitions(nonImportable, false, { createNewCopies, spaceId, singleRequest }), + createTestDefinitions(all, true, { + createNewCopies, + spaceId, + singleRequest, + responseBodyOverride: expectForbidden(['globaltype', 'isolatedtype', 'sharedtype']), + }), + ].flat(), + authorized: createTestDefinitions(all, false, { createNewCopies, spaceId, singleRequest }), + }; + } + + const { group1Importable, group1NonImportable, group1All, group2 } = createTestCases( + overwrite, + spaceId + ); return { unauthorized: [ - createTestDefinitions(importableTypes, true, overwrite, { spaceId }), - createTestDefinitions(nonImportableTypes, false, overwrite, { spaceId, singleRequest }), - createTestDefinitions(allTypes, true, overwrite, { + createTestDefinitions(group1Importable, true, { overwrite, spaceId }), + createTestDefinitions(group1NonImportable, false, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group1All, true, { + overwrite, spaceId, singleRequest, - responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']), + responseBodyOverride: expectForbidden(['globaltype', 'isolatedtype']), }), + createTestDefinitions(group2, true, { overwrite, spaceId, singleRequest }), + ].flat(), + authorized: [ + createTestDefinitions(group1All, false, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group2, false, { overwrite, spaceId, singleRequest }), ].flat(), - authorized: createTestDefinitions(allTypes, false, overwrite, { spaceId, singleRequest }), }; }; describe('_resolve_import_errors', () => { - getTestScenarios([false, true]).securityAndSpaces.forEach( - ({ spaceId, users, modifier: overwrite }) => { - const suffix = ` within the ${spaceId} space${overwrite ? ' with overwrite enabled' : ''}`; - const { unauthorized, authorized } = createTests(overwrite!, spaceId); - const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => { - addTests(`${user.description}${suffix}`, { user, spaceId, tests }); - }; + getTestScenarios([ + [false, false], + [false, true], + [true, false], + ]).securityAndSpaces.forEach(({ spaceId, users, modifier }) => { + const [overwrite, createNewCopies] = modifier!; + const suffix = ` within the ${spaceId} space${ + overwrite + ? ' with overwrite enabled' + : createNewCopies + ? ' with createNewCopies enabled' + : '' + }`; + const { unauthorized, authorized } = createTests(overwrite, createNewCopies, spaceId); + const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - [ - users.noAccess, - users.legacyAll, - users.dualRead, - users.readGlobally, - users.readAtSpace, - users.allAtOtherSpace, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - [users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach((user) => { - _addTests(user, authorized); - }); - } - ); + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach((user) => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach((user) => { + _addTests(user, authorized); + }); + }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts index 464a5a1e760163..725120687c2313 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts @@ -14,6 +14,7 @@ import { } from '../../common/suites/bulk_create'; const { fail400, fail409 } = testCaseFailures; +const unresolvableConflict = () => ({ fail409Param: 'unresolvableConflict' }); const createTestCases = (overwrite: boolean) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect @@ -23,8 +24,8 @@ const createTestCases = (overwrite: boolean) => { CASES.SINGLE_NAMESPACE_SPACE_1, CASES.SINGLE_NAMESPACE_SPACE_2, { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(), ...unresolvableConflict() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(), ...unresolvableConflict() }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, CASES.NEW_SINGLE_NAMESPACE_OBJ, CASES.NEW_MULTI_NAMESPACE_OBJ, diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/export.ts b/x-pack/test/saved_object_api_integration/security_only/apis/export.ts index 61ff6eeb4bd80f..99babf683ccfa2 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/export.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/export.ts @@ -18,15 +18,12 @@ const createTestCases = () => { const exportableTypes = [ cases.singleNamespaceObject, cases.singleNamespaceType, - cases.namespaceAgnosticObject, - cases.namespaceAgnosticType, - ]; - const nonExportableTypes = [ cases.multiNamespaceObject, cases.multiNamespaceType, - cases.hiddenObject, - cases.hiddenType, + cases.namespaceAgnosticObject, + cases.namespaceAgnosticType, ]; + const nonExportableTypes = [cases.hiddenObject, cases.hiddenType]; const allTypes = exportableTypes.concat(nonExportableTypes); return { exportableTypes, nonExportableTypes, allTypes }; }; diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts index beec276b3bd73c..34be3b7408432a 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts @@ -14,27 +14,63 @@ import { } from '../../common/suites/import'; const { fail400, fail409 } = testCaseFailures; +const destinationId = (condition?: boolean) => + condition !== false ? { successParam: 'destinationId' } : {}; +const newCopy = () => ({ successParam: 'createNewCopy' }); +const ambiguousConflict = (suffix: string) => ({ + failure: 409 as 409, + fail409Param: `ambiguous_conflict_${suffix}`, +}); -const createTestCases = () => { +const createNewCopiesTestCases = () => { + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); + const importable = cases.map(([, val]) => ({ ...val, successParam: 'createNewCopies' })); + const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; + const all = [...importable, ...nonImportable]; + return { importable, nonImportable, all }; +}; + +const createTestCases = (overwrite: boolean) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result - const importableTypes = [ - { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409() }, + const group1Importable = [ + // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, CASES.SINGLE_NAMESPACE_SPACE_1, CASES.SINGLE_NAMESPACE_SPACE_2, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409() }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, CASES.NEW_SINGLE_NAMESPACE_OBJ, CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, ]; - const nonImportableTypes = [ - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, - { ...CASES.HIDDEN, ...fail400() }, - { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; + const group1All = group1Importable.concat(group1NonImportable); + const group2 = [ + // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes + CASES.NEW_MULTI_NAMESPACE_OBJ, + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...destinationId() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...destinationId() }, + { ...CASES.CONFLICT_1A_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1B_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict + { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict + ]; + const group3 = [ + // when overwrite=true, all of the objects in this group are errors, so we cannot check the created object attributes + // grouping errors together simplifies the test suite code + { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict + ]; + const group4 = [ + // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes + { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict + CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match + CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match + { ...CASES.CONFLICT_2C_OBJ, ...newCopy() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_2D_OBJ, ...newCopy() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID ]; - const allTypes = importableTypes.concat(nonImportableTypes); - return { importableTypes, nonImportableTypes, allTypes }; + return { group1Importable, group1NonImportable, group1All, group2, group3, group4 }; }; export default function ({ getService }: FtrProviderContext) { @@ -47,27 +83,76 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = () => { - const { importableTypes, nonImportableTypes, allTypes } = createTestCases(); + const createTests = (overwrite: boolean, createNewCopies: boolean) => { // use singleRequest to reduce execution time and/or test combined cases + const singleRequest = true; + + if (createNewCopies) { + const { importable, nonImportable, all } = createNewCopiesTestCases(); + return { + unauthorized: [ + createTestDefinitions(importable, true, { createNewCopies }), + createTestDefinitions(nonImportable, false, { createNewCopies, singleRequest }), + createTestDefinitions(all, true, { + createNewCopies, + singleRequest, + responseBodyOverride: expectForbidden([ + 'dashboard', + 'globaltype', + 'isolatedtype', + 'sharedtype', + ]), + }), + ].flat(), + authorized: createTestDefinitions(all, false, { createNewCopies, singleRequest }), + }; + } + + const { + group1Importable, + group1NonImportable, + group1All, + group2, + group3, + group4, + } = createTestCases(overwrite); return { unauthorized: [ - createTestDefinitions(importableTypes, true), - createTestDefinitions(nonImportableTypes, false, { singleRequest: true }), - createTestDefinitions(allTypes, true, { - singleRequest: true, + createTestDefinitions(group1Importable, true, { overwrite }), + createTestDefinitions(group1NonImportable, false, { overwrite, singleRequest }), + createTestDefinitions(group1All, true, { + overwrite, + singleRequest, responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']), }), + createTestDefinitions(group2, true, { overwrite, singleRequest }), + createTestDefinitions(group3, true, { overwrite, singleRequest }), + createTestDefinitions(group4, true, { overwrite, singleRequest }), + ].flat(), + authorized: [ + createTestDefinitions(group1All, false, { overwrite, singleRequest }), + createTestDefinitions(group2, false, { overwrite, singleRequest }), + createTestDefinitions(group3, false, { overwrite, singleRequest }), + createTestDefinitions(group4, false, { overwrite, singleRequest }), ].flat(), - authorized: createTestDefinitions(allTypes, false, { singleRequest: true }), }; }; describe('_import', () => { - getTestScenarios().security.forEach(({ users }) => { - const { unauthorized, authorized } = createTests(); + getTestScenarios([ + [false, false], + [false, true], + [true, false], + ]).security.forEach(({ users, modifier }) => { + const [overwrite, createNewCopies] = modifier!; + const suffix = overwrite + ? ' with overwrite enabled' + : createNewCopies + ? ' with createNewCopies enabled' + : ''; + const { unauthorized, authorized } = createTests(overwrite, createNewCopies); const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => { - addTests(user.description, { user, tests }); + addTests(`${user.description}${suffix}`, { user, tests }); }; [ diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts index a0abe4b0483f82..91134dd14bd8a3 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { v4 as uuidv4 } from 'uuid'; import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -14,27 +15,45 @@ import { } from '../../common/suites/resolve_import_errors'; const { fail400, fail409 } = testCaseFailures; +const destinationId = (condition?: boolean) => + condition !== false ? { successParam: 'destinationId' } : {}; +const newCopy = () => ({ successParam: 'createNewCopy' }); + +const createNewCopiesTestCases = () => { + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); + const importable = cases.map(([, val]) => ({ + ...val, + successParam: 'createNewCopies', + expectedNewId: uuidv4(), + })); + const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; + const all = [...importable, ...nonImportable]; + return { importable, nonImportable, all }; +}; const createTestCases = (overwrite: boolean) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result - const importableTypes = [ + const group1Importable = [ { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, - CASES.SINGLE_NAMESPACE_SPACE_1, - CASES.SINGLE_NAMESPACE_SPACE_2, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, ]; - const nonImportableTypes = [ - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, - { ...CASES.HIDDEN, ...fail400() }, - { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }]; + const group1All = [...group1Importable, ...group1NonImportable]; + const group2 = [ + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, + { ...CASES.CONFLICT_1A_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1B_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict + // if we call _resolve_import_errors and don't specify overwrite, each of these will result in a conflict because an object with that + // `expectedDestinationId` already exists + { ...CASES.CONFLICT_2C_OBJ, ...fail409(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a' + { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' + { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' ]; - const allTypes = importableTypes.concat(nonImportableTypes); - return { importableTypes, nonImportableTypes, allTypes }; + return { group1Importable, group1NonImportable, group1All, group2 }; }; export default function ({ getService }: FtrProviderContext) { @@ -47,26 +66,58 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean) => { - const { importableTypes, nonImportableTypes, allTypes } = createTestCases(overwrite); + const createTests = (overwrite: boolean, createNewCopies: boolean) => { // use singleRequest to reduce execution time and/or test combined cases + const singleRequest = true; + + if (createNewCopies) { + const { importable, nonImportable, all } = createNewCopiesTestCases(); + return { + unauthorized: [ + createTestDefinitions(importable, true, { createNewCopies }), + createTestDefinitions(nonImportable, false, { createNewCopies, singleRequest }), + createTestDefinitions(all, true, { + createNewCopies, + singleRequest, + responseBodyOverride: expectForbidden(['globaltype', 'isolatedtype', 'sharedtype']), + }), + ].flat(), + authorized: createTestDefinitions(all, false, { createNewCopies, singleRequest }), + }; + } + + const { group1Importable, group1NonImportable, group1All, group2 } = createTestCases(overwrite); return { unauthorized: [ - createTestDefinitions(importableTypes, true, overwrite), - createTestDefinitions(nonImportableTypes, false, overwrite, { singleRequest: true }), - createTestDefinitions(allTypes, true, overwrite, { - singleRequest: true, - responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']), + createTestDefinitions(group1Importable, true, { overwrite }), + createTestDefinitions(group1NonImportable, false, { overwrite, singleRequest }), + createTestDefinitions(group1All, true, { + overwrite, + singleRequest, + responseBodyOverride: expectForbidden(['globaltype', 'isolatedtype']), }), + createTestDefinitions(group2, true, { overwrite, singleRequest }), + ].flat(), + authorized: [ + createTestDefinitions(group1All, false, { overwrite, singleRequest }), + createTestDefinitions(group2, false, { overwrite, singleRequest }), ].flat(), - authorized: createTestDefinitions(allTypes, false, overwrite, { singleRequest: true }), }; }; describe('_resolve_import_errors', () => { - getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => { - const suffix = overwrite ? ' with overwrite enabled' : ''; - const { unauthorized, authorized } = createTests(overwrite!); + getTestScenarios([ + [false, false], + [false, true], + [true, false], + ]).security.forEach(({ users, modifier }) => { + const [overwrite, createNewCopies] = modifier!; + const suffix = overwrite + ? ' with overwrite enabled' + : createNewCopies + ? ' with createNewCopies enabled' + : ''; + const { unauthorized, authorized } = createTests(overwrite, createNewCopies); const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, tests }); }; diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts index f9edc56b8ffea4..74fade39bf7a58 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts @@ -16,6 +16,8 @@ const { SPACE_2: { spaceId: SPACE_2_ID }, } = SPACES; const { fail400, fail409 } = testCaseFailures; +const unresolvableConflict = (condition?: boolean) => + condition !== false ? { fail409Param: 'unresolvableConflict' } : {}; const createTestCases = (overwrite: boolean, spaceId: string) => [ // for each outcome, if failure !== undefined then we expect to receive @@ -29,9 +31,18 @@ const createTestCases = (overwrite: boolean, spaceId: string) => [ { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), + ...unresolvableConflict(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, + ...fail409(!overwrite || spaceId !== SPACE_1_ID), + ...unresolvableConflict(spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, + ...fail409(!overwrite || spaceId !== SPACE_2_ID), + ...unresolvableConflict(spaceId !== SPACE_2_ID), }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, { ...CASES.HIDDEN, ...fail400() }, CASES.NEW_SINGLE_NAMESPACE_OBJ, diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts index 45a76a2f39e371..a36249528540b8 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts @@ -15,22 +15,75 @@ const { SPACE_2: { spaceId: SPACE_2_ID }, } = SPACES; const { fail400, fail409 } = testCaseFailures; +const destinationId = (condition?: boolean) => + condition !== false ? { successParam: 'destinationId' } : {}; +const newCopy = () => ({ successParam: 'createNewCopy' }); +const ambiguousConflict = (suffix: string) => ({ + failure: 409 as 409, + fail409Param: `ambiguous_conflict_${suffix}`, +}); -const createTestCases = (spaceId: string) => [ +const createNewCopiesTestCases = () => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result - { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(spaceId === DEFAULT_SPACE_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(spaceId === SPACE_1_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(spaceId === SPACE_2_ID) }, - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409() }, - { ...CASES.HIDDEN, ...fail400() }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, - CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, -]; + const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); + return [ + ...cases.map(([, val]) => ({ ...val, successParam: 'createNewCopies' })), + { ...CASES.HIDDEN, ...fail400() }, + ]; +}; + +const createTestCases = (overwrite: boolean, spaceId: string) => { + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + const group1 = [ + // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)), + ...destinationId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, + ...fail409(!overwrite && spaceId === SPACE_1_ID), + ...destinationId(spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, + ...fail409(!overwrite && spaceId === SPACE_2_ID), + ...destinationId(spaceId !== SPACE_2_ID), + }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + { ...CASES.HIDDEN, ...fail400() }, + { ...CASES.CONFLICT_1A_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1B_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict + { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_MULTI_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; + const group2 = [ + // when overwrite=true, all of the objects in this group are errors, so we cannot check the created object attributes + // grouping errors together simplifies the test suite code + { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict + ]; + const group3 = [ + // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes + { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict + CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match + CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match + { ...CASES.CONFLICT_2C_OBJ, ...newCopy() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_2D_OBJ, ...newCopy() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID + ]; + return { group1, group2, group3 }; +}; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -38,15 +91,35 @@ export default function ({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const { addTests, createTestDefinitions } = importTestSuiteFactory(es, esArchiver, supertest); - const createTests = (spaceId: string) => { - const testCases = createTestCases(spaceId); - return createTestDefinitions(testCases, false, { spaceId, singleRequest: true }); + const createTests = (overwrite: boolean, createNewCopies: boolean, spaceId: string) => { + const singleRequest = true; + if (createNewCopies) { + const cases = createNewCopiesTestCases(); + return createTestDefinitions(cases, false, { createNewCopies, spaceId, singleRequest }); + } + + const { group1, group2, group3 } = createTestCases(overwrite, spaceId); + return [ + createTestDefinitions(group1, false, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group2, false, { overwrite, spaceId, singleRequest }), + createTestDefinitions(group3, false, { overwrite, spaceId, singleRequest }), + ].flat(); }; describe('_import', () => { - getTestScenarios().spaces.forEach(({ spaceId }) => { - const tests = createTests(spaceId); - addTests(`within the ${spaceId} space`, { spaceId, tests }); + getTestScenarios([ + [false, false], + [false, true], + [true, false], + ]).spaces.forEach(({ spaceId, modifier }) => { + const [overwrite, createNewCopies] = modifier!; + const suffix = overwrite + ? ' with overwrite enabled' + : createNewCopies + ? ' with createNewCopies enabled' + : ''; + const tests = createTests(overwrite, createNewCopies, spaceId); + addTests(`within the ${spaceId} space${suffix}`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts index a6ef902e2e9ebb..1431a61b1cbe07 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { v4 as uuidv4 } from 'uuid'; import { SPACES } from '../../common/lib/spaces'; import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -18,25 +19,62 @@ const { SPACE_2: { spaceId: SPACE_2_ID }, } = SPACES; const { fail400, fail409 } = testCaseFailures; +const destinationId = (condition?: boolean) => + condition !== false ? { successParam: 'destinationId' } : {}; +const newCopy = () => ({ successParam: 'createNewCopy' }); -const createTestCases = (overwrite: boolean, spaceId: string) => [ +const createNewCopiesTestCases = () => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result - { - ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, - ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), - }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - { ...CASES.HIDDEN, ...fail400() }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, - CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, -]; + const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN'); + return [ + ...cases.map(([, val]) => ({ + ...val, + successParam: 'createNewCopies', + expectedNewId: uuidv4(), + })), + { ...CASES.HIDDEN, ...fail400() }, + ]; +}; + +const createTestCases = (overwrite: boolean, spaceId: string) => { + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + const singleNamespaceObject = + spaceId === DEFAULT_SPACE_ID + ? CASES.SINGLE_NAMESPACE_DEFAULT_SPACE + : spaceId === SPACE_1_ID + ? CASES.SINGLE_NAMESPACE_SPACE_1 + : CASES.SINGLE_NAMESPACE_SPACE_2; + return [ + { ...singleNamespaceObject, ...fail409(!overwrite) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)), + ...destinationId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, + ...fail409(!overwrite && spaceId === SPACE_1_ID), + ...destinationId(spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, + ...fail409(!overwrite && spaceId === SPACE_2_ID), + ...destinationId(spaceId !== SPACE_2_ID), + }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + { ...CASES.HIDDEN, ...fail400() }, + { ...CASES.CONFLICT_1A_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + { ...CASES.CONFLICT_1B_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID + // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict + // if we call _resolve_import_errors and don't specify overwrite, each of these will result in a conflict because an object with that + // `expectedDestinationId` already exists + { ...CASES.CONFLICT_2C_OBJ, ...fail409(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a' + { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3' + { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a' + ]; +}; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -48,15 +86,32 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean, spaceId: string) => { + const createTests = (overwrite: boolean, createNewCopies: boolean, spaceId: string) => { + const singleRequest = true; + if (createNewCopies) { + const cases = createNewCopiesTestCases(); + // The resolveImportErrors API doesn't actually have a flag for "createNewCopies" mode; rather, we create test cases as if we are resolving + // errors from a call to the import API that had createNewCopies mode enabled. + return createTestDefinitions(cases, false, { createNewCopies, spaceId, singleRequest }); + } + const testCases = createTestCases(overwrite, spaceId); - return createTestDefinitions(testCases, false, overwrite, { spaceId, singleRequest: true }); + return createTestDefinitions(testCases, false, { overwrite, spaceId, singleRequest }); }; describe('_resolve_import_errors', () => { - getTestScenarios([false, true]).spaces.forEach(({ spaceId, modifier: overwrite }) => { - const suffix = overwrite ? ' with overwrite enabled' : ''; - const tests = createTests(overwrite!, spaceId); + getTestScenarios([ + [false, false], + [false, true], + [true, false], + ]).spaces.forEach(({ spaceId, modifier }) => { + const [overwrite, createNewCopies] = modifier!; + const suffix = overwrite + ? ' with overwrite enabled' + : createNewCopies + ? ' with createNewCopies enabled' + : ''; + const tests = createTests(overwrite, createNewCopies, spaceId); addTests(`within the ${spaceId} space${suffix}`, { spaceId, tests }); }); }); diff --git a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json index 9a8a0a1fdda141..7e528c23c20a05 100644 --- a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json +++ b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -380,11 +380,11 @@ { "type": "doc", "value": { - "id": "sharedtype:default_space_only", + "id": "sharedtype:default_only", "index": ".kibana", "source": { "sharedtype": { - "title": "A shared saved-object in the default space" + "title": "A shared saved-object in one space" }, "type": "sharedtype", "namespaces": ["default"], @@ -401,7 +401,7 @@ "index": ".kibana", "source": { "sharedtype": { - "title": "A shared saved-object in the space_1 space" + "title": "A shared saved-object in one space" }, "type": "sharedtype", "namespaces": ["space_1"], @@ -418,7 +418,7 @@ "index": ".kibana", "source": { "sharedtype": { - "title": "A shared saved-object in the space_2 space" + "title": "A shared saved-object in one space" }, "type": "sharedtype", "namespaces": ["space_2"], @@ -496,3 +496,128 @@ } } +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_1_default", + "index": ".kibana", + "source": { + "originId": "conflict_1", + "sharedtype": { + "title": "A shared saved-object in one space" + }, + "type": "sharedtype", + "namespaces": ["default"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_1_space_1", + "index": ".kibana", + "source": { + "originId": "conflict_1", + "sharedtype": { + "title": "A shared saved-object in one space" + }, + "type": "sharedtype", + "namespaces": ["space_1"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_1_space_2", + "index": ".kibana", + "source": { + "originId": "conflict_1", + "sharedtype": { + "title": "A shared saved-object in one space" + }, + "type": "sharedtype", + "namespaces": ["space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_2_default", + "index": ".kibana", + "source": { + "originId": "conflict_2", + "sharedtype": { + "title": "A shared saved-object in one space" + }, + "type": "sharedtype", + "namespaces": ["default"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_2_space_1", + "index": ".kibana", + "source": { + "originId": "conflict_2", + "sharedtype": { + "title": "A shared saved-object in one space" + }, + "type": "sharedtype", + "namespaces": ["space_1"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_2_space_2", + "index": ".kibana", + "source": { + "originId": "conflict_2", + "sharedtype": { + "title": "A shared saved-object in one space" + }, + "type": "sharedtype", + "namespaces": ["space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:conflict_2_all", + "index": ".kibana", + "source": { + "originId": "conflict_2", + "sharedtype": { + "title": "A shared saved-object in all spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_1", "space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} diff --git a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json index 508de68c32f706..a2f8088ce04360 100644 --- a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json +++ b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json @@ -162,6 +162,9 @@ "namespaces": { "type": "keyword" }, + "originId": { + "type": "keyword" + }, "search": { "properties": { "columns": { diff --git a/x-pack/test/spaces_api_integration/common/fixtures/spaces_test_plugin/server/plugin.ts b/x-pack/test/spaces_api_integration/common/fixtures/spaces_test_plugin/server/plugin.ts index ee03fa6b648af8..0e63e1bc19954a 100644 --- a/x-pack/test/spaces_api_integration/common/fixtures/spaces_test_plugin/server/plugin.ts +++ b/x-pack/test/spaces_api_integration/common/fixtures/spaces_test_plugin/server/plugin.ts @@ -15,6 +15,13 @@ export class Plugin { name: 'sharedtype', hidden: false, namespaceType: 'multiple', + management: { + icon: 'beaker', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + }, mappings: { properties: { title: { type: 'text' }, diff --git a/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts b/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts index 67f5d737ba010b..3b0f5f8570aa32 100644 --- a/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts +++ b/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts @@ -5,8 +5,8 @@ */ export const MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES = Object.freeze({ - DEFAULT_SPACE_ONLY: Object.freeze({ - id: 'default_space_only', + DEFAULT_ONLY: Object.freeze({ + id: 'default_only', existingNamespaces: ['default'], }), SPACE_1_ONLY: Object.freeze({ diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index 2dd4484ffcde82..26c736034501f7 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -19,6 +19,11 @@ interface CopyToSpaceTest { response: (resp: TestResponse) => Promise; } +interface CopyToSpaceMultiNamespaceTest extends CopyToSpaceTest { + testTitle: string; + objects: Array>; +} + interface CopyToSpaceTests { noConflictsWithoutReferences: CopyToSpaceTest; noConflictsWithReferences: CopyToSpaceTest; @@ -30,6 +35,7 @@ interface CopyToSpaceTests { withConflictsResponse: (resp: TestResponse) => Promise; noConflictsResponse: (resp: TestResponse) => Promise; }; + multiNamespaceTestCases: (overwrite: boolean) => CopyToSpaceMultiNamespaceTest[]; } interface CopyToSpaceTestDefinition { @@ -53,28 +59,14 @@ interface SpaceBucket { } const INITIAL_COUNTS: Record> = { - [DEFAULT_SPACE_ID]: { - dashboard: 2, - visualization: 3, - 'index-pattern': 1, - }, - space_1: { - dashboard: 2, - visualization: 3, - 'index-pattern': 1, - }, - space_2: { - dashboard: 1, - }, + [DEFAULT_SPACE_ID]: { dashboard: 2, visualization: 3, 'index-pattern': 1 }, + space_1: { dashboard: 2, visualization: 3, 'index-pattern': 1 }, + space_2: { dashboard: 1 }, }; const getDestinationWithoutConflicts = () => 'space_2'; -const getDestinationWithConflicts = (originSpaceId?: string) => { - if (!originSpaceId || originSpaceId === DEFAULT_SPACE_ID) { - return 'space_1'; - } - return DEFAULT_SPACE_ID; -}; +const getDestinationWithConflicts = (originSpaceId?: string) => + !originSpaceId || originSpaceId === DEFAULT_SPACE_ID ? 'space_1' : DEFAULT_SPACE_ID; export function copyToSpaceTestSuiteFactory( es: any, @@ -86,27 +78,11 @@ export function copyToSpaceTestSuiteFactory( index: '.kibana', body: { size: 0, - query: { - terms: { - type: ['visualization', 'dashboard', 'index-pattern'], - }, - }, + query: { terms: { type: ['visualization', 'dashboard', 'index-pattern'] } }, aggs: { count: { - terms: { - field: 'namespace', - missing: DEFAULT_SPACE_ID, - size: 10, - }, - aggs: { - countByType: { - terms: { - field: 'type', - missing: 'UNKNOWN', - size: 10, - }, - }, - }, + terms: { field: 'namespace', missing: DEFAULT_SPACE_ID, size: 10 }, + aggs: { countByType: { terms: { field: 'type', missing: 'UNKNOWN', size: 10 } } }, }, }, }, @@ -135,13 +111,7 @@ export function copyToSpaceTestSuiteFactory( const { countByType } = spaceBucket; const expectedBuckets = Object.entries(expectedCounts).reduce((acc, entry) => { const [type, count] = entry; - return [ - ...acc, - { - key: type, - doc_count: count, - }, - ]; + return [...acc, { key: type, doc_count: count }]; }, [] as CountByTypeBucket[]); expectedBuckets.sort(bucketSorter); @@ -154,14 +124,6 @@ export function copyToSpaceTestSuiteFactory( }); }; - const expectRbacForbiddenResponse = async (resp: TestResponse) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: 'Unable to bulk_get dashboard', - }); - }; - const expectNotFoundResponse = async (resp: TestResponse) => { expect(resp.body).to.eql({ statusCode: 404, @@ -172,39 +134,81 @@ export function copyToSpaceTestSuiteFactory( const createExpectNoConflictsWithoutReferencesForSpace = ( spaceId: string, + destination: string, expectedDashboardCount: number ) => async (resp: TestResponse) => { const result = resp.body as CopyResponse; expect(result).to.eql({ - [spaceId]: { + [destination]: { success: true, successCount: 1, + successResults: [ + { + id: 'cts_dashboard', + type: 'dashboard', + meta: { + title: `This is the ${spaceId} test space CTS dashboard`, + icon: 'dashboardApp', + }, + }, + ], }, } as CopyResponse); // Query ES to ensure that we copied everything we expected - await assertSpaceCounts(spaceId, { + await assertSpaceCounts(destination, { dashboard: expectedDashboardCount, }); }; - const expectNoConflictsWithoutReferencesResult = createExpectNoConflictsWithoutReferencesForSpace( - getDestinationWithoutConflicts(), - 2 - ); + const expectNoConflictsWithoutReferencesResult = (spaceId: string = DEFAULT_SPACE_ID) => + createExpectNoConflictsWithoutReferencesForSpace(spaceId, getDestinationWithoutConflicts(), 2); - const expectNoConflictsForNonExistentSpaceResult = createExpectNoConflictsWithoutReferencesForSpace( - 'non_existent_space', - 1 - ); + const expectNoConflictsForNonExistentSpaceResult = (spaceId: string = DEFAULT_SPACE_ID) => + createExpectNoConflictsWithoutReferencesForSpace(spaceId, 'non_existent_space', 1); - const expectNoConflictsWithReferencesResult = async (resp: TestResponse) => { + const expectNoConflictsWithReferencesResult = (spaceId: string = DEFAULT_SPACE_ID) => async ( + resp: TestResponse + ) => { const destination = getDestinationWithoutConflicts(); const result = resp.body as CopyResponse; expect(result).to.eql({ [destination]: { success: true, successCount: 5, + successResults: [ + { + id: 'cts_ip_1', + type: 'index-pattern', + meta: { + icon: 'indexPatternApp', + title: `Copy to Space index pattern 1 from ${spaceId} space`, + }, + }, + { + id: `cts_vis_1_${spaceId}`, + type: 'visualization', + meta: { icon: 'visualizeApp', title: `CTS vis 1 from ${spaceId} space` }, + }, + { + id: `cts_vis_2_${spaceId}`, + type: 'visualization', + meta: { icon: 'visualizeApp', title: `CTS vis 2 from ${spaceId} space` }, + }, + { + id: 'cts_vis_3', + type: 'visualization', + meta: { icon: 'visualizeApp', title: `CTS vis 3 from ${spaceId} space` }, + }, + { + id: 'cts_dashboard', + type: 'dashboard', + meta: { + icon: 'dashboardApp', + title: `This is the ${spaceId} test space CTS dashboard`, + }, + }, + ], }, } as CopyResponse); @@ -288,6 +292,42 @@ export function copyToSpaceTestSuiteFactory( [destination]: { success: true, successCount: 5, + successResults: [ + { + id: 'cts_ip_1', + type: 'index-pattern', + meta: { + icon: 'indexPatternApp', + title: `Copy to Space index pattern 1 from ${spaceId} space`, + }, + overwrite: true, + }, + { + id: `cts_vis_1_${spaceId}`, + type: 'visualization', + meta: { icon: 'visualizeApp', title: `CTS vis 1 from ${spaceId} space` }, + }, + { + id: `cts_vis_2_${spaceId}`, + type: 'visualization', + meta: { icon: 'visualizeApp', title: `CTS vis 2 from ${spaceId} space` }, + }, + { + id: 'cts_vis_3', + type: 'visualization', + meta: { icon: 'visualizeApp', title: `CTS vis 3 from ${spaceId} space` }, + overwrite: true, + }, + { + id: 'cts_dashboard', + type: 'dashboard', + meta: { + icon: 'dashboardApp', + title: `This is the ${spaceId} test space CTS dashboard`, + }, + overwrite: true, + }, + ], }, } as CopyResponse); @@ -309,30 +349,48 @@ export function copyToSpaceTestSuiteFactory( const result = resp.body as CopyResponse; result[destination].errors!.sort(errorSorter); + const expectedSuccessResults = [ + { + id: `cts_vis_1_${spaceId}`, + type: 'visualization', + meta: { icon: 'visualizeApp', title: `CTS vis 1 from ${spaceId} space` }, + }, + { + id: `cts_vis_2_${spaceId}`, + type: 'visualization', + meta: { icon: 'visualizeApp', title: `CTS vis 2 from ${spaceId} space` }, + }, + ]; const expectedErrors = [ { - error: { - type: 'conflict', - }, + error: { type: 'conflict' }, id: 'cts_dashboard', title: `This is the ${spaceId} test space CTS dashboard`, type: 'dashboard', + meta: { + title: `This is the ${spaceId} test space CTS dashboard`, + icon: 'dashboardApp', + }, }, { - error: { - type: 'conflict', - }, + error: { type: 'conflict' }, id: 'cts_ip_1', title: `Copy to Space index pattern 1 from ${spaceId} space`, type: 'index-pattern', + meta: { + title: `Copy to Space index pattern 1 from ${spaceId} space`, + icon: 'indexPatternApp', + }, }, { - error: { - type: 'conflict', - }, + error: { type: 'conflict' }, id: 'cts_vis_3', title: `CTS vis 3 from ${spaceId} space`, type: 'visualization', + meta: { + title: `CTS vis 3 from ${spaceId} space`, + icon: 'visualizeApp', + }, }, ]; expectedErrors.sort(errorSorter); @@ -341,16 +399,176 @@ export function copyToSpaceTestSuiteFactory( [destination]: { success: false, successCount: 2, + successResults: expectedSuccessResults, errors: expectedErrors, }, } as CopyResponse); - // Query ES to ensure that we copied everything we expected - await assertSpaceCounts(destination, { - dashboard: 2, - visualization: 5, - 'index-pattern': 1, - }); + // Query ES to ensure that no objects were created + await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); + }; + + /** + * Creates test cases for multi-namespace saved object types. + * Note: these are written with the assumption that test data will only be reloaded between each group of test cases, *not* before every + * single test case. This saves time during test execution. + */ + const createMultiNamespaceTestCases = ( + spaceId: string, + outcome: 'authorized' | 'unauthorizedRead' | 'unauthorizedWrite' | 'noAccess' = 'authorized' + ) => (overwrite: boolean): CopyToSpaceMultiNamespaceTest[] => { + // the status code of the HTTP response differs depending on the error type + // a 403 error actually comes back as an HTTP 200 response + const statusCode = outcome === 'noAccess' ? 404 : 200; + const type = 'sharedtype'; + const v4 = new RegExp(/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i); + const noConflictId = `${spaceId}_only`; + const exactMatchId = 'all_spaces'; + const inexactMatchId = `conflict_1_${spaceId}`; + const ambiguousConflictId = `conflict_2_${spaceId}`; + + const getResult = (response: TestResponse) => (response.body as CopyResponse).space_2; + const expectForbiddenResponse = (response: TestResponse) => { + expect(response.body).to.eql({ + space_2: { + success: false, + successCount: 0, + errors: [ + { statusCode: 403, error: 'Forbidden', message: `Unable to bulk_create sharedtype` }, + ], + }, + }); + }; + + return [ + { + testTitle: 'copying with no conflict', + objects: [{ type, id: noConflictId }], + statusCode, + response: async (response: TestResponse) => { + if (outcome === 'authorized') { + const { success, successCount, successResults, errors } = getResult(response); + expect(success).to.eql(true); + expect(successCount).to.eql(1); + const destinationId = successResults![0].destinationId; + expect(destinationId).to.match(v4); + const meta = { title: 'A shared saved-object in one space', icon: 'beaker' }; + expect(successResults).to.eql([{ type, id: noConflictId, meta, destinationId }]); + expect(errors).to.be(undefined); + } else if (outcome === 'noAccess') { + expectNotFoundResponse(response); + } else { + // unauthorized read/write + expectForbiddenResponse(response); + } + }, + }, + { + testTitle: 'copying with an exact match conflict', + objects: [{ type, id: exactMatchId }], + statusCode, + response: async (response: TestResponse) => { + if (outcome === 'authorized') { + const { success, successCount, successResults, errors } = getResult(response); + const title = 'A shared saved-object in the default, space_1, and space_2 spaces'; + const meta = { title, icon: 'beaker' }; + if (overwrite) { + expect(success).to.eql(true); + expect(successCount).to.eql(1); + expect(successResults).to.eql([{ type, id: exactMatchId, meta, overwrite: true }]); + expect(errors).to.be(undefined); + } else { + expect(success).to.eql(false); + expect(successCount).to.eql(0); + expect(successResults).to.be(undefined); + expect(errors).to.eql([ + { error: { type: 'conflict' }, type, id: exactMatchId, title, meta }, + ]); + } + } else if (outcome === 'noAccess') { + expectNotFoundResponse(response); + } else { + // unauthorized read/write + expectForbiddenResponse(response); + } + }, + }, + { + testTitle: 'copying with an inexact match conflict', + objects: [{ type, id: inexactMatchId }], + statusCode, + response: async (response: TestResponse) => { + if (outcome === 'authorized') { + const { success, successCount, successResults, errors } = getResult(response); + const title = 'A shared saved-object in one space'; + const meta = { title, icon: 'beaker' }; + const destinationId = 'conflict_1_space_2'; + if (overwrite) { + expect(success).to.eql(true); + expect(successCount).to.eql(1); + expect(successResults).to.eql([ + { type, id: inexactMatchId, meta, overwrite: true, destinationId }, + ]); + expect(errors).to.be(undefined); + } else { + expect(success).to.eql(false); + expect(successCount).to.eql(0); + expect(successResults).to.be(undefined); + expect(errors).to.eql([ + { + error: { type: 'conflict', destinationId }, + type, + id: inexactMatchId, + title, + meta, + }, + ]); + } + } else if (outcome === 'noAccess') { + expectNotFoundResponse(response); + } else { + // unauthorized read/write + expectForbiddenResponse(response); + } + }, + }, + { + testTitle: 'copying with an ambiguous conflict', + objects: [{ type, id: ambiguousConflictId }], + statusCode, + response: async (response: TestResponse) => { + if (outcome === 'authorized') { + const { success, successCount, successResults, errors } = getResult(response); + const updatedAt = '2017-09-21T18:59:16.270Z'; + const destinations = [ + // response should be sorted by updatedAt in descending order + { id: 'conflict_2_space_2', title: 'A shared saved-object in one space', updatedAt }, + { id: 'conflict_2_all', title: 'A shared saved-object in all spaces', updatedAt }, + ]; + expect(success).to.eql(false); + expect(successCount).to.eql(0); + expect(successResults).to.be(undefined); + expect(errors).to.eql([ + { + error: { type: 'ambiguous_conflict', destinations }, + type, + id: ambiguousConflictId, + title: 'A shared saved-object in one space', + meta: { + title: 'A shared saved-object in one space', + icon: 'beaker', + }, + }, + ]); + } else if (outcome === 'noAccess') { + expectNotFoundResponse(response); + } else { + // unauthorized read/write + expectForbiddenResponse(response); + } + }, + }, + ]; }; const makeCopyToSpaceTest = (describeFn: DescribeFn) => ( @@ -363,162 +581,153 @@ export function copyToSpaceTestSuiteFactory( expect(['default', 'space_1']).to.contain(spaceId); }); - beforeEach(() => esArchiver.load('saved_objects/spaces')); - afterEach(() => esArchiver.unload('saved_objects/spaces')); - - it(`should return ${tests.noConflictsWithoutReferences.statusCode} when copying to space without conflicts or references`, async () => { - const destination = getDestinationWithoutConflicts(); - - await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); - - return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - spaces: [destination], - includeReferences: false, - overwrite: false, - }) - .expect(tests.noConflictsWithoutReferences.statusCode) - .then(tests.noConflictsWithoutReferences.response); - }); - - it(`should return ${tests.noConflictsWithReferences.statusCode} when copying to space without conflicts with references`, async () => { - const destination = getDestinationWithoutConflicts(); - - await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); - - return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - spaces: [destination], - includeReferences: true, - overwrite: false, - }) - .expect(tests.noConflictsWithReferences.statusCode) - .then(tests.noConflictsWithReferences.response); - }); - - it(`should return ${tests.withConflictsOverwriting.statusCode} when copying to space with conflicts when overwriting`, async () => { - const destination = getDestinationWithConflicts(spaceId); - - await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); - - return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - spaces: [destination], - includeReferences: true, - overwrite: true, - }) - .expect(tests.withConflictsOverwriting.statusCode) - .then(tests.withConflictsOverwriting.response); - }); - - it(`should return ${tests.withConflictsWithoutOverwriting.statusCode} when copying to space with conflicts without overwriting`, async () => { - const destination = getDestinationWithConflicts(spaceId); - - await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); - - return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - spaces: [destination], - includeReferences: true, - overwrite: false, - }) - .expect(tests.withConflictsWithoutOverwriting.statusCode) - .then(tests.withConflictsWithoutOverwriting.response); - }); - - it(`should return ${tests.multipleSpaces.statusCode} when copying to multiple spaces`, async () => { - const conflictDestination = getDestinationWithConflicts(spaceId); - const noConflictDestination = getDestinationWithoutConflicts(); - - return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - spaces: [conflictDestination, noConflictDestination], - includeReferences: true, - overwrite: true, - }) - .expect(tests.multipleSpaces.statusCode) - .then((response: TestResponse) => { - if (tests.multipleSpaces.statusCode === 200) { - expect(Object.keys(response.body).length).to.eql(2); + describe('single-namespace types', () => { + beforeEach(() => esArchiver.load('saved_objects/spaces')); + afterEach(() => esArchiver.unload('saved_objects/spaces')); + + const dashboardObject = { type: 'dashboard', id: 'cts_dashboard' }; + + it(`should return ${tests.noConflictsWithoutReferences.statusCode} when copying to space without conflicts or references`, async () => { + const destination = getDestinationWithoutConflicts(); + + await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); + + return supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + spaces: [destination], + includeReferences: false, + overwrite: false, + }) + .expect(tests.noConflictsWithoutReferences.statusCode) + .then(tests.noConflictsWithoutReferences.response); + }); + + it(`should return ${tests.noConflictsWithReferences.statusCode} when copying to space without conflicts with references`, async () => { + const destination = getDestinationWithoutConflicts(); + + await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); + + return supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + spaces: [destination], + includeReferences: true, + overwrite: false, + }) + .expect(tests.noConflictsWithReferences.statusCode) + .then(tests.noConflictsWithReferences.response); + }); + + it(`should return ${tests.withConflictsOverwriting.statusCode} when copying to space with conflicts when overwriting`, async () => { + const destination = getDestinationWithConflicts(spaceId); + + await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); + + return supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + spaces: [destination], + includeReferences: true, + overwrite: true, + }) + .expect(tests.withConflictsOverwriting.statusCode) + .then(tests.withConflictsOverwriting.response); + }); + + it(`should return ${tests.withConflictsWithoutOverwriting.statusCode} when copying to space with conflicts without overwriting`, async () => { + const destination = getDestinationWithConflicts(spaceId); + + await assertSpaceCounts(destination, INITIAL_COUNTS[destination]); + + return supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + spaces: [destination], + includeReferences: true, + overwrite: false, + }) + .expect(tests.withConflictsWithoutOverwriting.statusCode) + .then(tests.withConflictsWithoutOverwriting.response); + }); + + it(`should return ${tests.multipleSpaces.statusCode} when copying to multiple spaces`, async () => { + const conflictDestination = getDestinationWithConflicts(spaceId); + const noConflictDestination = getDestinationWithoutConflicts(); + + return supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + spaces: [conflictDestination, noConflictDestination], + includeReferences: true, + overwrite: true, + }) + .expect(tests.multipleSpaces.statusCode) + .then((response: TestResponse) => { + if (tests.multipleSpaces.statusCode === 200) { + expect(Object.keys(response.body).length).to.eql(2); + return Promise.all([ + tests.multipleSpaces.noConflictsResponse({ + body: { [noConflictDestination]: response.body[noConflictDestination] }, + }), + tests.multipleSpaces.withConflictsResponse({ + body: { [conflictDestination]: response.body[conflictDestination] }, + }), + ]); + } + + // non-200 status codes will not have a response body broken out by space id, like above. return Promise.all([ - tests.multipleSpaces.noConflictsResponse({ - body: { - [noConflictDestination]: response.body[noConflictDestination], - }, - }), - tests.multipleSpaces.withConflictsResponse({ - body: { - [conflictDestination]: response.body[conflictDestination], - }, - }), + tests.multipleSpaces.noConflictsResponse(response), + tests.multipleSpaces.withConflictsResponse(response), ]); - } - - // non-200 status codes will not have a response body broken out by space id, like above. - return Promise.all([ - tests.multipleSpaces.noConflictsResponse(response), - tests.multipleSpaces.withConflictsResponse(response), - ]); - }); + }); + }); + + it(`should return ${tests.nonExistentSpace.statusCode} when copying to non-existent space`, async () => { + return supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + spaces: ['non_existent_space'], + includeReferences: false, + overwrite: true, + }) + .expect(tests.nonExistentSpace.statusCode) + .then(tests.nonExistentSpace.response); + }); }); - it(`should return ${tests.nonExistentSpace.statusCode} when copying to non-existent space`, async () => { - return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - spaces: ['non_existent_space'], - includeReferences: false, - overwrite: true, - }) - .expect(tests.nonExistentSpace.statusCode) - .then(tests.nonExistentSpace.response); + [false, true].forEach((overwrite) => { + const spaces = ['space_2']; + const includeReferences = false; + describe(`multi-namespace types with overwrite=${overwrite}`, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + const testCases = tests.multiNamespaceTestCases(overwrite); + testCases.forEach(({ testTitle, objects, statusCode, response }) => { + it(`should return ${statusCode} when ${testTitle}`, async () => { + return supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) + .auth(user.username, user.password) + .send({ objects, spaces, includeReferences, overwrite }) + .expect(statusCode) + .then(response); + }); + }); + }); }); }); }; @@ -534,10 +743,10 @@ export function copyToSpaceTestSuiteFactory( expectNoConflictsForNonExistentSpaceResult, createExpectWithConflictsOverwritingResult, createExpectWithConflictsWithoutOverwritingResult, - expectRbacForbiddenResponse, expectNotFoundResponse, createExpectUnauthorizedAtSpaceWithReferencesResult, createExpectUnauthorizedAtSpaceWithoutReferencesResult, + createMultiNamespaceTestCases, originSpaces: ['default', 'space_1'], }; } diff --git a/x-pack/test/spaces_api_integration/common/suites/delete.ts b/x-pack/test/spaces_api_integration/common/suites/delete.ts index 15a90092f55177..69b5697d8a9a8a 100644 --- a/x-pack/test/spaces_api_integration/common/suites/delete.ts +++ b/x-pack/test/spaces_api_integration/common/suites/delete.ts @@ -130,7 +130,7 @@ export function deleteTestSuiteFactory(es: any, esArchiver: any, supertest: Supe expect(buckets).to.eql(expectedBuckets); - // There were seven multi-namespace objects. + // There were eleven multi-namespace objects. // Since Space 2 was deleted, any multi-namespace objects that existed in that space // are updated to remove it, and of those, any that don't exist in any space are deleted. const multiNamespaceResponse = await es.search({ @@ -138,16 +138,13 @@ export function deleteTestSuiteFactory(es: any, esArchiver: any, supertest: Supe body: { query: { terms: { type: ['sharedtype'] } } }, }); const docs: [Record] = multiNamespaceResponse.hits.hits; - expect(docs).length(6); // just six results, since spaces_2_only got deleted - Object.values(CASES).forEach(({ id, existingNamespaces }) => { - const remainingNamespaces = existingNamespaces.filter((x) => x !== 'space_2'); - const doc = docs.find((x) => x._id === `sharedtype:${id}`); - if (remainingNamespaces.length > 0) { - expect(doc?._source?.namespaces).to.eql(remainingNamespaces); - } else { - expect(doc).to.be(undefined); - } + expect(docs).length(10); // just ten results, since spaces_2_only got deleted + docs.forEach((doc) => () => { + const containsSpace2 = doc?._source?.namespaces.includes('space_2'); + expect(containsSpace2).to.eql(false); }); + const space2OnlyObjExists = docs.some((x) => x._id === CASES.SPACE_2_ONLY); + expect(space2OnlyObjExists).to.eql(false); }; const expectNotFound = (resp: { [key: string]: any }) => { diff --git a/x-pack/test/spaces_api_integration/common/suites/get_all.ts b/x-pack/test/spaces_api_integration/common/suites/get_all.ts index b6fb449e7b087d..d41d73bba90bc1 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get_all.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get_all.ts @@ -16,6 +16,7 @@ interface GetAllTest { interface GetAllTests { exists: GetAllTest; copySavedObjectsPurpose: GetAllTest; + shareSavedObjectsPurpose: GetAllTest; } interface GetAllTestDefinition { @@ -88,6 +89,17 @@ export function getAllTestSuiteFactory(esArchiver: any, supertest: SuperTest { + it(`should return ${tests.shareSavedObjectsPurpose.statusCode}`, async () => { + return supertest + .get(`${getUrlPrefix(spaceId)}/api/spaces/space`) + .query({ purpose: 'shareSavedObjectsIntoSpace' }) + .auth(user.username, user.password) + .expect(tests.copySavedObjectsPurpose.statusCode) + .then(tests.copySavedObjectsPurpose.response); + }); + }); }); }; diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts index 6d80688b7a7038..cb9219b1ba2ed8 100644 --- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts @@ -20,12 +20,19 @@ interface ResolveCopyToSpaceTest { response: (resp: TestResponse) => Promise; } +interface ResolveCopyToSpaceMultiNamespaceTest extends ResolveCopyToSpaceTest { + testTitle: string; + objects: Array>; + retries: Record; +} + interface ResolveCopyToSpaceTests { withReferencesNotOverwriting: ResolveCopyToSpaceTest; withReferencesOverwriting: ResolveCopyToSpaceTest; withoutReferencesOverwriting: ResolveCopyToSpaceTest; withoutReferencesNotOverwriting: ResolveCopyToSpaceTest; nonExistentSpace: ResolveCopyToSpaceTest; + multiNamespaceTestCases: () => ResolveCopyToSpaceMultiNamespaceTest[]; } interface ResolveCopyToSpaceTestDefinition { @@ -76,6 +83,17 @@ export function resolveCopyToSpaceConflictsSuite( [destination]: { success: true, successCount: 1, + successResults: [ + { + id: 'cts_vis_3', + type: 'visualization', + meta: { + title: `CTS vis 3 from ${sourceSpaceId} space`, + icon: 'visualizeApp', + }, + overwrite: true, + }, + ], }, }); const [dashboard, visualization] = await getObjectsAtSpace(destination); @@ -94,6 +112,17 @@ export function resolveCopyToSpaceConflictsSuite( [destinationSpaceId]: { success: true, successCount: 1, + successResults: [ + { + id: 'cts_dashboard', + type: 'dashboard', + meta: { + title: `This is the ${sourceSpaceId} test space CTS dashboard`, + icon: 'dashboardApp', + }, + overwrite: true, + }, + ], }, }); const [dashboard, visualization] = await getObjectsAtSpace(destinationSpaceId); @@ -119,11 +148,13 @@ export function resolveCopyToSpaceConflictsSuite( successCount: 0, errors: [ { - error: { - type: 'conflict', - }, + error: { type: 'conflict' }, id: 'cts_vis_3', title: `CTS vis 3 from ${sourceSpaceId} space`, + meta: { + title: `CTS vis 3 from ${sourceSpaceId} space`, + icon: 'visualizeApp', + }, type: 'visualization', }, ], @@ -149,12 +180,14 @@ export function resolveCopyToSpaceConflictsSuite( successCount: 0, errors: [ { - error: { - type: 'conflict', - }, + error: { type: 'conflict' }, id: 'cts_dashboard', - title: `This is the ${sourceSpaceId} test space CTS dashboard`, type: 'dashboard', + title: `This is the ${sourceSpaceId} test space CTS dashboard`, + meta: { + title: `This is the ${sourceSpaceId} test space CTS dashboard`, + icon: 'dashboardApp', + }, }, ], }, @@ -264,6 +297,113 @@ export function resolveCopyToSpaceConflictsSuite( } }; + /** + * Creates test cases for multi-namespace saved object types. + * Note: these are written with the assumption that test data will only be reloaded between each group of test cases, *not* before every + * single test case. This saves time during test execution. + */ + const createMultiNamespaceTestCases = ( + spaceId: string, + outcome: 'authorized' | 'unauthorizedRead' | 'unauthorizedWrite' | 'noAccess' = 'authorized' + ) => (): ResolveCopyToSpaceMultiNamespaceTest[] => { + // the status code of the HTTP response differs depending on the error type + // a 403 error actually comes back as an HTTP 200 response + const statusCode = outcome === 'noAccess' ? 404 : 200; + const type = 'sharedtype'; + const exactMatchId = 'all_spaces'; + const inexactMatchId = `conflict_1_${spaceId}`; + const ambiguousConflictId = `conflict_2_${spaceId}`; + + const createRetries = (overwriteRetry: Record) => ({ space_2: [overwriteRetry] }); + const getResult = (response: TestResponse) => (response.body as CopyResponse).space_2; + const expectForbiddenResponse = (response: TestResponse) => { + expect(response.body).to.eql({ + space_2: { + success: false, + successCount: 0, + errors: [ + { statusCode: 403, error: 'Forbidden', message: `Unable to bulk_create sharedtype` }, + ], + }, + }); + }; + const expectSuccessResponse = (response: TestResponse, id: string, destinationId?: string) => { + const { success, successCount, successResults, errors } = getResult(response); + expect(success).to.eql(true); + expect(successCount).to.eql(1); + expect(errors).to.be(undefined); + const title = + id === exactMatchId + ? 'A shared saved-object in the default, space_1, and space_2 spaces' + : 'A shared saved-object in one space'; + const meta = { title, icon: 'beaker' }; + expect(successResults).to.eql([ + { type, id, meta, overwrite: true, ...(destinationId && { destinationId }) }, + ]); + }; + + return [ + { + testTitle: 'copying with an exact match conflict', + objects: [{ type, id: exactMatchId }], + retries: createRetries({ type, id: exactMatchId, overwrite: true }), + statusCode, + response: async (response: TestResponse) => { + if (outcome === 'authorized') { + expectSuccessResponse(response, exactMatchId); + } else if (outcome === 'noAccess') { + expectNotFoundResponse(response); + } else { + // unauthorized read/write + expectForbiddenResponse(response); + } + }, + }, + { + testTitle: 'copying with an inexact match conflict', + objects: [{ type, id: inexactMatchId }], + retries: createRetries({ + type, + id: inexactMatchId, + overwrite: true, + destinationId: 'conflict_1_space_2', + }), + statusCode, + response: async (response: TestResponse) => { + if (outcome === 'authorized') { + expectSuccessResponse(response, inexactMatchId, 'conflict_1_space_2'); + } else if (outcome === 'noAccess') { + expectNotFoundResponse(response); + } else { + // unauthorized read/write + expectForbiddenResponse(response); + } + }, + }, + { + testTitle: 'copying with an ambiguous conflict', + objects: [{ type, id: ambiguousConflictId }], + retries: createRetries({ + type, + id: ambiguousConflictId, + overwrite: true, + destinationId: 'conflict_2_space_2', + }), + statusCode, + response: async (response: TestResponse) => { + if (outcome === 'authorized') { + expectSuccessResponse(response, ambiguousConflictId, 'conflict_2_space_2'); + } else if (outcome === 'noAccess') { + expectNotFoundResponse(response); + } else { + // unauthorized read/write + expectForbiddenResponse(response); + } + }, + }, + ]; + }; + const makeResolveCopyToSpaceConflictsTest = (describeFn: DescribeFn) => ( description: string, { user = {}, spaceId = DEFAULT_SPACE_ID, tests }: ResolveCopyToSpaceTestDefinition @@ -274,147 +414,105 @@ export function resolveCopyToSpaceConflictsSuite( expect(['default', 'space_1']).to.contain(spaceId); }); - beforeEach(() => esArchiver.load('saved_objects/spaces')); - afterEach(() => esArchiver.unload('saved_objects/spaces')); - - it(`should return ${tests.withReferencesNotOverwriting.statusCode} when not overwriting, with references`, async () => { - const destination = getDestinationSpace(spaceId); - - return supertestWithoutAuth - .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - includeReferences: true, - retries: { - [destination]: [ - { - type: 'visualization', - id: 'cts_vis_3', - overwrite: false, - }, - ], - }, - }) - .expect(tests.withReferencesNotOverwriting.statusCode) - .then(tests.withReferencesNotOverwriting.response); - }); - - it(`should return ${tests.withReferencesOverwriting.statusCode} when overwriting, with references`, async () => { - const destination = getDestinationSpace(spaceId); - - return supertestWithoutAuth - .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - includeReferences: true, - retries: { - [destination]: [ - { - type: 'visualization', - id: 'cts_vis_3', - overwrite: true, - }, - ], - }, - }) - .expect(tests.withReferencesOverwriting.statusCode) - .then(tests.withReferencesOverwriting.response); - }); - - it(`should return ${tests.withoutReferencesOverwriting.statusCode} when overwriting, without references`, async () => { - const destination = getDestinationSpace(spaceId); - - return supertestWithoutAuth - .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - includeReferences: false, - retries: { - [destination]: [ - { - type: 'dashboard', - id: 'cts_dashboard', - overwrite: true, - }, - ], - }, - }) - .expect(tests.withoutReferencesOverwriting.statusCode) - .then(tests.withoutReferencesOverwriting.response); + describe('single-namespace types', () => { + beforeEach(() => esArchiver.load('saved_objects/spaces')); + afterEach(() => esArchiver.unload('saved_objects/spaces')); + + const dashboardObject = { type: 'dashboard', id: 'cts_dashboard' }; + const visualizationObject = { type: 'visualization', id: 'cts_vis_3' }; + + it(`should return ${tests.withReferencesNotOverwriting.statusCode} when not overwriting, with references`, async () => { + const destination = getDestinationSpace(spaceId); + + return supertestWithoutAuth + .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + includeReferences: true, + retries: { [destination]: [{ ...visualizationObject, overwrite: false }] }, + }) + .expect(tests.withReferencesNotOverwriting.statusCode) + .then(tests.withReferencesNotOverwriting.response); + }); + + it(`should return ${tests.withReferencesOverwriting.statusCode} when overwriting, with references`, async () => { + const destination = getDestinationSpace(spaceId); + + return supertestWithoutAuth + .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + includeReferences: true, + retries: { [destination]: [{ ...visualizationObject, overwrite: true }] }, + }) + .expect(tests.withReferencesOverwriting.statusCode) + .then(tests.withReferencesOverwriting.response); + }); + + it(`should return ${tests.withoutReferencesOverwriting.statusCode} when overwriting, without references`, async () => { + const destination = getDestinationSpace(spaceId); + + return supertestWithoutAuth + .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + includeReferences: false, + retries: { [destination]: [{ ...dashboardObject, overwrite: true }] }, + }) + .expect(tests.withoutReferencesOverwriting.statusCode) + .then(tests.withoutReferencesOverwriting.response); + }); + + it(`should return ${tests.withoutReferencesNotOverwriting.statusCode} when not overwriting, without references`, async () => { + const destination = getDestinationSpace(spaceId); + + return supertestWithoutAuth + .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + includeReferences: false, + retries: { [destination]: [{ ...dashboardObject, overwrite: false }] }, + }) + .expect(tests.withoutReferencesNotOverwriting.statusCode) + .then(tests.withoutReferencesNotOverwriting.response); + }); + + it(`should return ${tests.nonExistentSpace.statusCode} when resolving within a non-existent space`, async () => { + const destination = NON_EXISTENT_SPACE_ID; + + return supertestWithoutAuth + .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) + .auth(user.username, user.password) + .send({ + objects: [dashboardObject], + includeReferences: false, + retries: { [destination]: [{ ...dashboardObject, overwrite: true }] }, + }) + .expect(tests.nonExistentSpace.statusCode) + .then(tests.nonExistentSpace.response); + }); }); - it(`should return ${tests.withoutReferencesNotOverwriting.statusCode} when not overwriting, without references`, async () => { - const destination = getDestinationSpace(spaceId); - - return supertestWithoutAuth - .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - includeReferences: false, - retries: { - [destination]: [ - { - type: 'dashboard', - id: 'cts_dashboard', - overwrite: false, - }, - ], - }, - }) - .expect(tests.withoutReferencesNotOverwriting.statusCode) - .then(tests.withoutReferencesNotOverwriting.response); - }); - - it(`should return ${tests.nonExistentSpace.statusCode} when resolving within a non-existent space`, async () => { - const destination = NON_EXISTENT_SPACE_ID; - - return supertestWithoutAuth - .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) - .auth(user.username, user.password) - .send({ - objects: [ - { - type: 'dashboard', - id: 'cts_dashboard', - }, - ], - includeReferences: false, - retries: { - [destination]: [ - { - type: 'dashboard', - id: 'cts_dashboard', - overwrite: true, - }, - ], - }, - }) - .expect(tests.nonExistentSpace.statusCode) - .then(tests.nonExistentSpace.response); + const includeReferences = false; + describe(`multi-namespace types with "overwrite" retry`, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + const testCases = tests.multiNamespaceTestCases(); + testCases.forEach(({ testTitle, objects, retries, statusCode, response }) => { + it(`should return ${statusCode} when ${testTitle}`, async () => { + return supertestWithoutAuth + .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) + .auth(user.username, user.password) + .send({ objects, includeReferences, retries }) + .expect(statusCode) + .then(response); + }); + }); }); }); }; @@ -433,6 +531,7 @@ export function resolveCopyToSpaceConflictsSuite( createExpectUnauthorizedAtSpaceWithReferencesResult, createExpectReadonlyAtSpaceWithReferencesResult, createExpectUnauthorizedAtSpaceWithoutReferencesResult, + createMultiNamespaceTestCases, originSpaces: ['default', 'space_1'], NON_EXISTENT_SPACE_ID, }; diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts index 08450f48567c8e..0f1c27098af92b 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts @@ -25,6 +25,7 @@ export default function copyToSpaceSpacesAndSecuritySuite({ getService }: TestIn createExpectUnauthorizedAtSpaceWithReferencesResult, createExpectUnauthorizedAtSpaceWithoutReferencesResult, expectNotFoundResponse, + createMultiNamespaceTestCases, } = copyToSpaceTestSuiteFactory(es, esArchiver, supertestWithoutAuth); describe('copy to spaces', () => { @@ -55,325 +56,148 @@ export default function copyToSpaceSpacesAndSecuritySuite({ getService }: TestIn dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, }, }, - ].forEach((scenario) => { - copyToSpaceTest(`user with no access from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.noAccess, + ].forEach(({ spaceId, ...scenario }) => { + const definitionNoAccess = (user: { username: string; password: string }) => ({ + spaceId, + user, tests: { - noConflictsWithoutReferences: { - statusCode: 404, - response: expectNotFoundResponse, - }, - noConflictsWithReferences: { - statusCode: 404, - response: expectNotFoundResponse, - }, - withConflictsOverwriting: { - statusCode: 404, - response: expectNotFoundResponse, - }, - withConflictsWithoutOverwriting: { - statusCode: 404, - response: expectNotFoundResponse, - }, + noConflictsWithoutReferences: { statusCode: 404, response: expectNotFoundResponse }, + noConflictsWithReferences: { statusCode: 404, response: expectNotFoundResponse }, + withConflictsOverwriting: { statusCode: 404, response: expectNotFoundResponse }, + withConflictsWithoutOverwriting: { statusCode: 404, response: expectNotFoundResponse }, multipleSpaces: { statusCode: 404, withConflictsResponse: expectNotFoundResponse, noConflictsResponse: expectNotFoundResponse, }, - nonExistentSpace: { - statusCode: 404, - response: expectNotFoundResponse, - }, + nonExistentSpace: { statusCode: 404, response: expectNotFoundResponse }, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'noAccess'), }, }); - - copyToSpaceTest(`superuser from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.superuser, - tests: { - noConflictsWithoutReferences: { - statusCode: 200, - response: expectNoConflictsWithoutReferencesResult, - }, - noConflictsWithReferences: { - statusCode: 200, - response: expectNoConflictsWithReferencesResult, - }, - withConflictsOverwriting: { - statusCode: 200, - response: createExpectWithConflictsOverwritingResult(scenario.spaceId), - }, - withConflictsWithoutOverwriting: { - statusCode: 200, - response: createExpectWithConflictsWithoutOverwritingResult(scenario.spaceId), - }, - multipleSpaces: { - statusCode: 200, - withConflictsResponse: createExpectWithConflictsOverwritingResult(scenario.spaceId), - noConflictsResponse: expectNoConflictsWithReferencesResult, - }, - nonExistentSpace: { - statusCode: 200, - response: expectNoConflictsForNonExistentSpaceResult, - }, + // In *this* test suite, a user who is unauthorized to write (but authorized to read) in the destination space will get the same exact + // results as a user who is unauthorized to read in the destination space. However, that may not *always* be the case depending on the + // input that is submitted, due to the `validateReferences` check that can trigger a `bulkGet` for the destination space. See also the + // integration tests in `./resolve_copy_to_space_conflicts`, which behave differently. + const commonUnauthorizedTests = { + noConflictsWithoutReferences: { + statusCode: 200, + response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( + spaceId, + 'without-conflicts' + ), }, - }); - - copyToSpaceTest(`rbac user with all globally from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.allGlobally, - tests: { - noConflictsWithoutReferences: { - statusCode: 200, - response: expectNoConflictsWithoutReferencesResult, - }, - noConflictsWithReferences: { - statusCode: 200, - response: expectNoConflictsWithReferencesResult, - }, - withConflictsOverwriting: { - statusCode: 200, - response: createExpectWithConflictsOverwritingResult(scenario.spaceId), - }, - withConflictsWithoutOverwriting: { - statusCode: 200, - response: createExpectWithConflictsWithoutOverwritingResult(scenario.spaceId), - }, - multipleSpaces: { - statusCode: 200, - withConflictsResponse: createExpectWithConflictsOverwritingResult(scenario.spaceId), - noConflictsResponse: expectNoConflictsWithReferencesResult, - }, - nonExistentSpace: { - statusCode: 200, - response: expectNoConflictsForNonExistentSpaceResult, - }, + noConflictsWithReferences: { + statusCode: 200, + response: createExpectUnauthorizedAtSpaceWithReferencesResult( + spaceId, + 'without-conflicts' + ), }, - }); - - copyToSpaceTest(`dual-privileges user from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.dualAll, - tests: { - noConflictsWithoutReferences: { - statusCode: 200, - response: expectNoConflictsWithoutReferencesResult, - }, - noConflictsWithReferences: { - statusCode: 200, - response: expectNoConflictsWithReferencesResult, - }, - withConflictsOverwriting: { - statusCode: 200, - response: createExpectWithConflictsOverwritingResult(scenario.spaceId), - }, - withConflictsWithoutOverwriting: { - statusCode: 200, - response: createExpectWithConflictsWithoutOverwritingResult(scenario.spaceId), - }, - multipleSpaces: { - statusCode: 200, - withConflictsResponse: createExpectWithConflictsOverwritingResult(scenario.spaceId), - noConflictsResponse: expectNoConflictsWithReferencesResult, - }, - nonExistentSpace: { - statusCode: 200, - response: expectNoConflictsForNonExistentSpaceResult, - }, + withConflictsOverwriting: { + statusCode: 200, + response: createExpectUnauthorizedAtSpaceWithReferencesResult(spaceId, 'with-conflicts'), }, - }); - - copyToSpaceTest(`legacy user from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.legacyAll, - tests: { - noConflictsWithoutReferences: { - statusCode: 404, - response: expectNotFoundResponse, - }, - noConflictsWithReferences: { - statusCode: 404, - response: expectNotFoundResponse, - }, - withConflictsOverwriting: { - statusCode: 404, - response: expectNotFoundResponse, - }, - withConflictsWithoutOverwriting: { - statusCode: 404, - response: expectNotFoundResponse, - }, - multipleSpaces: { - statusCode: 404, - withConflictsResponse: expectNotFoundResponse, - noConflictsResponse: expectNotFoundResponse, - }, - nonExistentSpace: { - statusCode: 404, - response: expectNotFoundResponse, - }, + withConflictsWithoutOverwriting: { + statusCode: 200, + response: createExpectUnauthorizedAtSpaceWithReferencesResult(spaceId, 'with-conflicts'), }, - }); - - copyToSpaceTest(`rbac user with read globally from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.readGlobally, + multipleSpaces: { + statusCode: 200, + withConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult( + spaceId, + 'with-conflicts' + ), + noConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult( + spaceId, + 'without-conflicts' + ), + }, + nonExistentSpace: { + statusCode: 200, + response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(spaceId, 'non-existent'), + }, + }; + const definitionUnauthorizedRead = (user: { username: string; password: string }) => ({ + spaceId, + user, tests: { - noConflictsWithoutReferences: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( - scenario.spaceId, - 'without-conflicts' - ), - }, - noConflictsWithReferences: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'without-conflicts' - ), - }, - withConflictsOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'with-conflicts' - ), - }, - withConflictsWithoutOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'with-conflicts' - ), - }, - multipleSpaces: { - statusCode: 200, - withConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'with-conflicts' - ), - noConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'without-conflicts' - ), - }, - nonExistentSpace: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( - scenario.spaceId, - 'non-existent' - ), - }, + ...commonUnauthorizedTests, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'unauthorizedRead'), }, }); - - copyToSpaceTest(`dual-privileges readonly user from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.dualRead, + const definitionUnauthorizedWrite = (user: { username: string; password: string }) => ({ + spaceId, + user, tests: { - noConflictsWithoutReferences: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( - scenario.spaceId, - 'without-conflicts' - ), - }, - noConflictsWithReferences: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'without-conflicts' - ), - }, - withConflictsOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'with-conflicts' - ), - }, - withConflictsWithoutOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'with-conflicts' - ), - }, - multipleSpaces: { - statusCode: 200, - withConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'with-conflicts' - ), - noConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'without-conflicts' - ), - }, - nonExistentSpace: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( - scenario.spaceId, - 'non-existent' - ), - }, + ...commonUnauthorizedTests, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'unauthorizedWrite'), }, }); - - copyToSpaceTest(`rbac user with all at space from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.allAtSpace, + const definitionAuthorized = (user: { username: string; password: string }) => ({ + spaceId, + user, tests: { noConflictsWithoutReferences: { statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( - scenario.spaceId, - 'without-conflicts' - ), + response: expectNoConflictsWithoutReferencesResult(spaceId), }, noConflictsWithReferences: { statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'without-conflicts' - ), + response: expectNoConflictsWithReferencesResult(spaceId), }, withConflictsOverwriting: { statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'with-conflicts' - ), + response: createExpectWithConflictsOverwritingResult(spaceId), }, withConflictsWithoutOverwriting: { statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'with-conflicts' - ), + response: createExpectWithConflictsWithoutOverwritingResult(spaceId), }, multipleSpaces: { statusCode: 200, - withConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'with-conflicts' - ), - noConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult( - scenario.spaceId, - 'without-conflicts' - ), + withConflictsResponse: createExpectWithConflictsOverwritingResult(spaceId), + noConflictsResponse: expectNoConflictsWithReferencesResult(spaceId), }, nonExistentSpace: { statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( - scenario.spaceId, - 'non-existent' - ), + response: expectNoConflictsForNonExistentSpaceResult(spaceId), }, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'authorized'), }, }); + + copyToSpaceTest( + `user with no access from the ${spaceId} space`, + definitionNoAccess(scenario.users.noAccess) + ); + copyToSpaceTest( + `superuser from the ${spaceId} space`, + definitionAuthorized(scenario.users.superuser) + ); + copyToSpaceTest( + `rbac user with all globally from the ${spaceId} space`, + definitionAuthorized(scenario.users.allGlobally) + ); + copyToSpaceTest( + `dual-privileges user from the ${spaceId} space`, + definitionAuthorized(scenario.users.dualAll) + ); + copyToSpaceTest( + `legacy user from the ${spaceId} space`, + definitionNoAccess(scenario.users.legacyAll) + ); + copyToSpaceTest( + `rbac user with read globally from the ${spaceId} space`, + definitionUnauthorizedWrite(scenario.users.readGlobally) + ); + copyToSpaceTest( + `dual-privileges readonly user from the ${spaceId} space`, + definitionUnauthorizedWrite(scenario.users.dualRead) + ); + copyToSpaceTest( + `rbac user with all at space from the ${spaceId} space`, + definitionUnauthorizedRead(scenario.users.allAtSpace) + ); }); }); } diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts index e64f7218250895..bf1d90bfc35565 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts @@ -88,6 +88,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 403, response: expectRbacForbidden, }, + shareSavedObjectsPurpose: { + statusCode: 403, + response: expectRbacForbidden, + }, }, }); @@ -103,6 +107,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 200, response: createExpectResults('default', 'space_1', 'space_2'), }, + shareSavedObjectsPurpose: { + statusCode: 200, + response: createExpectResults('default', 'space_1', 'space_2'), + }, }, }); @@ -118,6 +126,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 200, response: createExpectResults('default', 'space_1', 'space_2'), }, + shareSavedObjectsPurpose: { + statusCode: 200, + response: createExpectResults('default', 'space_1', 'space_2'), + }, }, }); @@ -133,6 +145,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 200, response: createExpectResults('default', 'space_1', 'space_2'), }, + shareSavedObjectsPurpose: { + statusCode: 200, + response: createExpectResults('default', 'space_1', 'space_2'), + }, }, }); @@ -148,6 +164,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 403, response: expectRbacForbidden, }, + shareSavedObjectsPurpose: { + statusCode: 403, + response: expectRbacForbidden, + }, }, }); @@ -163,6 +183,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 403, response: expectRbacForbidden, }, + shareSavedObjectsPurpose: { + statusCode: 403, + response: expectRbacForbidden, + }, }, }); @@ -178,6 +202,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 403, response: expectRbacForbidden, }, + shareSavedObjectsPurpose: { + statusCode: 403, + response: expectRbacForbidden, + }, }, }); @@ -193,6 +221,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 200, response: createExpectResults('space_1'), }, + shareSavedObjectsPurpose: { + statusCode: 200, + response: createExpectResults('space_1'), + }, }, }); @@ -208,6 +240,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 403, response: expectRbacForbidden, }, + shareSavedObjectsPurpose: { + statusCode: 403, + response: expectRbacForbidden, + }, }, }); @@ -225,6 +261,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 200, response: createExpectResults('default'), }, + shareSavedObjectsPurpose: { + statusCode: 200, + response: createExpectResults('default'), + }, }, } ); @@ -243,6 +283,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 403, response: expectRbacForbidden, }, + shareSavedObjectsPurpose: { + statusCode: 403, + response: expectRbacForbidden, + }, }, } ); @@ -261,6 +305,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 200, response: createExpectResults('default'), }, + shareSavedObjectsPurpose: { + statusCode: 200, + response: createExpectResults('default'), + }, }, } ); @@ -279,6 +327,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 403, response: expectRbacForbidden, }, + shareSavedObjectsPurpose: { + statusCode: 403, + response: expectRbacForbidden, + }, }, } ); @@ -297,6 +349,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 200, response: createExpectResults('space_1'), }, + shareSavedObjectsPurpose: { + statusCode: 200, + response: createExpectResults('space_1'), + }, }, } ); @@ -315,6 +371,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 403, response: expectRbacForbidden, }, + shareSavedObjectsPurpose: { + statusCode: 403, + response: expectRbacForbidden, + }, }, } ); @@ -331,6 +391,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 403, response: expectRbacForbidden, }, + shareSavedObjectsPurpose: { + statusCode: 403, + response: expectRbacForbidden, + }, }, }); @@ -346,6 +410,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 403, response: expectRbacForbidden, }, + shareSavedObjectsPurpose: { + statusCode: 403, + response: expectRbacForbidden, + }, }, }); @@ -361,6 +429,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 403, response: expectRbacForbidden, }, + shareSavedObjectsPurpose: { + statusCode: 403, + response: expectRbacForbidden, + }, }, }); @@ -376,6 +448,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 403, response: expectRbacForbidden, }, + shareSavedObjectsPurpose: { + statusCode: 403, + response: expectRbacForbidden, + }, }, }); }); diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts index 472ec1a9271267..b81f2965eba22c 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts @@ -25,6 +25,7 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Tes createExpectUnauthorizedAtSpaceWithReferencesResult, createExpectReadonlyAtSpaceWithReferencesResult, createExpectUnauthorizedAtSpaceWithoutReferencesResult, + createMultiNamespaceTestCases, NON_EXISTENT_SPACE_ID, } = resolveCopyToSpaceConflictsSuite(esArchiver, supertestWithAuth, supertestWithoutAuth); @@ -56,10 +57,10 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Tes dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, }, }, - ].forEach((scenario) => { - resolveCopyToSpaceConflictsTest(`user with no access from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.noAccess, + ].forEach(({ spaceId, ...scenario }) => { + const definitionNoAccess = (user: { username: string; password: string }) => ({ + spaceId, + user, tests: { withReferencesNotOverwriting: { statusCode: 404, @@ -81,226 +82,131 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Tes statusCode: 404, response: expectNotFoundResponse, }, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'noAccess'), }, }); - - resolveCopyToSpaceConflictsTest(`superuser from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.superuser, + const definitionUnauthorizedRead = (user: { username: string; password: string }) => ({ + spaceId, + user, tests: { withReferencesNotOverwriting: { statusCode: 200, - response: createExpectNonOverriddenResponseWithReferences(scenario.spaceId), + response: createExpectUnauthorizedAtSpaceWithReferencesResult(spaceId), }, withReferencesOverwriting: { statusCode: 200, - response: createExpectOverriddenResponseWithReferences(scenario.spaceId), + response: createExpectUnauthorizedAtSpaceWithReferencesResult(spaceId), }, withoutReferencesOverwriting: { statusCode: 200, - response: createExpectOverriddenResponseWithoutReferences(scenario.spaceId), + response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(spaceId), }, withoutReferencesNotOverwriting: { statusCode: 200, - response: createExpectNonOverriddenResponseWithoutReferences(scenario.spaceId), + response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(spaceId), }, nonExistentSpace: { statusCode: 200, - response: createExpectOverriddenResponseWithoutReferences( - scenario.spaceId, + response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( + spaceId, NON_EXISTENT_SPACE_ID ), }, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'unauthorizedRead'), }, }); - - resolveCopyToSpaceConflictsTest( - `rbac user with all globally from the ${scenario.spaceId} space`, - { - spaceId: scenario.spaceId, - user: scenario.users.allGlobally, - tests: { - withReferencesNotOverwriting: { - statusCode: 200, - response: createExpectNonOverriddenResponseWithReferences(scenario.spaceId), - }, - withReferencesOverwriting: { - statusCode: 200, - response: createExpectOverriddenResponseWithReferences(scenario.spaceId), - }, - withoutReferencesOverwriting: { - statusCode: 200, - response: createExpectOverriddenResponseWithoutReferences(scenario.spaceId), - }, - withoutReferencesNotOverwriting: { - statusCode: 200, - response: createExpectNonOverriddenResponseWithoutReferences(scenario.spaceId), - }, - nonExistentSpace: { - statusCode: 200, - response: createExpectOverriddenResponseWithoutReferences( - scenario.spaceId, - NON_EXISTENT_SPACE_ID - ), - }, - }, - } - ); - - resolveCopyToSpaceConflictsTest(`dual-privileges user from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.dualAll, + const definitionUnauthorizedWrite = (user: { username: string; password: string }) => ({ + spaceId, + user, tests: { withReferencesNotOverwriting: { statusCode: 200, - response: createExpectNonOverriddenResponseWithReferences(scenario.spaceId), + response: createExpectReadonlyAtSpaceWithReferencesResult(spaceId), }, withReferencesOverwriting: { statusCode: 200, - response: createExpectOverriddenResponseWithReferences(scenario.spaceId), + response: createExpectReadonlyAtSpaceWithReferencesResult(spaceId), }, withoutReferencesOverwriting: { statusCode: 200, - response: createExpectOverriddenResponseWithoutReferences(scenario.spaceId), + response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(spaceId), }, withoutReferencesNotOverwriting: { statusCode: 200, - response: createExpectNonOverriddenResponseWithoutReferences(scenario.spaceId), + response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(spaceId), }, nonExistentSpace: { statusCode: 200, - response: createExpectOverriddenResponseWithoutReferences( - scenario.spaceId, + response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( + spaceId, NON_EXISTENT_SPACE_ID ), }, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'unauthorizedWrite'), }, }); - - resolveCopyToSpaceConflictsTest(`legacy user from the ${scenario.spaceId} space`, { - spaceId: scenario.spaceId, - user: scenario.users.legacyAll, + const definitionAuthorized = (user: { username: string; password: string }) => ({ + spaceId, + user, tests: { withReferencesNotOverwriting: { - statusCode: 404, - response: expectNotFoundResponse, + statusCode: 200, + response: createExpectNonOverriddenResponseWithReferences(spaceId), }, withReferencesOverwriting: { - statusCode: 404, - response: expectNotFoundResponse, + statusCode: 200, + response: createExpectOverriddenResponseWithReferences(spaceId), }, withoutReferencesOverwriting: { - statusCode: 404, - response: expectNotFoundResponse, + statusCode: 200, + response: createExpectOverriddenResponseWithoutReferences(spaceId), }, withoutReferencesNotOverwriting: { - statusCode: 404, - response: expectNotFoundResponse, + statusCode: 200, + response: createExpectNonOverriddenResponseWithoutReferences(spaceId), }, nonExistentSpace: { - statusCode: 404, - response: expectNotFoundResponse, + statusCode: 200, + response: createExpectOverriddenResponseWithoutReferences( + spaceId, + NON_EXISTENT_SPACE_ID + ), }, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'authorized'), }, }); resolveCopyToSpaceConflictsTest( - `rbac user with read globally from the ${scenario.spaceId} space`, - { - spaceId: scenario.spaceId, - user: scenario.users.readGlobally, - tests: { - withReferencesNotOverwriting: { - statusCode: 200, - response: createExpectReadonlyAtSpaceWithReferencesResult(scenario.spaceId), - }, - withReferencesOverwriting: { - statusCode: 200, - response: createExpectReadonlyAtSpaceWithReferencesResult(scenario.spaceId), - }, - withoutReferencesOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId), - }, - withoutReferencesNotOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId), - }, - nonExistentSpace: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( - scenario.spaceId, - NON_EXISTENT_SPACE_ID - ), - }, - }, - } + `user with no access from the ${spaceId} space`, + definitionNoAccess(scenario.users.noAccess) ); - resolveCopyToSpaceConflictsTest( - `dual-privileges readonly user from the ${scenario.spaceId} space`, - { - spaceId: scenario.spaceId, - user: scenario.users.dualRead, - tests: { - withReferencesNotOverwriting: { - statusCode: 200, - response: createExpectReadonlyAtSpaceWithReferencesResult(scenario.spaceId), - }, - withReferencesOverwriting: { - statusCode: 200, - response: createExpectReadonlyAtSpaceWithReferencesResult(scenario.spaceId), - }, - withoutReferencesOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId), - }, - withoutReferencesNotOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId), - }, - nonExistentSpace: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( - scenario.spaceId, - NON_EXISTENT_SPACE_ID - ), - }, - }, - } + `superuser from the ${spaceId} space`, + definitionAuthorized(scenario.users.superuser) ); - resolveCopyToSpaceConflictsTest( - `rbac user with all at space from the ${scenario.spaceId} space`, - { - spaceId: scenario.spaceId, - user: scenario.users.allAtSpace, - tests: { - withReferencesNotOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult(scenario.spaceId), - }, - withReferencesOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithReferencesResult(scenario.spaceId), - }, - withoutReferencesOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId), - }, - withoutReferencesNotOverwriting: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId), - }, - nonExistentSpace: { - statusCode: 200, - response: createExpectUnauthorizedAtSpaceWithoutReferencesResult( - scenario.spaceId, - NON_EXISTENT_SPACE_ID - ), - }, - }, - } + `rbac user with all globally from the ${spaceId} space`, + definitionAuthorized(scenario.users.allGlobally) + ); + resolveCopyToSpaceConflictsTest( + `dual-privileges user from the ${spaceId} space`, + definitionAuthorized(scenario.users.dualAll) + ); + resolveCopyToSpaceConflictsTest( + `legacy user from the ${spaceId} space`, + definitionNoAccess(scenario.users.legacyAll) + ); + resolveCopyToSpaceConflictsTest( + `rbac user with read globally from the ${spaceId} space`, + definitionUnauthorizedWrite(scenario.users.readGlobally) + ); + resolveCopyToSpaceConflictsTest( + `dual-privileges readonly user from the ${spaceId} space`, + definitionUnauthorizedWrite(scenario.users.dualRead) + ); + resolveCopyToSpaceConflictsTest( + `rbac user with all at space from the ${spaceId} space`, + definitionUnauthorizedRead(scenario.users.allAtSpace) ); }); }); diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts index f3e6580e439bb5..ddd029c8d7d687 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts @@ -25,7 +25,7 @@ const createTestCases = (spaceId: string) => { const namespaces = [spaceId]; return [ // Test cases to check adding the target namespace to different saved objects - { ...CASES.DEFAULT_SPACE_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.DEFAULT_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, { ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, { ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, { ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) }, @@ -37,7 +37,7 @@ const createTestCases = (spaceId: string) => { // These are non-exhaustive, they only check cases for adding two additional namespaces to a saved object // More permutations are covered in the corresponding spaces_only test suite { - ...CASES.DEFAULT_SPACE_ONLY, + ...CASES.DEFAULT_ONLY, namespaces: [SPACE_1_ID, SPACE_2_ID], ...fail404(spaceId !== DEFAULT_SPACE_ID), }, diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts index d83020a9598f19..4b120a71213b75 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts @@ -29,7 +29,7 @@ const createTestCases = (spaceId: string) => { // Test cases to check removing the target namespace from different saved objects let namespaces = [spaceId]; const singleSpace = [ - { id: CASES.DEFAULT_SPACE_ONLY.id, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { id: CASES.DEFAULT_ONLY.id, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, { id: CASES.SPACE_1_ONLY.id, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, { id: CASES.SPACE_2_ONLY.id, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, { id: CASES.DEFAULT_AND_SPACE_1.id, namespaces, ...fail404(spaceId === SPACE_2_ID) }, diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts index 75b35fecd5d83c..cc5bb9cf8c739b 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts @@ -20,6 +20,7 @@ export default function copyToSpacesOnlySuite({ getService }: FtrProviderContext expectNoConflictsForNonExistentSpaceResult, createExpectWithConflictsOverwritingResult, createExpectWithConflictsWithoutOverwritingResult, + createMultiNamespaceTestCases, originSpaces, } = copyToSpaceTestSuiteFactory(es, esArchiver, supertestWithoutAuth); @@ -30,11 +31,11 @@ export default function copyToSpacesOnlySuite({ getService }: FtrProviderContext tests: { noConflictsWithoutReferences: { statusCode: 200, - response: expectNoConflictsWithoutReferencesResult, + response: expectNoConflictsWithoutReferencesResult(spaceId), }, noConflictsWithReferences: { statusCode: 200, - response: expectNoConflictsWithReferencesResult, + response: expectNoConflictsWithReferencesResult(spaceId), }, withConflictsOverwriting: { statusCode: 200, @@ -47,12 +48,13 @@ export default function copyToSpacesOnlySuite({ getService }: FtrProviderContext multipleSpaces: { statusCode: 200, withConflictsResponse: createExpectWithConflictsOverwritingResult(spaceId), - noConflictsResponse: expectNoConflictsWithReferencesResult, + noConflictsResponse: expectNoConflictsWithReferencesResult(spaceId), }, nonExistentSpace: { statusCode: 200, - response: expectNoConflictsForNonExistentSpaceResult, + response: expectNoConflictsForNonExistentSpaceResult(spaceId), }, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId), }, }); }); diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/get_all.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/get_all.ts index 1e56a583eca1fa..14c98aff262fea 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/get_all.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/get_all.ts @@ -38,6 +38,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { statusCode: 200, response: createExpectResults('default', 'space_1', 'space_2'), }, + shareSavedObjectsPurpose: { + statusCode: 200, + response: createExpectResults('default', 'space_1', 'space_2'), + }, }, }); }); diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts index ef2735de3d3dbb..5c84475d328508 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts @@ -19,6 +19,7 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Ftr createExpectNonOverriddenResponseWithoutReferences, createExpectOverriddenResponseWithReferences, createExpectOverriddenResponseWithoutReferences, + createMultiNamespaceTestCases, NON_EXISTENT_SPACE_ID, originSpaces, } = resolveCopyToSpaceConflictsSuite(esArchiver, supertestWithAuth, supertestWithoutAuth); @@ -51,6 +52,7 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Ftr NON_EXISTENT_SPACE_ID ), }, + multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId), }, }); }); diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts index 5cdebf9edfcfd5..25ba986a12fd88 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts @@ -27,7 +27,7 @@ const { fail404 } = testCaseFailures; const createSingleTestCases = (spaceId: string) => { const namespaces = ['some-space-id']; return [ - { ...CASES.DEFAULT_SPACE_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.DEFAULT_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, { ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, { ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, { ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) }, @@ -43,7 +43,7 @@ const createSingleTestCases = (spaceId: string) => { */ const createMultiTestCases = () => { const allSpaces = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; - let id = CASES.DEFAULT_SPACE_ONLY.id; + let id = CASES.DEFAULT_ONLY.id; const one = [{ id, namespaces: allSpaces }]; id = CASES.DEFAULT_AND_SPACE_1.id; const two = [{ id, namespaces: allSpaces }]; diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts index 8bcd294b38f3fa..2c4506b7235339 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts @@ -27,7 +27,7 @@ const { fail404 } = testCaseFailures; const createSingleTestCases = (spaceId: string) => { const namespaces = [spaceId]; return [ - { ...CASES.DEFAULT_SPACE_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.DEFAULT_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, { ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, { ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, { ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) }, @@ -43,7 +43,7 @@ const createSingleTestCases = (spaceId: string) => { */ const createMultiTestCases = () => { const nonExistentSpaceId = 'does_not_exist'; // space that doesn't exist - let id = CASES.DEFAULT_SPACE_ONLY.id; + let id = CASES.DEFAULT_ONLY.id; const one = [ { id, namespaces: [nonExistentSpaceId] }, { id, namespaces: [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID] }, From deb71ecbb7a0a7f0e6eb5159854a782a9aa89a65 Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Wed, 26 Aug 2020 17:13:38 -0400 Subject: [PATCH 046/216] [Security Solution][Exceptions Modal] Switches modal header (#76016) --- .../components/exceptions/add_exception_modal/translations.ts | 2 +- .../components/exceptions/edit_exception_modal/translations.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/translations.ts index 39162844167073..2e9bced21fe714 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/translations.ts @@ -13,7 +13,7 @@ export const CANCEL = i18n.translate('xpack.securitySolution.exceptions.addExcep export const ADD_EXCEPTION = i18n.translate( 'xpack.securitySolution.exceptions.addException.addException', { - defaultMessage: 'Add Exception', + defaultMessage: 'Add Rule Exception', } ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts index 09e0a75d215730..1452003d8f8b80 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts @@ -20,7 +20,7 @@ export const EDIT_EXCEPTION_SAVE_BUTTON = i18n.translate( export const EDIT_EXCEPTION_TITLE = i18n.translate( 'xpack.securitySolution.exceptions.editException.editExceptionTitle', { - defaultMessage: 'Edit Exception', + defaultMessage: 'Edit Rule Exception', } ); From fd39f094ccc3bf0aba39789961d31bedebe2cce1 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Wed, 26 Aug 2020 17:19:30 -0400 Subject: [PATCH 047/216] Duplicate title warning wording (#75908) Changed wording on duplicate title warning. --- .../save_modal/saved_object_save_modal.tsx | 17 ++++------------- .../translations/translations/ja-JP.json | 2 -- .../translations/translations/zh-CN.json | 2 -- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx index 962f993633e6fd..3b9efbee22ba6c 100644 --- a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx +++ b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx @@ -281,8 +281,8 @@ export class SavedObjectSaveModal extends React.Component title={ } color="warning" @@ -292,18 +292,9 @@ export class SavedObjectSaveModal extends React.Component

- {this.props.confirmButtonLabel - ? this.props.confirmButtonLabel - : i18n.translate('savedObjects.saveModal.saveButtonLabel', { - defaultMessage: 'Save', - })} - - ), + title: this.props.title, }} />

diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 70e2b34d06ce6d..d6e611e65154b7 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2880,8 +2880,6 @@ "savedObjects.saveDuplicateRejectedDescription": "重複ファイルの保存確認が拒否されました", "savedObjects.saveModal.cancelButtonLabel": "キャンセル", "savedObjects.saveModal.descriptionLabel": "説明", - "savedObjects.saveModal.duplicateTitleDescription": "{confirmSaveLabel} をクリックすると {objectType} がこの重複したタイトルで保存されます。", - "savedObjects.saveModal.duplicateTitleLabel": "「{title}」というタイトルの {objectType} が既に存在します", "savedObjects.saveModal.saveAsNewLabel": "新しい {objectType} として保存", "savedObjects.saveModal.saveButtonLabel": "保存", "savedObjects.saveModal.saveTitle": "{objectType} を保存", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e682a12859c472..54c69d849e3a9f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2881,8 +2881,6 @@ "savedObjects.saveDuplicateRejectedDescription": "已拒绝使用重复标题保存确认", "savedObjects.saveModal.cancelButtonLabel": "取消", "savedObjects.saveModal.descriptionLabel": "描述", - "savedObjects.saveModal.duplicateTitleDescription": "单击“{confirmSaveLabel}”将会使用此重复标题保存 {objectType}。", - "savedObjects.saveModal.duplicateTitleLabel": "具有标题“{title}”的 {objectType} 已存在", "savedObjects.saveModal.saveAsNewLabel": "另存为新的 {objectType}", "savedObjects.saveModal.saveButtonLabel": "保存", "savedObjects.saveModal.saveTitle": "保存 {objectType}", From 35b8d50ccd412ab50e5b22726f4455000c5fa72b Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Wed, 26 Aug 2020 16:21:11 -0500 Subject: [PATCH 048/216] [Enterprise Search] Adds app logic file to Workplace Search (#76009) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add new Workplace Search initial data properties * Add app logic * Refactor index to match App Search Adds the easier-to-read ComponentConfigured and ComponentUnconfigured FCs with a ternary in the root compoenent * Remove ‘Logic’ from interface names * Extract initial data from WS into interface This allows for breaking apart the app-specific data and also having an interface to extend in the app_logic file * Destructuring FTW --- .../common/__mocks__/initial_app_data.ts | 2 + .../enterprise_search/common/types/index.ts | 7 +- .../common/types/workplace_search.ts | 7 ++ .../workplace_search/app_logic.test.ts | 35 +++++++++ .../workplace_search/app_logic.ts | 32 ++++++++ .../workplace_search/index.test.tsx | 77 ++++++++++++++----- .../applications/workplace_search/index.tsx | 38 +++++---- .../lib/enterprise_search_config_api.test.ts | 4 + .../lib/enterprise_search_config_api.ts | 2 + 9 files changed, 165 insertions(+), 39 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts diff --git a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts index 79e1efc425b4e2..2d31be65dd30ea 100644 --- a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts +++ b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts @@ -29,6 +29,8 @@ export const DEFAULT_INITIAL_APP_DATA = { }, }, workplaceSearch: { + canCreateInvitations: true, + isFederatedAuth: false, organization: { name: 'ACME Donuts', defaultOrgName: 'My Organization', diff --git a/x-pack/plugins/enterprise_search/common/types/index.ts b/x-pack/plugins/enterprise_search/common/types/index.ts index 52e468b741a071..008afb234a3764 100644 --- a/x-pack/plugins/enterprise_search/common/types/index.ts +++ b/x-pack/plugins/enterprise_search/common/types/index.ts @@ -5,17 +5,14 @@ */ import { IAccount as IAppSearchAccount } from './app_search'; -import { IAccount as IWorkplaceSearchAccount, IOrganization } from './workplace_search'; +import { IWorkplaceSearchInitialData } from './workplace_search'; export interface IInitialAppData { readOnlyMode?: boolean; ilmEnabled?: boolean; configuredLimits?: IConfiguredLimits; appSearch?: IAppSearchAccount; - workplaceSearch?: { - organization: IOrganization; - fpAccount: IWorkplaceSearchAccount; - }; + workplaceSearch?: IWorkplaceSearchInitialData; } export interface IConfiguredLimits { diff --git a/x-pack/plugins/enterprise_search/common/types/workplace_search.ts b/x-pack/plugins/enterprise_search/common/types/workplace_search.ts index fd8fa6daf81acd..bc4e39b0788d9d 100644 --- a/x-pack/plugins/enterprise_search/common/types/workplace_search.ts +++ b/x-pack/plugins/enterprise_search/common/types/workplace_search.ts @@ -17,3 +17,10 @@ export interface IOrganization { name: string; defaultOrgName: string; } + +export interface IWorkplaceSearchInitialData { + canCreateInvitations: boolean; + isFederatedAuth: boolean; + organization: IOrganization; + fpAccount: IAccount; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts new file mode 100644 index 00000000000000..bc31b7df5d971d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resetContext } from 'kea'; + +import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; +import { AppLogic } from './app_logic'; + +describe('AppLogic', () => { + beforeEach(() => { + resetContext({}); + AppLogic.mount(); + }); + + const DEFAULT_VALUES = { + hasInitialized: false, + }; + + it('has expected default values', () => { + expect(AppLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('initializeAppData()', () => { + it('sets values based on passed props', () => { + AppLogic.actions.initializeAppData(DEFAULT_INITIAL_APP_DATA); + + expect(AppLogic.values).toEqual({ + hasInitialized: true, + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts new file mode 100644 index 00000000000000..b7116f02663c16 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts @@ -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 { kea } from 'kea'; + +import { IInitialAppData } from '../../../common/types'; +import { IWorkplaceSearchInitialData } from '../../../common/types/workplace_search'; +import { IKeaLogic } from '../shared/types'; + +export interface IAppValues extends IWorkplaceSearchInitialData { + hasInitialized: boolean; +} +export interface IAppActions { + initializeAppData(props: IInitialAppData): void; +} + +export const AppLogic = kea({ + actions: (): IAppActions => ({ + initializeAppData: ({ workplaceSearch }) => workplaceSearch, + }), + reducers: () => ({ + hasInitialized: [ + false, + { + initializeAppData: () => true, + }, + ], + }), +}) as IKeaLogic; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index a0d9352ee9f82a..39280ad6f4be4a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -10,39 +10,76 @@ import '../__mocks__/kea.mock'; import React, { useContext } from 'react'; import { Redirect } from 'react-router-dom'; import { shallow } from 'enzyme'; -import { useValues } from 'kea'; +import { useValues, useActions } from 'kea'; -import { Overview } from './views/overview'; +import { SetupGuide } from './views/setup_guide'; import { ErrorState } from './views/error_state'; +import { Overview } from './views/overview'; -import { WorkplaceSearch } from './'; +import { WorkplaceSearch, WorkplaceSearchUnconfigured, WorkplaceSearchConfigured } from './'; -describe('Workplace Search', () => { - it('redirects to Setup Guide when enterpriseSearchUrl is not set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ - config: { host: '' }, - })); +describe('WorkplaceSearch', () => { + it('renders WorkplaceSearchUnconfigured when config.host is not set', () => { + (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); const wrapper = shallow(); - expect(wrapper.find(Redirect)).toHaveLength(1); - expect(wrapper.find(Overview)).toHaveLength(0); + expect(wrapper.find(WorkplaceSearchUnconfigured)).toHaveLength(1); }); - it('renders the Overview when enterpriseSearchUrl is set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ - config: { host: 'https://foo.bar' }, - })); + it('renders WorkplaceSearchConfigured when config.host set', () => { + (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'some.url' } })); const wrapper = shallow(); + expect(wrapper.find(WorkplaceSearchConfigured)).toHaveLength(1); + }); +}); + +describe('WorkplaceSearchUnconfigured', () => { + it('renders the Setup Guide and redirects to the Setup Guide', () => { + const wrapper = shallow(); + + expect(wrapper.find(SetupGuide)).toHaveLength(1); + expect(wrapper.find(Redirect)).toHaveLength(1); + }); +}); + +describe('WorkplaceSearchConfigured', () => { + beforeEach(() => { + // Mock resets + (useValues as jest.Mock).mockImplementation(() => ({})); + (useActions as jest.Mock).mockImplementation(() => ({ initializeAppData: () => {} })); + }); + + it('renders with layout', () => { + const wrapper = shallow(); + expect(wrapper.find(Overview)).toHaveLength(1); - expect(wrapper.find(Redirect)).toHaveLength(0); }); - it('renders ErrorState when the app cannot connect to Enterprise Search', () => { - (useValues as jest.Mock).mockImplementationOnce(() => ({ errorConnecting: true })); - const wrapper = shallow(); + it('initializes app data with passed props', () => { + const initializeAppData = jest.fn(); + (useActions as jest.Mock).mockImplementation(() => ({ initializeAppData })); + + shallow(); + + expect(initializeAppData).toHaveBeenCalledWith({ readOnlyMode: true }); + }); + + it('does not re-initialize app data', () => { + const initializeAppData = jest.fn(); + (useActions as jest.Mock).mockImplementation(() => ({ initializeAppData })); + (useValues as jest.Mock).mockImplementation(() => ({ hasInitialized: true })); + + shallow(); + + expect(initializeAppData).not.toHaveBeenCalled(); + }); + + it('renders ErrorState', () => { + (useValues as jest.Mock).mockImplementation(() => ({ errorConnecting: true })); + + const wrapper = shallow(); - expect(wrapper.find(ErrorState).exists()).toBe(true); - expect(wrapper.find(Overview)).toHaveLength(0); + expect(wrapper.find(ErrorState)).toHaveLength(2); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 8582a003c6fa88..c0a51d5670a147 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React, { useContext, useEffect } from 'react'; import { Route, Redirect, Switch } from 'react-router-dom'; -import { useValues } from 'kea'; +import { useActions, useValues } from 'kea'; import { IInitialAppData } from '../../../common/types'; import { KibanaContext, IKibanaContext } from '../index'; import { HttpLogic, IHttpLogicValues } from '../shared/http'; +import { AppLogic, IAppActions, IAppValues } from './app_logic'; import { Layout } from '../shared/layout'; import { WorkplaceSearchNav } from './components/layout/nav'; @@ -20,21 +21,19 @@ import { SetupGuide } from './views/setup_guide'; import { ErrorState } from './views/error_state'; import { Overview } from './views/overview'; -export const WorkplaceSearch: React.FC = () => { +export const WorkplaceSearch: React.FC = (props) => { const { config } = useContext(KibanaContext) as IKibanaContext; + return !config.host ? : ; +}; + +export const WorkplaceSearchConfigured: React.FC = (props) => { + const { hasInitialized } = useValues(AppLogic) as IAppValues; + const { initializeAppData } = useActions(AppLogic) as IAppActions; const { errorConnecting } = useValues(HttpLogic) as IHttpLogicValues; - if (!config.host) - return ( - - - - - - - - - ); + useEffect(() => { + if (!hasInitialized) initializeAppData(props); + }, [hasInitialized]); return ( @@ -61,3 +60,14 @@ export const WorkplaceSearch: React.FC = () => { ); }; + +export const WorkplaceSearchUnconfigured: React.FC = () => ( + + + + + + + + +); diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts index c26ada77f504fa..323f79e63bc6f0 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts @@ -47,6 +47,8 @@ describe('callEnterpriseSearchConfigAPI', () => { onboarding_complete: true, }, workplace_search: { + can_create_invitations: true, + is_federated_auth: false, organization: { name: 'ACME Donuts', default_org_name: 'My Organization', @@ -136,6 +138,8 @@ describe('callEnterpriseSearchConfigAPI', () => { }, }, workplaceSearch: { + canCreateInvitations: false, + isFederatedAuth: false, organization: { name: undefined, defaultOrgName: undefined, diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts index 1dbec76806ba8e..c9cbec15169d9a 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts @@ -90,6 +90,8 @@ export const callEnterpriseSearchConfigAPI = async ({ }, }, workplaceSearch: { + canCreateInvitations: !!data?.settings?.workplace_search?.can_create_invitations, + isFederatedAuth: !!data?.settings?.workplace_search?.is_federated_auth, organization: { name: data?.settings?.workplace_search?.organization?.name, defaultOrgName: data?.settings?.workplace_search?.organization?.default_org_name, From d2d7b0decfef5016a7996a284dd13565ac6b43cf Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Wed, 26 Aug 2020 23:33:15 +0200 Subject: [PATCH 049/216] Legacy ES plugin pre-removal cleanup (#75779) * delete integration tests * remove legacy version healthcheck / waitUntilReady * remove handleESError * remove createCluster * no longer depends on kibana plugin * fix kbn_server * remove deprecated comment and dead code * revert code removal, apparently was used (?) * Revert "revert code removal, apparently was used (?)" This reverts commit 69481850 --- .../integration_tests/index.test.ts | 16 ---- .../integration_tests/lib/servers.ts | 16 ---- .../core_plugins/elasticsearch/index.d.ts | 2 - .../core_plugins/elasticsearch/index.js | 33 +------ .../integration_tests/elasticsearch.test.ts | 89 ------------------- .../elasticsearch/lib/version_health_check.js | 39 -------- .../lib/version_health_check.test.js | 71 --------------- .../server/lib/__tests__/handle_es_error.js | 58 ------------ .../server/lib/handle_es_error.js | 50 ----------- src/test_utils/kbn_server.ts | 5 +- 10 files changed, 3 insertions(+), 376 deletions(-) delete mode 100644 src/legacy/core_plugins/elasticsearch/integration_tests/elasticsearch.test.ts delete mode 100644 src/legacy/core_plugins/elasticsearch/lib/version_health_check.js delete mode 100644 src/legacy/core_plugins/elasticsearch/lib/version_health_check.test.js delete mode 100644 src/legacy/core_plugins/elasticsearch/server/lib/__tests__/handle_es_error.js delete mode 100644 src/legacy/core_plugins/elasticsearch/server/lib/handle_es_error.js diff --git a/src/core/server/ui_settings/integration_tests/index.test.ts b/src/core/server/ui_settings/integration_tests/index.test.ts index e704532ee4cdfb..7353f5d3eb760c 100644 --- a/src/core/server/ui_settings/integration_tests/index.test.ts +++ b/src/core/server/ui_settings/integration_tests/index.test.ts @@ -24,22 +24,6 @@ import { docMissingSuite } from './doc_missing'; import { docMissingAndIndexReadOnlySuite } from './doc_missing_and_index_read_only'; describe('uiSettings/routes', function () { - /** - * The "doc missing" and "index missing" tests verify how the uiSettings - * API behaves in between healthChecks, so they interact with the healthCheck - * in somewhat weird ways (can't wait until we get to https://github.com/elastic/kibana/issues/14163) - * - * To make this work we have a `waitUntilNextHealthCheck()` function in ./lib/servers.js - * that deletes the kibana index and then calls `plugins.elasticsearch.waitUntilReady()`. - * - * waitUntilReady() waits for the kibana index to exist and then for the - * elasticsearch plugin to go green. Since we have verified that the kibana index - * does not exist we know that the plugin will also turn yellow while waiting for - * it and then green once the health check is complete, ensuring that we run our - * tests right after the health check. All of this is to say that the tests are - * stupidly fragile and timing sensitive. #14163 should fix that, but until then - * this is the most stable way I've been able to get this to work. - */ jest.setTimeout(10000); beforeAll(startServers); diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts index ea462291059a51..b4cfc3c1efe8b4 100644 --- a/src/core/server/ui_settings/integration_tests/lib/servers.ts +++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts @@ -39,7 +39,6 @@ interface AllServices { savedObjectsClient: SavedObjectsClientContract; callCluster: LegacyAPICaller; uiSettings: IUiSettingsClient; - deleteKibanaIndex: typeof deleteKibanaIndex; } let services: AllServices; @@ -62,20 +61,6 @@ export async function startServers() { kbnServer = kbn.kbnServer; } -async function deleteKibanaIndex(callCluster: LegacyAPICaller) { - const kibanaIndices = await callCluster('cat.indices', { index: '.kibana*', format: 'json' }); - const indexNames = kibanaIndices.map((x: any) => x.index); - if (!indexNames.length) { - return; - } - await callCluster('indices.putSettings', { - index: indexNames, - body: { index: { blocks: { read_only: false } } }, - }); - await callCluster('indices.delete', { index: indexNames }); - return indexNames; -} - export function getServices() { if (services) { return services; @@ -97,7 +82,6 @@ export function getServices() { callCluster, savedObjectsClient, uiSettings, - deleteKibanaIndex, }; return services; diff --git a/src/legacy/core_plugins/elasticsearch/index.d.ts b/src/legacy/core_plugins/elasticsearch/index.d.ts index 683f58b1a80ce9..83e7bb19e57baa 100644 --- a/src/legacy/core_plugins/elasticsearch/index.d.ts +++ b/src/legacy/core_plugins/elasticsearch/index.d.ts @@ -523,6 +523,4 @@ export interface CallCluster { export interface ElasticsearchPlugin { status: { on: (status: string, cb: () => void) => void }; getCluster(name: string): Cluster; - createCluster(name: string, config: ClusterConfig): Cluster; - waitUntilReady(): Promise; } diff --git a/src/legacy/core_plugins/elasticsearch/index.js b/src/legacy/core_plugins/elasticsearch/index.js index eb502e97fb77c7..599886788604bb 100644 --- a/src/legacy/core_plugins/elasticsearch/index.js +++ b/src/legacy/core_plugins/elasticsearch/index.js @@ -19,14 +19,12 @@ import { first } from 'rxjs/operators'; import { Cluster } from './server/lib/cluster'; import { createProxy } from './server/lib/create_proxy'; -import { handleESError } from './server/lib/handle_es_error'; -import { versionHealthCheck } from './lib/version_health_check'; export default function (kibana) { let defaultVars; return new kibana.Plugin({ - require: ['kibana'], + require: [], uiExports: { injectDefaultVars: () => defaultVars }, @@ -61,25 +59,6 @@ export default function (kibana) { return clusters.get(name); }); - server.expose('createCluster', (name, clientConfig = {}) => { - // NOTE: Not having `admin` and `data` clients provided by the core in `clusters` - // map implicitly allows to create custom `data` and `admin` clients. This is - // allowed intentionally to support custom `admin` cluster client created by the - // x-pack/monitoring bulk uploader. We should forbid that as soon as monitoring - // bulk uploader is refactored, see https://github.com/elastic/kibana/issues/31934. - if (clusters.has(name)) { - throw new Error(`cluster '${name}' already exists`); - } - - const cluster = new Cluster( - server.newPlatform.setup.core.elasticsearch.legacy.createClient(name, clientConfig) - ); - - clusters.set(name, cluster); - - return cluster; - }); - server.events.on('stop', () => { for (const cluster of clusters.values()) { cluster.close(); @@ -88,17 +67,7 @@ export default function (kibana) { clusters.clear(); }); - server.expose('handleESError', handleESError); - createProxy(server); - - const waitUntilHealthy = versionHealthCheck( - this, - server.logWithMetadata, - server.newPlatform.__internals.elasticsearch.esNodesCompatibility$ - ); - - server.expose('waitUntilReady', () => waitUntilHealthy); }, }); } diff --git a/src/legacy/core_plugins/elasticsearch/integration_tests/elasticsearch.test.ts b/src/legacy/core_plugins/elasticsearch/integration_tests/elasticsearch.test.ts deleted file mode 100644 index 0331153cdf6152..00000000000000 --- a/src/legacy/core_plugins/elasticsearch/integration_tests/elasticsearch.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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 { - createTestServers, - TestElasticsearchUtils, - TestKibanaUtils, - TestUtils, - createRootWithCorePlugins, - getKbnServer, -} from '../../../../test_utils/kbn_server'; - -import { BehaviorSubject } from 'rxjs'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { NodesVersionCompatibility } from 'src/core/server/elasticsearch/version_check/ensure_es_version'; - -describe('Elasticsearch plugin', () => { - let servers: TestUtils; - let esServer: TestElasticsearchUtils; - let root: TestKibanaUtils['root']; - let elasticsearch: TestKibanaUtils['kbnServer']['server']['plugins']['elasticsearch']; - - const esNodesCompatibility$ = new BehaviorSubject({ - isCompatible: true, - incompatibleNodes: [], - warningNodes: [], - kibanaVersion: '8.0.0', - }); - - beforeAll(async function () { - const settings = { - elasticsearch: {}, - adjustTimeout: (t: any) => { - jest.setTimeout(t); - }, - }; - servers = createTestServers(settings); - esServer = await servers.startES(); - - const elasticsearchSettings = { - hosts: esServer.hosts, - username: esServer.username, - password: esServer.password, - }; - root = createRootWithCorePlugins({ elasticsearch: elasticsearchSettings }); - - const setup = await root.setup(); - setup.elasticsearch.esNodesCompatibility$ = esNodesCompatibility$; - await root.start(); - - elasticsearch = getKbnServer(root).server.plugins.elasticsearch; - }); - - afterAll(async () => { - await esServer.stop(); - await root.shutdown(); - }, 30000); - - it("should set it's status to green when all nodes are compatible", (done) => { - jest.setTimeout(30000); - elasticsearch.status.on('green', () => done()); - }); - - it("should set it's status to red when some nodes aren't compatible", (done) => { - esNodesCompatibility$.next({ - isCompatible: false, - incompatibleNodes: [], - warningNodes: [], - kibanaVersion: '8.0.0', - }); - elasticsearch.status.on('red', () => done()); - }); -}); diff --git a/src/legacy/core_plugins/elasticsearch/lib/version_health_check.js b/src/legacy/core_plugins/elasticsearch/lib/version_health_check.js deleted file mode 100644 index b1a106d2aae5dc..00000000000000 --- a/src/legacy/core_plugins/elasticsearch/lib/version_health_check.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 versionHealthCheck = (esPlugin, logWithMetadata, esNodesCompatibility$) => { - esPlugin.status.yellow('Waiting for Elasticsearch'); - - return new Promise((resolve) => { - esNodesCompatibility$.subscribe(({ isCompatible, message, kibanaVersion, warningNodes }) => { - if (!isCompatible) { - esPlugin.status.red(message); - } else { - if (message) { - logWithMetadata(['warning'], message, { - kibanaVersion, - nodes: warningNodes, - }); - } - esPlugin.status.green('Ready'); - resolve(); - } - }); - }); -}; diff --git a/src/legacy/core_plugins/elasticsearch/lib/version_health_check.test.js b/src/legacy/core_plugins/elasticsearch/lib/version_health_check.test.js deleted file mode 100644 index 4c03c0c0105ee7..00000000000000 --- a/src/legacy/core_plugins/elasticsearch/lib/version_health_check.test.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 { versionHealthCheck } from './version_health_check'; -import { Subject } from 'rxjs'; - -describe('plugins/elasticsearch', () => { - describe('lib/health_version_check', function () { - let plugin; - let logWithMetadata; - - beforeEach(() => { - plugin = { - status: { - red: jest.fn(), - green: jest.fn(), - yellow: jest.fn(), - }, - }; - - logWithMetadata = jest.fn(); - jest.clearAllMocks(); - }); - - it('returned promise resolves when all nodes are compatible ', function () { - const esNodesCompatibility$ = new Subject(); - const versionHealthyPromise = versionHealthCheck( - plugin, - logWithMetadata, - esNodesCompatibility$ - ); - esNodesCompatibility$.next({ isCompatible: true, message: undefined }); - return expect(versionHealthyPromise).resolves.toBe(undefined); - }); - - it('should set elasticsearch plugin status to green when all nodes are compatible', function () { - const esNodesCompatibility$ = new Subject(); - versionHealthCheck(plugin, logWithMetadata, esNodesCompatibility$); - expect(plugin.status.yellow).toHaveBeenCalledWith('Waiting for Elasticsearch'); - expect(plugin.status.green).not.toHaveBeenCalled(); - esNodesCompatibility$.next({ isCompatible: true, message: undefined }); - expect(plugin.status.green).toHaveBeenCalledWith('Ready'); - expect(plugin.status.red).not.toHaveBeenCalled(); - }); - - it('should set elasticsearch plugin status to red when some nodes are incompatible', function () { - const esNodesCompatibility$ = new Subject(); - versionHealthCheck(plugin, logWithMetadata, esNodesCompatibility$); - expect(plugin.status.yellow).toHaveBeenCalledWith('Waiting for Elasticsearch'); - expect(plugin.status.red).not.toHaveBeenCalled(); - esNodesCompatibility$.next({ isCompatible: false, message: 'your nodes are incompatible' }); - expect(plugin.status.red).toHaveBeenCalledWith('your nodes are incompatible'); - expect(plugin.status.green).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/handle_es_error.js b/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/handle_es_error.js deleted file mode 100644 index ccab1a3b830b61..00000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/handle_es_error.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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 expect from '@kbn/expect'; -import { handleESError } from '../handle_es_error'; -import { errors as esErrors } from 'elasticsearch'; - -describe('handleESError', function () { - it('should transform elasticsearch errors into boom errors with the same status code', function () { - const conflict = handleESError(new esErrors.Conflict()); - expect(conflict.isBoom).to.be(true); - expect(conflict.output.statusCode).to.be(409); - - const forbidden = handleESError(new esErrors[403]()); - expect(forbidden.isBoom).to.be(true); - expect(forbidden.output.statusCode).to.be(403); - - const notFound = handleESError(new esErrors.NotFound()); - expect(notFound.isBoom).to.be(true); - expect(notFound.output.statusCode).to.be(404); - - const badRequest = handleESError(new esErrors.BadRequest()); - expect(badRequest.isBoom).to.be(true); - expect(badRequest.output.statusCode).to.be(400); - }); - - it('should return an unknown error without transforming it', function () { - const unknown = new Error('mystery error'); - expect(handleESError(unknown)).to.be(unknown); - }); - - it('should return a boom 503 server timeout error for ES connection errors', function () { - expect(handleESError(new esErrors.ConnectionFault()).output.statusCode).to.be(503); - expect(handleESError(new esErrors.ServiceUnavailable()).output.statusCode).to.be(503); - expect(handleESError(new esErrors.NoConnections()).output.statusCode).to.be(503); - expect(handleESError(new esErrors.RequestTimeout()).output.statusCode).to.be(503); - }); - - it('should throw an error if called with a non-error argument', function () { - expect(handleESError).withArgs('notAnError').to.throwException(); - }); -}); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/handle_es_error.js b/src/legacy/core_plugins/elasticsearch/server/lib/handle_es_error.js deleted file mode 100644 index d76b2a2aa9364c..00000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/handle_es_error.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 Boom from 'boom'; -import _ from 'lodash'; -import { errors as esErrors } from 'elasticsearch'; - -export function handleESError(error) { - if (!(error instanceof Error)) { - throw new Error('Expected an instance of Error'); - } - - if ( - error instanceof esErrors.ConnectionFault || - error instanceof esErrors.ServiceUnavailable || - error instanceof esErrors.NoConnections || - error instanceof esErrors.RequestTimeout - ) { - return Boom.serverUnavailable(error); - } else if ( - error instanceof esErrors.Conflict || - _.includes(error.message, 'index_template_already_exists') - ) { - return Boom.conflict(error); - } else if (error instanceof esErrors[403]) { - return Boom.forbidden(error); - } else if (error instanceof esErrors.NotFound) { - return Boom.notFound(error); - } else if (error instanceof esErrors.BadRequest) { - return Boom.badRequest(error); - } else { - return error; - } -} diff --git a/src/test_utils/kbn_server.ts b/src/test_utils/kbn_server.ts index e337a469f17e6e..e44ce0de403d94 100644 --- a/src/test_utils/kbn_server.ts +++ b/src/test_utils/kbn_server.ts @@ -32,10 +32,10 @@ import { defaultsDeep, get } from 'lodash'; import { resolve } from 'path'; import { BehaviorSubject } from 'rxjs'; import supertest from 'supertest'; +import { LegacyAPICaller } from '../core/server'; import { CliArgs, Env } from '../core/server/config'; import { Root } from '../core/server/root'; import KbnServer from '../legacy/server/kbn_server'; -import { CallCluster } from '../legacy/core_plugins/elasticsearch'; export type HttpMethod = 'delete' | 'get' | 'head' | 'post' | 'put'; @@ -156,7 +156,7 @@ export interface TestElasticsearchServer { stop: () => Promise; cleanup: () => Promise; getClient: () => Client; - getCallCluster: () => CallCluster; + getCallCluster: () => LegacyAPICaller; getUrl: () => string; } @@ -292,7 +292,6 @@ export function createTestServers({ await root.start(); const kbnServer = getKbnServer(root); - await kbnServer.server.plugins.elasticsearch.waitUntilReady(); return { root, From 595dfdb023d472c9f0bbecdb4201947b76435f09 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 26 Aug 2020 14:37:55 -0700 Subject: [PATCH 050/216] Disables Chromedriver version detection (#75984) Signed-off-by: Tyler Smalley --- src/dev/ci_setup/setup.sh | 6 ++++++ src/dev/ci_setup/setup_env.sh | 14 +++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index aabc1e75b90255..3351170c29e01a 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -16,6 +16,12 @@ echo " -- TEST_ES_SNAPSHOT_VERSION='$TEST_ES_SNAPSHOT_VERSION'" echo " -- installing node.js dependencies" yarn kbn bootstrap --prefer-offline +### +### ensure Chromedriver install hook is triggered +### when modules are up-to-date +### +node node_modules/chromedriver/install.js + ### ### Download es snapshots ### diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 72ec73ad810e6d..5757d72f995827 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -134,13 +134,13 @@ export CYPRESS_DOWNLOAD_MIRROR="https://us-central1-elastic-kibana-184716.cloudf export CHECKS_REPORTER_ACTIVE=false # This is mainly for release-manager builds, which run in an environment that doesn't have Chrome installed -if [[ "$(which google-chrome-stable)" || "$(which google-chrome)" ]]; then - echo "Chrome detected, setting DETECT_CHROMEDRIVER_VERSION=true" - export DETECT_CHROMEDRIVER_VERSION=true - export CHROMEDRIVER_FORCE_DOWNLOAD=true -else - echo "Chrome not detected, installing default chromedriver binary for the package version" -fi +# if [[ "$(which google-chrome-stable)" || "$(which google-chrome)" ]]; then +# echo "Chrome detected, setting DETECT_CHROMEDRIVER_VERSION=true" +# export DETECT_CHROMEDRIVER_VERSION=true +# export CHROMEDRIVER_FORCE_DOWNLOAD=true +# else +# echo "Chrome not detected, installing default chromedriver binary for the package version" +# fi ### only run on pr jobs for elastic/kibana, checks-reporter doesn't work for other repos if [[ "$ghprbPullId" && "$ghprbGhRepository" == 'elastic/kibana' ]] ; then From 979d1dbca801839d0f896599665c564639b3a973 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Wed, 26 Aug 2020 18:18:39 -0400 Subject: [PATCH 051/216] [Security Solution] [Detections] Updates rules routes to validate "from" param on rules (#76000) * updates validation on 'from' param to prevent malformed datemath strings from being accepted * fix imports * copy paste is not my friend * missed type check somehow * forgot to mock common utils * updates bodies for request validation tests --- .../schemas/common/schemas.ts | 17 ++++++++- .../schemas/types/default_from_string.ts | 10 +++-- .../common/detection_engine/utils.ts | 15 ++++++++ .../rules_notification_alert_type.ts | 2 +- .../rules/create_rules_bulk_route.test.ts | 27 ++++++++++++++ .../routes/rules/create_rules_route.test.ts | 25 +++++++++++++ .../rules/patch_rules_bulk_route.test.ts | 27 ++++++++++++++ .../routes/rules/patch_rules_route.test.ts | 33 +++++++++++++++-- .../rules/update_rules_bulk_route.test.ts | 28 ++++++++++++++ .../routes/rules/update_rules_route.test.ts | 34 +++++++++++++++-- .../signals/signal_rule_alert_type.test.ts | 3 +- .../signals/signal_rule_alert_type.ts | 2 +- .../detection_engine/signals/utils.test.ts | 2 +- .../lib/detection_engine/signals/utils.ts | 14 +------ .../basic/tests/import_rules.ts | 37 +++++++++++++++++++ 15 files changed, 246 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 2a0d1ef8b9dfde..64f2f223a3073c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -7,11 +7,14 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + import { RiskScore } from '../types/risk_score'; import { UUID } from '../types/uuid'; import { IsoDateString } from '../types/iso_date_string'; import { PositiveIntegerGreaterThanZero } from '../types/positive_integer_greater_than_zero'; import { PositiveInteger } from '../types/positive_integer'; +import { parseScheduleDates } from '../../utils'; export const author = t.array(t.string); export type Author = t.TypeOf; @@ -76,8 +79,18 @@ export const action = t.exact( export const actions = t.array(action); export type Actions = t.TypeOf; -// TODO: Create a regular expression type or custom date math part type here -export const from = t.string; +const stringValidator = (input: unknown): input is string => typeof input === 'string'; +export const from = new t.Type( + 'From', + t.string.is, + (input, context): Either => { + if (stringValidator(input) && parseScheduleDates(input) == null) { + return t.failure(input, context, 'Failed to parse "from" on rule param'); + } + return t.string.validate(input, context); + }, + t.identity +); export type From = t.TypeOf; export const fromOrUndefined = t.union([from, t.undefined]); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_from_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_from_string.ts index a85ea58b26478b..5b1c837db9f745 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_from_string.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_from_string.ts @@ -6,7 +6,7 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; - +import { from } from '../common/schemas'; /** * Types the DefaultFromString as: * - If null or undefined, then a default of the string "now-6m" will be used @@ -14,7 +14,11 @@ import { Either } from 'fp-ts/lib/Either'; export const DefaultFromString = new t.Type( 'DefaultFromString', t.string.is, - (input, context): Either => - input == null ? t.success('now-6m') : t.string.validate(input, context), + (input, context): Either => { + if (input == null) { + return t.success('now-6m'); + } + return from.validate(input, context); + }, t.identity ); diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index 153130fc16d603..a70258c2684b6c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; +import dateMath from '@elastic/datemath'; + import { EntriesArray } from '../shared_imports'; import { RuleType } from './types'; @@ -18,3 +21,15 @@ export const hasNestedEntry = (entries: EntriesArray): boolean => { }; export const isThresholdRule = (ruleType: RuleType) => ruleType === 'threshold'; + +export const parseScheduleDates = (time: string): moment.Moment | null => { + const isValidDateString = !isNaN(Date.parse(time)); + const isValidInput = isValidDateString || time.trim().startsWith('now'); + const formattedDate = isValidDateString + ? moment(time) + : isValidInput + ? dateMath.parse(time) + : null; + + return formattedDate ?? null; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts index 2eb34529d044c5..0a899562d61c2d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts @@ -14,7 +14,7 @@ import { RuleAlertAttributes } from '../signals/types'; import { siemRuleActionGroups } from '../signals/siem_rule_action_groups'; import { scheduleNotificationActions } from './schedule_notification_actions'; import { getNotificationResultsLink } from './utils'; -import { parseScheduleDates } from '../signals/utils'; +import { parseScheduleDates } from '../../../../common/detection_engine/utils'; export const rulesNotificationAlertType = ({ logger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index 4636618cc5ac03..06fcba36642caa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -161,6 +161,17 @@ describe('create_rules_bulk', () => { expect(result.ok).toHaveBeenCalled(); }); + test('allows rule type of query and custom from and interval', async () => { + const request = requestMock.create({ + method: 'post', + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + body: [{ from: 'now-7m', interval: '5m', ...getCreateRulesSchemaMock() }], + }); + const result = server.validate(request); + + expect(result.ok).toHaveBeenCalled(); + }); + test('disallows unknown rule type', async () => { const request = requestMock.create({ method: 'post', @@ -173,5 +184,21 @@ describe('create_rules_bulk', () => { 'Invalid value "unexpected_type" supplied to "type"' ); }); + + test('disallows invalid "from" param on rule', async () => { + const request = requestMock.create({ + method: 'post', + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + body: [ + { + from: 'now-3755555555555555.67s', + interval: '5m', + ...getCreateRulesSchemaMock(), + }, + ], + }); + const result = server.validate(request); + expect(result.badRequest).toHaveBeenCalledWith('Failed to parse "from" on rule param'); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 59c64fbf8fce1d..26febb0999ac70 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -164,5 +164,30 @@ describe('create_rules', () => { 'Invalid value "unexpected_type" supplied to "type"' ); }); + + test('allows rule type of query and custom from and interval', async () => { + const request = requestMock.create({ + method: 'post', + path: DETECTION_ENGINE_RULES_URL, + body: { from: 'now-7m', interval: '5m', ...getCreateRulesSchemaMock() }, + }); + const result = server.validate(request); + + expect(result.ok).toHaveBeenCalled(); + }); + + test('disallows invalid "from" param on rule', async () => { + const request = requestMock.create({ + method: 'post', + path: DETECTION_ENGINE_RULES_URL, + body: { + from: 'now-3755555555555555.67s', + interval: '5m', + ...getCreateRulesSchemaMock(), + }, + }); + const result = server.validate(request); + expect(result.badRequest).toHaveBeenCalledWith('Failed to parse "from" on rule param'); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts index db32f7f4485b16..c162caa1278e6e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts @@ -183,5 +183,32 @@ describe('patch_rules_bulk', () => { 'Invalid value "unknown_type" supplied to "type"' ); }); + + test('allows rule type of query and custom from and interval', async () => { + const request = requestMock.create({ + method: 'patch', + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + body: [{ from: 'now-7m', interval: '5m', ...getCreateRulesSchemaMock() }], + }); + const result = server.validate(request); + + expect(result.ok).toHaveBeenCalled(); + }); + + test('disallows invalid "from" param on rule', async () => { + const request = requestMock.create({ + method: 'patch', + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + body: [ + { + from: 'now-3755555555555555.67s', + interval: '5m', + ...getCreateRulesSchemaMock(), + }, + ], + }); + const result = server.validate(request); + expect(result.badRequest).toHaveBeenCalledWith('Failed to parse "from" on rule param'); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index d3350bcb0d7621..a406de593652be 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -18,7 +18,7 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { patchRulesRoute } from './patch_rules_route'; -import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getPatchRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/patch_rules_schema.mock'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -156,7 +156,7 @@ describe('patch_rules', () => { const request = requestMock.create({ method: 'patch', path: DETECTION_ENGINE_RULES_URL, - body: { ...getCreateRulesSchemaMock(), rule_id: undefined }, + body: { ...getPatchRulesSchemaMock(), rule_id: undefined }, }); const response = await server.inject(request, context); expect(response.body).toEqual({ @@ -169,7 +169,7 @@ describe('patch_rules', () => { const request = requestMock.create({ method: 'patch', path: DETECTION_ENGINE_RULES_URL, - body: { ...getCreateRulesSchemaMock(), type: 'query' }, + body: { ...getPatchRulesSchemaMock(), type: 'query' }, }); const result = server.validate(request); @@ -180,7 +180,7 @@ describe('patch_rules', () => { const request = requestMock.create({ method: 'patch', path: DETECTION_ENGINE_RULES_URL, - body: { ...getCreateRulesSchemaMock(), type: 'unknown_type' }, + body: { ...getPatchRulesSchemaMock(), type: 'unknown_type' }, }); const result = server.validate(request); @@ -188,5 +188,30 @@ describe('patch_rules', () => { 'Invalid value "unknown_type" supplied to "type"' ); }); + + test('allows rule type of query and custom from and interval', async () => { + const request = requestMock.create({ + method: 'patch', + path: DETECTION_ENGINE_RULES_URL, + body: { from: 'now-7m', interval: '5m', ...getPatchRulesSchemaMock() }, + }); + const result = server.validate(request); + + expect(result.ok).toHaveBeenCalled(); + }); + + test('disallows invalid "from" param on rule', async () => { + const request = requestMock.create({ + method: 'patch', + path: DETECTION_ENGINE_RULES_URL, + body: { + from: 'now-3755555555555555.67s', + interval: '5m', + ...getPatchRulesSchemaMock(), + }, + }); + const result = server.validate(request); + expect(result.badRequest).toHaveBeenCalledWith('Failed to parse "from" on rule param'); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index 9c5df89a52bed5..ec5a2be255a2cf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -154,5 +154,33 @@ describe('update_rules_bulk', () => { 'Invalid value "unknown_type" supplied to "type"' ); }); + + test('allows rule type of query and custom from and interval', async () => { + const request = requestMock.create({ + method: 'put', + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + body: [{ from: 'now-7m', interval: '5m', ...getCreateRulesSchemaMock(), type: 'query' }], + }); + const result = server.validate(request); + + expect(result.ok).toHaveBeenCalled(); + }); + + test('disallows invalid "from" param on rule', async () => { + const request = requestMock.create({ + method: 'put', + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + body: [ + { + from: 'now-3755555555555555.67s', + interval: '5m', + ...getCreateRulesSchemaMock(), + type: 'query', + }, + ], + }); + const result = server.validate(request); + expect(result.badRequest).toHaveBeenCalledWith('Failed to parse "from" on rule param'); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index 46fe773e1a88df..fd077c18b7983e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -19,7 +19,7 @@ import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { updateRulesRoute } from './update_rules_route'; -import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); jest.mock('../../rules/update_rules_notifications'); @@ -130,7 +130,7 @@ describe('update_rules', () => { method: 'put', path: DETECTION_ENGINE_RULES_URL, body: { - ...getCreateRulesSchemaMock(), + ...getUpdateRulesSchemaMock(), rule_id: undefined, }, }); @@ -145,7 +145,7 @@ describe('update_rules', () => { const request = requestMock.create({ method: 'put', path: DETECTION_ENGINE_RULES_URL, - body: { ...getCreateRulesSchemaMock(), type: 'query' }, + body: { ...getUpdateRulesSchemaMock(), type: 'query' }, }); const result = await server.validate(request); @@ -156,7 +156,7 @@ describe('update_rules', () => { const request = requestMock.create({ method: 'put', path: DETECTION_ENGINE_RULES_URL, - body: { ...getCreateRulesSchemaMock(), type: 'unknown type' }, + body: { ...getUpdateRulesSchemaMock(), type: 'unknown type' }, }); const result = await server.validate(request); @@ -164,5 +164,31 @@ describe('update_rules', () => { 'Invalid value "unknown type" supplied to "type"' ); }); + + test('allows rule type of query and custom from and interval', async () => { + const request = requestMock.create({ + method: 'put', + path: DETECTION_ENGINE_RULES_URL, + body: { from: 'now-7m', interval: '5m', ...getUpdateRulesSchemaMock(), type: 'query' }, + }); + const result = server.validate(request); + + expect(result.ok).toHaveBeenCalled(); + }); + + test('disallows invalid "from" param on rule', async () => { + const request = requestMock.create({ + method: 'put', + path: DETECTION_ENGINE_RULES_URL, + body: { + from: 'now-3755555555555555.67s', + interval: '5m', + ...getUpdateRulesSchemaMock(), + type: 'query', + }, + }); + const result = server.validate(request); + expect(result.badRequest).toHaveBeenCalledWith('Failed to parse "from" on rule param'); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index b29d15f5e5c727..a7213c30eb3fb2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -16,8 +16,8 @@ import { getListsClient, getExceptions, sortExceptionItems, - parseScheduleDates, } from './utils'; +import { parseScheduleDates } from '../../../../common/detection_engine/utils'; import { RuleExecutorOptions } from './types'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; import { scheduleNotificationActions } from '../notifications/schedule_notification_actions'; @@ -37,6 +37,7 @@ jest.mock('./utils'); jest.mock('../notifications/schedule_notification_actions'); jest.mock('./find_ml_signals'); jest.mock('./bulk_create_ml_signals'); +jest.mock('./../../../../common/detection_engine/utils'); const getPayload = (ruleAlert: RuleAlertType, services: AlertServicesMock) => ({ alertId: ruleAlert.id, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index b5cbf80b084f79..c5124edcaf187a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -14,6 +14,7 @@ import { SERVER_APP_ID, } from '../../../../common/constants'; import { isJobStarted, isMlRule } from '../../../../common/machine_learning/helpers'; +import { parseScheduleDates } from '../../../../common/detection_engine/utils'; import { SetupPlugins } from '../../../plugin'; import { getInputIndex } from './get_input_output_index'; import { @@ -24,7 +25,6 @@ import { getFilter } from './get_filter'; import { SignalRuleAlertTypeDefinition, RuleAlertAttributes } from './types'; import { getGapBetweenRuns, - parseScheduleDates, getListsClient, getExceptions, getGapMaxCatchupRatio, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 3c41f29625a51e..a2e2fec3309c31 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -13,11 +13,11 @@ import { buildRuleMessageFactory } from './rule_messages'; import { ExceptionListClient } from '../../../../../lists/server'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { parseScheduleDates } from '../../../../common/detection_engine/utils'; import { generateId, parseInterval, - parseScheduleDates, getDriftTolerance, getGapBetweenRuns, getGapMaxCatchupRatio, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 9519720d0bbecd..92cc9be69839f3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -14,7 +14,7 @@ import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { ListArrayOrUndefined } from '../../../../common/detection_engine/schemas/types/lists'; import { BulkResponse, BulkResponseErrorAggregation, isValidUnit } from './types'; import { BuildRuleMessage } from './rule_messages'; -import { hasLargeValueList } from '../../../../common/detection_engine/utils'; +import { hasLargeValueList, parseScheduleDates } from '../../../../common/detection_engine/utils'; import { MAX_EXCEPTION_LIST_SIZE } from '../../../../../lists/common/constants'; interface SortExceptionsReturn { @@ -220,18 +220,6 @@ export const parseInterval = (intervalString: string): moment.Duration | null => } }; -export const parseScheduleDates = (time: string): moment.Moment | null => { - const isValidDateString = !isNaN(Date.parse(time)); - const isValidInput = isValidDateString || time.trim().startsWith('now'); - const formattedDate = isValidDateString - ? moment(time) - : isValidInput - ? dateMath.parse(time) - : null; - - return formattedDate ?? null; -}; - export const getDriftTolerance = ({ from, to, diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts index e0b60ae1fbeebb..108ca365bc14ff 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts @@ -141,6 +141,43 @@ export default ({ getService }: FtrProviderContext): void => { expect(bodyToCompare).to.eql(getSimpleRuleOutput('rule-1')); }); + it('should fail validation when importing a rule with malformed "from" params on the rules', async () => { + const stringifiedRule = JSON.stringify({ + from: 'now-3755555555555555.67s', + interval: '5m', + ...getSimpleRule('rule-1'), + }); + const fileNdJson = Buffer.from(stringifiedRule + '\n'); + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', fileNdJson, 'rules.ndjson') + .expect(200); + + expect(body.errors[0].error.message).to.eql('Failed to parse "from" on rule param'); + }); + + it('should fail validation when importing two rules and one has a malformed "from" params', async () => { + const stringifiedRule = JSON.stringify({ + from: 'now-3755555555555555.67s', + interval: '5m', + ...getSimpleRule('rule-1'), + }); + const stringifiedRule2 = JSON.stringify({ + ...getSimpleRule('rule-2'), + }); + const fileNdJson = Buffer.from([stringifiedRule, stringifiedRule2].join('\n')); + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', fileNdJson, 'rules.ndjson') + .expect(200); + + // should result in one success and a failure message + expect(body.success_count).to.eql(1); + expect(body.errors[0].error.message).to.eql('Failed to parse "from" on rule param'); + }); + it('should be able to import two rules', async () => { const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) From 8364d8d67acb3d905a08e020eb1f906d82cd1a0c Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Wed, 26 Aug 2020 18:27:40 -0400 Subject: [PATCH 052/216] [Lens] Decouple visualizations from specific operations (#75703) * [Lens] Decouple visualizations from specific operations * Remove unused mock --- .../expression.test.tsx | 52 +++++++++++++++++++ .../datatable_visualization/expression.tsx | 5 +- .../pie_visualization/suggestions.test.ts | 36 ++++++++++++- .../public/pie_visualization/suggestions.ts | 2 +- x-pack/plugins/lens/public/types.ts | 5 ++ .../xy_visualization/xy_suggestions.test.ts | 39 ++++++++++++++ .../public/xy_visualization/xy_suggestions.ts | 10 ++-- 7 files changed, 139 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx index ac435932136879..b9bdea5522f321 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx @@ -144,6 +144,58 @@ describe('datatable_expression', () => { }); }); + test('it invokes executeTriggerActions with correct context on click on timefield from range', () => { + const data: LensMultiTable = { + type: 'lens_multitable', + tables: { + l1: { + type: 'kibana_datatable', + columns: [ + { id: 'a', name: 'a', meta: { type: 'date_range', aggConfigParams: { field: 'a' } } }, + { id: 'b', name: 'b', meta: { type: 'count' } }, + ], + rows: [{ a: 1588024800000, b: 3 }], + }, + }, + }; + + const args: DatatableProps['args'] = { + title: '', + columns: { columnIds: ['a', 'b'], type: 'lens_datatable_columns' }, + }; + + const wrapper = mountWithIntl( + x as IFieldFormat} + onClickValue={onClickValue} + getType={jest.fn(() => ({ type: 'buckets' } as IAggType))} + /> + ); + + wrapper.find('[data-test-subj="lensDatatableFilterFor"]').at(1).simulate('click'); + + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 0, + row: 0, + table: data.tables.l1, + value: 1588024800000, + }, + ], + negate: false, + timeFieldName: 'a', + }); + }); + test('it shows emptyPlaceholder for undefined bucketed data', () => { const { args, data } = sampleArgs(); const emptyData: LensMultiTable = { diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index 02186ecf09b4bc..87ac2d1710b19b 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -164,9 +164,8 @@ export function DatatableComponent(props: DatatableRenderProps) { const handleFilterClick = useMemo( () => (field: string, value: unknown, colIndex: number, negate: boolean = false) => { const col = firstTable.columns[colIndex]; - const isDateHistogram = col.meta?.type === 'date_histogram'; - const timeFieldName = - negate && isDateHistogram ? undefined : col?.meta?.aggConfigParams?.field; + const isDate = col.meta?.type === 'date_histogram' || col.meta?.type === 'date_range'; + const timeFieldName = negate && isDate ? undefined : col?.meta?.aggConfigParams?.field; const rowIndex = firstTable.rows.findIndex((row) => row[field] === value); const data: LensFilterEvent['data'] = { diff --git a/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts b/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts index 20b267caa9074a..b8b43c3ed248b1 100644 --- a/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts @@ -90,7 +90,41 @@ describe('suggestions', () => { columns: [ { columnId: 'b', - operation: { label: 'Days', dataType: 'date' as DataType, isBucketed: true }, + operation: { + label: 'Days', + dataType: 'date' as DataType, + isBucketed: true, + scale: 'interval', + }, + }, + { + columnId: 'c', + operation: { label: 'Count', dataType: 'number' as DataType, isBucketed: false }, + }, + ], + changeType: 'initial', + }, + state: undefined, + keptLayerIds: ['first'], + }) + ).toHaveLength(0); + }); + + it('should reject any histogram operations', () => { + expect( + suggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [ + { + columnId: 'b', + operation: { + label: 'Durations', + dataType: 'number' as DataType, + isBucketed: true, + scale: 'interval', + }, }, { columnId: 'c', diff --git a/x-pack/plugins/lens/public/pie_visualization/suggestions.ts b/x-pack/plugins/lens/public/pie_visualization/suggestions.ts index 5d85ac3bbd56ab..067b0bb4906df7 100644 --- a/x-pack/plugins/lens/public/pie_visualization/suggestions.ts +++ b/x-pack/plugins/lens/public/pie_visualization/suggestions.ts @@ -15,7 +15,7 @@ function shouldReject({ table, keptLayerIds }: SuggestionRequest 1 || (keptLayerIds.length && table.layerId !== keptLayerIds[0]) || table.changeType === 'reorder' || - table.columns.some((col) => col.operation.dataType === 'date') + table.columns.some((col) => col.operation.scale === 'interval') // Histograms are not good for pie ); } diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 20f2ce6c567740..729daed7223fe3 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -259,6 +259,11 @@ export interface OperationMetadata { // A bucketed operation is grouped by duplicate values, otherwise each row is // treated as unique isBucketed: boolean; + /** + * ordinal: Each name is a unique value, but the names are in sorted order, like "Top values" + * interval: Histogram data, like date or number histograms + * ratio: Most number data is rendered as a ratio that includes 0 + */ scale?: 'ordinal' | 'interval' | 'ratio'; // Extra meta-information like cardinality, color // TODO currently it's not possible to differentiate between a field from a raw diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index 632f6fc8861a4f..79e4ed69581934 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -54,6 +54,18 @@ describe('xy_suggestions', () => { }; } + function histogramCol(columnId: string): TableSuggestionColumn { + return { + columnId, + operation: { + dataType: 'number', + isBucketed: true, + label: `${columnId} histogram`, + scale: 'interval', + }, + }; + } + // Helper that plucks out the important part of a suggestion for // most test assertions function suggestionSubset(suggestion: VisualizationSuggestion) { @@ -274,6 +286,33 @@ describe('xy_suggestions', () => { `); }); + test('suggests all basic x y chart with histogram on x', () => { + (generateId as jest.Mock).mockReturnValueOnce('aaa'); + const [suggestion, ...rest] = getSuggestions({ + table: { + isMultiRow: true, + columns: [numCol('bytes'), histogramCol('duration')], + layerId: 'first', + changeType: 'unchanged', + }, + keptLayerIds: [], + }); + + expect(rest).toHaveLength(visualizationTypes.length - 1); + expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` + Array [ + Object { + "seriesType": "bar_stacked", + "splitAccessor": undefined, + "x": "duration", + "y": Array [ + "bytes", + ], + }, + ] + `); + }); + test('does not suggest multiple splits', () => { const suggestions = getSuggestions({ table: { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index 387d56c03e31a7..75dd5a7a579b81 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -112,13 +112,13 @@ function getBucketMappings(table: TableSuggestion, currentState?: State) { const currentXColumnIndex = prioritizedBuckets.findIndex( ({ columnId }) => columnId === currentLayer.xAccessor ); - const currentXDataType = - currentXColumnIndex > -1 && prioritizedBuckets[currentXColumnIndex].operation.dataType; + const currentXScaleType = + currentXColumnIndex > -1 && prioritizedBuckets[currentXColumnIndex].operation.scale; if ( - currentXDataType && - // make sure time gets mapped to x dimension even when changing current bucket/dimension mapping - (currentXDataType === 'date' || prioritizedBuckets[0].operation.dataType !== 'date') + currentXScaleType && + // make sure histograms get mapped to x dimension even when changing current bucket/dimension mapping + (currentXScaleType === 'interval' || prioritizedBuckets[0].operation.scale !== 'interval') ) { const [x] = prioritizedBuckets.splice(currentXColumnIndex, 1); prioritizedBuckets.unshift(x); From 043382d686d8c7cd1d5bc5918d813e0654334b77 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 26 Aug 2020 18:46:15 -0400 Subject: [PATCH 053/216] [Security Solution][Exceptions] - Improve UX for missing exception list associated with rule (#76012) ## Summary **Current behavior:** - **Scenario 1:** User is in the exceptions viewer flow, they select to edit an exception item, but the list the item is associated with has since been deleted (let's say by another user) - a user is able to open modal to edit exception item and on save, an error toaster shows but no information is given to the user to indicate the issue. - **Scenario 2:** User exports rules from space 'X' and imports into space 'Y'. The exception lists associated with their newly imported rules do not exist in space 'Y' - a user goes to add an exception item and gets a modal with an error, unable to add any exceptions. - **Workaround:** current workaround exists only via API - user would need to remove the exception list from their rule via API **New behavior:** - **Scenario 1:** User is still able to oped edit modal, but on save they see an error explaining that the associated exception list does not exist and prompts them to remove the exception list --> now they're able to add exceptions to their rule - **Scenario 2:** User navigates to exceptions after importing their rule, tries to add exception, modal pops up with error informing them that they need to remove association to missing exception list, button prompts them to do so --> now can continue adding exceptions to rule --- .../exceptions/add_exception_modal/index.tsx | 90 +++++++--- .../edit_exception_modal/index.test.tsx | 5 + .../exceptions/edit_exception_modal/index.tsx | 91 +++++++--- .../exceptions/error_callout.test.tsx | 160 +++++++++++++++++ .../components/exceptions/error_callout.tsx | 169 ++++++++++++++++++ .../components/exceptions/translations.ts | 49 +++++ .../exceptions/use_add_exception.test.tsx | 44 +++++ .../exceptions/use_add_exception.tsx | 8 +- ...tch_or_create_rule_exception_list.test.tsx | 2 +- ...se_fetch_or_create_rule_exception_list.tsx | 8 +- .../components/exceptions/viewer/index.tsx | 2 + .../use_dissasociate_exception_list.test.tsx | 52 ++++++ .../rules/use_dissasociate_exception_list.tsx | 80 +++++++++ 13 files changed, 706 insertions(+), 54 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 03051ead357c96..21f82c6ab4c986 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -18,7 +18,6 @@ import { EuiCheckbox, EuiSpacer, EuiFormRow, - EuiCallOut, EuiText, } from '@elastic/eui'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; @@ -28,6 +27,7 @@ import { ExceptionListType, } from '../../../../../public/lists_plugin_deps'; import * as i18n from './translations'; +import * as sharedI18n from '../translations'; import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; import { useAppToasts } from '../../../hooks/use_app_toasts'; import { useKibana } from '../../../lib/kibana'; @@ -35,6 +35,7 @@ import { ExceptionBuilderComponent } from '../builder'; import { Loader } from '../../loader'; import { useAddOrUpdateException } from '../use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; +import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list'; import { AddExceptionComments } from '../add_exception_comments'; import { @@ -46,6 +47,7 @@ import { entryHasNonEcsType, getMappedNonEcsValue, } from '../helpers'; +import { ErrorInfo, ErrorCallout } from '../error_callout'; import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; export interface AddExceptionModalBaseProps { @@ -107,13 +109,14 @@ export const AddExceptionModal = memo(function AddExceptionModal({ }: AddExceptionModalProps) { const { http } = useKibana().services; const [comment, setComment] = useState(''); + const { rule: maybeRule } = useRuleAsync(ruleId); const [shouldCloseAlert, setShouldCloseAlert] = useState(false); const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false); const [shouldDisableBulkClose, setShouldDisableBulkClose] = useState(false); const [exceptionItemsToAdd, setExceptionItemsToAdd] = useState< Array >([]); - const [fetchOrCreateListError, setFetchOrCreateListError] = useState(false); + const [fetchOrCreateListError, setFetchOrCreateListError] = useState(null); const { addError, addSuccess } = useAppToasts(); const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); const [ @@ -164,17 +167,41 @@ export const AddExceptionModal = memo(function AddExceptionModal({ }, [onRuleChange] ); - const onFetchOrCreateExceptionListError = useCallback( - (error: Error) => { - setFetchOrCreateListError(true); + + const handleDissasociationSuccess = useCallback( + (id: string): void => { + handleRuleChange(true); + addSuccess(sharedI18n.DISSASOCIATE_LIST_SUCCESS(id)); + onCancel(); + }, + [handleRuleChange, addSuccess, onCancel] + ); + + const handleDissasociationError = useCallback( + (error: Error): void => { + addError(error, { title: sharedI18n.DISSASOCIATE_EXCEPTION_LIST_ERROR }); + onCancel(); + }, + [addError, onCancel] + ); + + const handleFetchOrCreateExceptionListError = useCallback( + (error: Error, statusCode: number | null, message: string | null) => { + setFetchOrCreateListError({ + reason: error.message, + code: statusCode, + details: message, + listListId: null, + }); }, [setFetchOrCreateListError] ); + const [isLoadingExceptionList, ruleExceptionList] = useFetchOrCreateRuleExceptionList({ http, ruleId, exceptionListType, - onError: onFetchOrCreateExceptionListError, + onError: handleFetchOrCreateExceptionListError, onSuccess: handleRuleChange, }); @@ -279,7 +306,9 @@ export const AddExceptionModal = memo(function AddExceptionModal({ ]); const isSubmitButtonDisabled = useMemo( - () => fetchOrCreateListError || exceptionItemsToAdd.every((item) => item.entries.length === 0), + () => + fetchOrCreateListError != null || + exceptionItemsToAdd.every((item) => item.entries.length === 0), [fetchOrCreateListError, exceptionItemsToAdd] ); @@ -295,19 +324,27 @@ export const AddExceptionModal = memo(function AddExceptionModal({ - {fetchOrCreateListError === true && ( - -

{i18n.ADD_EXCEPTION_FETCH_ERROR}

-
+ {fetchOrCreateListError != null && ( + + + )} - {fetchOrCreateListError === false && + {fetchOrCreateListError == null && (isLoadingExceptionList || isIndexPatternLoading || isSignalIndexLoading || isSignalIndexPatternLoading) && ( )} - {fetchOrCreateListError === false && + {fetchOrCreateListError == null && !isSignalIndexLoading && !isSignalIndexPatternLoading && !isLoadingExceptionList && @@ -377,20 +414,21 @@ export const AddExceptionModal = memo(function AddExceptionModal({ )} + {fetchOrCreateListError == null && ( + + {i18n.CANCEL} - - {i18n.CANCEL} - - - {i18n.ADD_EXCEPTION} - - + + {i18n.ADD_EXCEPTION} + + + )} ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx index 6ff218ca06059f..c724e6a2c711fd 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx @@ -77,6 +77,7 @@ describe('When the edit exception modal is opened', () => { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> void; onConfirm: () => void; + onRuleChange?: () => void; } const Modal = styled(EuiModal)` @@ -83,14 +88,18 @@ const ModalBodySection = styled.section` export const EditExceptionModal = memo(function EditExceptionModal({ ruleName, + ruleId, ruleIndices, exceptionItem, exceptionListType, onCancel, onConfirm, + onRuleChange, }: EditExceptionModalProps) { const { http } = useKibana().services; const [comment, setComment] = useState(''); + const { rule: maybeRule } = useRuleAsync(ruleId); + const [updateError, setUpdateError] = useState(null); const [hasVersionConflict, setHasVersionConflict] = useState(false); const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false); const [shouldDisableBulkClose, setShouldDisableBulkClose] = useState(false); @@ -108,18 +117,44 @@ export const EditExceptionModal = memo(function EditExceptionModal({ 'rules' ); - const onError = useCallback( - (error) => { + const handleExceptionUpdateError = useCallback( + (error: Error, statusCode: number | null, message: string | null) => { if (error.message.includes('Conflict')) { setHasVersionConflict(true); } else { - addError(error, { title: i18n.EDIT_EXCEPTION_ERROR }); - onCancel(); + setUpdateError({ + reason: error.message, + code: statusCode, + details: message, + listListId: exceptionItem.list_id, + }); } }, + [setUpdateError, setHasVersionConflict, exceptionItem.list_id] + ); + + const handleDissasociationSuccess = useCallback( + (id: string): void => { + addSuccess(sharedI18n.DISSASOCIATE_LIST_SUCCESS(id)); + + if (onRuleChange) { + onRuleChange(); + } + + onCancel(); + }, + [addSuccess, onCancel, onRuleChange] + ); + + const handleDissasociationError = useCallback( + (error: Error): void => { + addError(error, { title: sharedI18n.DISSASOCIATE_EXCEPTION_LIST_ERROR }); + onCancel(); + }, [addError, onCancel] ); - const onSuccess = useCallback(() => { + + const handleExceptionUpdateSuccess = useCallback((): void => { addSuccess(i18n.EDIT_EXCEPTION_SUCCESS); onConfirm(); }, [addSuccess, onConfirm]); @@ -127,8 +162,8 @@ export const EditExceptionModal = memo(function EditExceptionModal({ const [{ isLoading: addExceptionIsLoading }, addOrUpdateExceptionItems] = useAddOrUpdateException( { http, - onSuccess, - onError, + onSuccess: handleExceptionUpdateSuccess, + onError: handleExceptionUpdateError, } ); @@ -222,11 +257,9 @@ export const EditExceptionModal = memo(function EditExceptionModal({ {ruleName} - {(addExceptionIsLoading || isIndexPatternLoading || isSignalIndexLoading) && ( )} - {!isSignalIndexLoading && !addExceptionIsLoading && !isIndexPatternLoading && ( <> @@ -280,7 +313,18 @@ export const EditExceptionModal = memo(function EditExceptionModal({ )} - + {updateError != null && ( + + + + )} {hasVersionConflict && ( @@ -288,20 +332,21 @@ export const EditExceptionModal = memo(function EditExceptionModal({ )} + {updateError == null && ( + + {i18n.CANCEL} - - {i18n.CANCEL} - - - {i18n.EDIT_EXCEPTION_SAVE_BUTTON} - - + + {i18n.EDIT_EXCEPTION_SAVE_BUTTON} + + + )} ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx new file mode 100644 index 00000000000000..9c86c502a76483 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.test.tsx @@ -0,0 +1,160 @@ +/* + * 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 { mountWithIntl } from 'test_utils/enzyme_helpers'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { getListMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; +import { useDissasociateExceptionList } from '../../../detections/containers/detection_engine/rules/use_dissasociate_exception_list'; +import { ErrorCallout } from './error_callout'; +import { savedRuleMock } from '../../../detections/containers/detection_engine/rules/mock'; + +jest.mock('../../../detections/containers/detection_engine/rules/use_dissasociate_exception_list'); + +const mockKibanaHttpService = coreMock.createStart().http; + +describe('ErrorCallout', () => { + const mockDissasociate = jest.fn(); + + beforeEach(() => { + (useDissasociateExceptionList as jest.Mock).mockReturnValue([false, mockDissasociate]); + }); + + it('it renders error details', () => { + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="errorCalloutContainer"] .euiCallOutHeader__title').text() + ).toEqual('Error: error reason (500)'); + expect(wrapper.find('[data-test-subj="errorCalloutMessage"]').at(0).text()).toEqual( + 'Error fetching exception list' + ); + }); + + it('it invokes "onCancel" when cancel button clicked', () => { + const mockOnCancel = jest.fn(); + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="errorCalloutCancelButton"]').at(0).simulate('click'); + + expect(mockOnCancel).toHaveBeenCalled(); + }); + + it('it does not render status code if not available', () => { + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="errorCalloutContainer"] .euiCallOutHeader__title').text() + ).toEqual('Error: not found'); + expect(wrapper.find('[data-test-subj="errorCalloutMessage"]').at(0).text()).toEqual( + 'Error fetching exception list' + ); + expect(wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').exists()).toBeFalsy(); + }); + + it('it renders specific missing exceptions list error', () => { + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="errorCalloutContainer"] .euiCallOutHeader__title').text() + ).toEqual('Error: not found (404)'); + expect(wrapper.find('[data-test-subj="errorCalloutMessage"]').at(0).text()).toEqual( + 'The associated exception list (some_uuid) no longer exists. Please remove the missing exception list to add additional exceptions to the detection rule.' + ); + expect(wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').exists()).toBeTruthy(); + }); + + it('it dissasociates list from rule when remove exception list clicked ', () => { + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').at(0).simulate('click'); + + expect(mockDissasociate).toHaveBeenCalledWith([]); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx new file mode 100644 index 00000000000000..a2419ef16df3ab --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.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 React, { useMemo, useEffect, useState, useCallback } from 'react'; +import { + EuiButtonEmpty, + EuiAccordion, + EuiCodeBlock, + EuiButton, + EuiCallOut, + EuiText, + EuiSpacer, +} from '@elastic/eui'; + +import { HttpSetup } from '../../../../../../../src/core/public'; +import { List } from '../../../../common/detection_engine/schemas/types/lists'; +import { Rule } from '../../../detections/containers/detection_engine/rules/types'; +import * as i18n from './translations'; +import { useDissasociateExceptionList } from '../../../detections/containers/detection_engine/rules/use_dissasociate_exception_list'; + +export interface ErrorInfo { + reason: string | null; + code: number | null; + details: string | null; + listListId: string | null; +} + +export interface ErrorCalloutProps { + http: HttpSetup; + rule: Rule | null; + errorInfo: ErrorInfo; + onCancel: () => void; + onSuccess: (listId: string) => void; + onError: (arg: Error) => void; +} + +const ErrorCalloutComponent = ({ + http, + rule, + errorInfo, + onCancel, + onError, + onSuccess, +}: ErrorCalloutProps): JSX.Element => { + const [listToDelete, setListToDelete] = useState(null); + const [errorTitle, setErrorTitle] = useState(''); + const [errorMessage, setErrorMessage] = useState(i18n.ADD_EXCEPTION_FETCH_ERROR); + + const handleOnSuccess = useCallback((): void => { + onSuccess(listToDelete != null ? listToDelete.id : ''); + }, [onSuccess, listToDelete]); + + const [isDissasociatingList, handleDissasociateExceptionList] = useDissasociateExceptionList({ + http, + ruleRuleId: rule != null ? rule.rule_id : '', + onSuccess: handleOnSuccess, + onError, + }); + + const canDisplay404Actions = useMemo( + (): boolean => + errorInfo.code === 404 && + rule != null && + listToDelete != null && + handleDissasociateExceptionList != null, + [errorInfo.code, listToDelete, handleDissasociateExceptionList, rule] + ); + + useEffect((): void => { + // Yes, it's redundant, unfortunately typescript wasn't picking up + // that `listToDelete` is checked in canDisplay404Actions + if (canDisplay404Actions && listToDelete != null) { + setErrorMessage(i18n.ADD_EXCEPTION_FETCH_404_ERROR(listToDelete.id)); + } + + setErrorTitle(`${errorInfo.reason}${errorInfo.code != null ? ` (${errorInfo.code})` : ''}`); + }, [errorInfo.reason, errorInfo.code, listToDelete, canDisplay404Actions]); + + const handleDissasociateList = useCallback((): void => { + // Yes, it's redundant, unfortunately typescript wasn't picking up + // that `handleDissasociateExceptionList` and `list` are checked in + // canDisplay404Actions + if ( + canDisplay404Actions && + rule != null && + listToDelete != null && + handleDissasociateExceptionList != null + ) { + const exceptionLists = (rule.exceptions_list ?? []).filter( + ({ id }) => id !== listToDelete.id + ); + + handleDissasociateExceptionList(exceptionLists); + } + }, [handleDissasociateExceptionList, listToDelete, canDisplay404Actions, rule]); + + useEffect((): void => { + if (errorInfo.code === 404 && rule != null && rule.exceptions_list != null) { + const [listFound] = rule.exceptions_list.filter( + ({ id, list_id: listId }) => + (errorInfo.details != null && errorInfo.details.includes(id)) || + errorInfo.listListId === listId + ); + setListToDelete(listFound); + } + }, [rule, errorInfo.details, errorInfo.code, errorInfo.listListId]); + + return ( + + +

{errorMessage}

+
+ + {listToDelete != null && ( + +

{i18n.MODAL_ERROR_ACCORDION_TEXT}

+ + } + > + + {JSON.stringify(listToDelete)} + +
+ )} + + + {i18n.CANCEL} + + {canDisplay404Actions && ( + + {i18n.CLEAR_EXCEPTIONS_LABEL} + + )} +
+ ); +}; + +ErrorCalloutComponent.displayName = 'ErrorCalloutComponent'; + +export const ErrorCallout = React.memo(ErrorCalloutComponent); + +ErrorCallout.displayName = 'ErrorCallout'; 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 index 13e9d0df549f8a..484a3d593026e1 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts @@ -190,3 +190,52 @@ export const TOTAL_ITEMS_FETCH_ERROR = i18n.translate( defaultMessage: 'Error getting exception item totals', } ); + +export const CLEAR_EXCEPTIONS_LABEL = i18n.translate( + 'xpack.securitySolution.exceptions.clearExceptionsLabel', + { + defaultMessage: 'Remove Exception List', + } +); + +export const ADD_EXCEPTION_FETCH_404_ERROR = (listId: string) => + i18n.translate('xpack.securitySolution.exceptions.fetch404Error', { + values: { listId }, + defaultMessage: + 'The associated exception list ({listId}) no longer exists. Please remove the missing exception list to add additional exceptions to the detection rule.', + }); + +export const ADD_EXCEPTION_FETCH_ERROR = i18n.translate( + 'xpack.securitySolution.exceptions.fetchError', + { + defaultMessage: 'Error fetching exception list', + } +); + +export const ERROR = i18n.translate('xpack.securitySolution.exceptions.errorLabel', { + defaultMessage: 'Error', +}); + +export const CANCEL = i18n.translate('xpack.securitySolution.exceptions.cancelLabel', { + defaultMessage: 'Cancel', +}); + +export const MODAL_ERROR_ACCORDION_TEXT = i18n.translate( + 'xpack.securitySolution.exceptions.modalErrorAccordionText', + { + defaultMessage: 'Show rule reference information:', + } +); + +export const DISSASOCIATE_LIST_SUCCESS = (id: string) => + i18n.translate('xpack.securitySolution.exceptions.dissasociateListSuccessText', { + values: { id }, + defaultMessage: 'Exception list ({id}) has successfully been removed', + }); + +export const DISSASOCIATE_EXCEPTION_LIST_ERROR = i18n.translate( + 'xpack.securitySolution.exceptions.dissasociateExceptionListError', + { + defaultMessage: 'Failed to remove exception list', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx index 6611ee2385d108..46923e07d225ad 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx @@ -148,6 +148,50 @@ describe('useAddOrUpdateException', () => { }); }); + it('invokes "onError" if call to add exception item fails', async () => { + const mockError = new Error('error adding item'); + + addExceptionListItem = jest + .spyOn(listsApi, 'addExceptionListItem') + .mockRejectedValue(mockError); + + await act(async () => { + const { rerender, result, waitForNextUpdate } = render(); + const addOrUpdateItems = await waitForAddOrUpdateFunc({ + rerender, + result, + waitForNextUpdate, + }); + if (addOrUpdateItems) { + addOrUpdateItems(...addOrUpdateItemsArgs); + } + await waitForNextUpdate(); + expect(onError).toHaveBeenCalledWith(mockError, null, null); + }); + }); + + it('invokes "onError" if call to update exception item fails', async () => { + const mockError = new Error('error updating item'); + + updateExceptionListItem = jest + .spyOn(listsApi, 'updateExceptionListItem') + .mockRejectedValue(mockError); + + await act(async () => { + const { rerender, result, waitForNextUpdate } = render(); + const addOrUpdateItems = await waitForAddOrUpdateFunc({ + rerender, + result, + waitForNextUpdate, + }); + if (addOrUpdateItems) { + addOrUpdateItems(...addOrUpdateItemsArgs); + } + await waitForNextUpdate(); + expect(onError).toHaveBeenCalledWith(mockError, null, null); + }); + }); + describe('when alertIdToClose is not passed in', () => { it('should not update the alert status', async () => { await act(async () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx index 9d45a411b51302..be289b0e85e66b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx @@ -42,7 +42,7 @@ export type ReturnUseAddOrUpdateException = [ export interface UseAddOrUpdateExceptionProps { http: HttpStart; - onError: (arg: Error) => void; + onError: (arg: Error, code: number | null, message: string | null) => void; onSuccess: () => void; } @@ -157,7 +157,11 @@ export const useAddOrUpdateException = ({ } catch (error) { if (isSubscribed) { setIsLoading(false); - onError(error); + if (error.body != null) { + onError(error, error.body.status_code, error.body.message); + } else { + onError(error, null, null); + } } } }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx index 39d88bd8e4724d..f20a58b9ffa36a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx @@ -379,7 +379,7 @@ describe('useFetchOrCreateRuleExceptionList', () => { await waitForNextUpdate(); await waitForNextUpdate(); expect(onError).toHaveBeenCalledTimes(1); - expect(onError).toHaveBeenCalledWith(error); + expect(onError).toHaveBeenCalledWith(error, null, null); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx index 0d367e03a799f3..944631d4e9fb5f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx @@ -30,7 +30,7 @@ export interface UseFetchOrCreateRuleExceptionListProps { http: HttpStart; ruleId: Rule['id']; exceptionListType: ExceptionListSchema['type']; - onError: (arg: Error) => void; + onError: (arg: Error, code: number | null, message: string | null) => void; onSuccess?: (ruleWasChanged: boolean) => void; } @@ -179,7 +179,11 @@ export const useFetchOrCreateRuleExceptionList = ({ if (isSubscribed) { setIsLoading(false); setExceptionList(null); - onError(error); + if (error.body != null) { + onError(error, error.body.status_code, error.body.message); + } else { + onError(error, null, null); + } } } } 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 index 7482068454a97c..c97895cdfe2363 100644 --- 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 @@ -322,11 +322,13 @@ const ExceptionsViewerComponent = ({ exceptionListTypeToEdit != null && ( )} diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx new file mode 100644 index 00000000000000..6721d89f2799b7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx @@ -0,0 +1,52 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; + +import { coreMock } from '../../../../../../../../src/core/public/mocks'; + +import * as api from './api'; +import { ruleMock } from './mock'; +import { + ReturnUseDissasociateExceptionList, + UseDissasociateExceptionListProps, + useDissasociateExceptionList, +} from './use_dissasociate_exception_list'; + +const mockKibanaHttpService = coreMock.createStart().http; + +describe('useDissasociateExceptionList', () => { + const onError = jest.fn(); + const onSuccess = jest.fn(); + + beforeEach(() => { + jest.spyOn(api, 'patchRule').mockResolvedValue(ruleMock); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('initializes hook', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseDissasociateExceptionListProps, + ReturnUseDissasociateExceptionList + >(() => + useDissasociateExceptionList({ + http: mockKibanaHttpService, + ruleRuleId: 'rule_id', + onError, + onSuccess, + }) + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual([false, null]); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx new file mode 100644 index 00000000000000..dffba3e6e04368 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx @@ -0,0 +1,80 @@ +/* + * 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, useState, useRef } from 'react'; + +import { HttpStart } from '../../../../../../../../src/core/public'; +import { List } from '../../../../../common/detection_engine/schemas/types/lists'; +import { patchRule } from './api'; + +type Func = (lists: List[]) => void; +export type ReturnUseDissasociateExceptionList = [boolean, Func | null]; + +export interface UseDissasociateExceptionListProps { + http: HttpStart; + ruleRuleId: string; + onError: (arg: Error) => void; + onSuccess: () => void; +} + +/** + * Hook for removing an exception list reference from a rule + * + * @param http Kibana http service + * @param ruleRuleId a rule_id (NOT id) + * @param onError error callback + * @param onSuccess success callback + * + */ +export const useDissasociateExceptionList = ({ + http, + ruleRuleId, + onError, + onSuccess, +}: UseDissasociateExceptionListProps): ReturnUseDissasociateExceptionList => { + const [isLoading, setLoading] = useState(false); + const dissasociateList = useRef(null); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + + const dissasociateListFromRule = (id: string) => async ( + exceptionLists: List[] + ): Promise => { + try { + if (isSubscribed) { + setLoading(true); + + await patchRule({ + ruleProperties: { + rule_id: id, + exceptions_list: exceptionLists, + }, + signal: abortCtrl.signal, + }); + + onSuccess(); + setLoading(false); + } + } catch (err) { + if (isSubscribed) { + setLoading(false); + onError(err); + } + } + }; + + dissasociateList.current = dissasociateListFromRule(ruleRuleId); + + return (): void => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [http, ruleRuleId, onError, onSuccess]); + + return [isLoading, dissasociateList.current]; +}; From 4289f9d8b110fb03bc9b16eabdef8d37b073d409 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 26 Aug 2020 14:23:27 -0700 Subject: [PATCH 054/216] skip all tests that rely on es authentication type --- .../test/login_selector_api_integration/apis/login_selector.ts | 3 ++- .../apis/authorization_code_flow/oidc_auth.ts | 3 ++- .../test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts | 3 ++- x-pack/test/pki_api_integration/apis/security/pki_auth.ts | 3 ++- x-pack/test/saml_api_integration/apis/security/saml_login.ts | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/x-pack/test/login_selector_api_integration/apis/login_selector.ts b/x-pack/test/login_selector_api_integration/apis/login_selector.ts index 439e553b17a86a..63084d3bfc9e96 100644 --- a/x-pack/test/login_selector_api_integration/apis/login_selector.ts +++ b/x-pack/test/login_selector_api_integration/apis/login_selector.ts @@ -62,7 +62,8 @@ export default function ({ getService }: FtrProviderContext) { expect(apiResponse.body.authentication_provider).to.be(providerName); } - describe('Login Selector', () => { + // FAILING: https://github.com/elastic/kibana/issues/75707 + describe.skip('Login Selector', () => { it('should redirect user to a login selector', async () => { const response = await supertest .get('/abc/xyz/handshake?one=two three') diff --git a/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.ts b/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.ts index 18dfdcffef363e..1b37d60436ddcc 100644 --- a/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.ts +++ b/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.ts @@ -15,7 +15,8 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const config = getService('config'); - describe('OpenID Connect authentication', () => { + // FAILING: https://github.com/elastic/kibana/issues/75707 + describe.skip('OpenID Connect authentication', () => { it('should reject API requests if client is not authenticated', async () => { await supertest.get('/internal/security/me').set('kbn-xsrf', 'xxx').expect(401); }); diff --git a/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts b/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts index bea2f996141d50..43d9d680e102af 100644 --- a/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts +++ b/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts @@ -15,7 +15,8 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const config = getService('config'); - describe('OpenID Connect Implicit Flow authentication', () => { + // FAILING: https://github.com/elastic/kibana/issues/75707 + describe.skip('OpenID Connect Implicit Flow authentication', () => { describe('finishing handshake', () => { let stateAndNonce: ReturnType; let handshakeCookie: Cookie; diff --git a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts index 664fdb9fba67af..a2090a8c2cc481 100644 --- a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts +++ b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts @@ -41,7 +41,8 @@ export default function ({ getService }: FtrProviderContext) { expect(cookie.maxAge).to.be(0); } - describe('PKI authentication', () => { + // FAILING: https://github.com/elastic/kibana/issues/75707 + describe.skip('PKI authentication', () => { before(async () => { await getService('esSupertest') .post('/_security/role_mapping/first_client_pki') diff --git a/x-pack/test/saml_api_integration/apis/security/saml_login.ts b/x-pack/test/saml_api_integration/apis/security/saml_login.ts index d78f4da63ab5b4..13b541f75e5bd0 100644 --- a/x-pack/test/saml_api_integration/apis/security/saml_login.ts +++ b/x-pack/test/saml_api_integration/apis/security/saml_login.ts @@ -61,7 +61,8 @@ export default function ({ getService }: FtrProviderContext) { expect(apiResponse.body.username).to.be(username); } - describe('SAML authentication', () => { + // FAILING: https://github.com/elastic/kibana/issues/75707 + describe.skip('SAML authentication', () => { it('should reject API requests if client is not authenticated', async () => { await supertest.get('/internal/security/me').set('kbn-xsrf', 'xxx').expect(401); }); From c08bf7f3ca6374a7eb4adb7b5ee760d75ceddf0b Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Wed, 26 Aug 2020 17:21:16 -0700 Subject: [PATCH 055/216] using test_user with minimum privileges for canvas functional ui tests (#75917) * incorporating test_user wth specific roles for the canvas functional ui tests * additional checks - removed comments * changes to incorporate code comments * lint check * incorporate code reviews Co-authored-by: Elastic Machine --- .../functional/apps/canvas/custom_elements.ts | 6 +----- x-pack/test/functional/apps/canvas/expression.ts | 6 +----- x-pack/test/functional/apps/canvas/index.js | 15 ++++++++++++++- x-pack/test/functional/apps/canvas/smoke_test.js | 7 +------ x-pack/test/functional/config.js | 11 +++++++++++ 5 files changed, 28 insertions(+), 17 deletions(-) diff --git a/x-pack/test/functional/apps/canvas/custom_elements.ts b/x-pack/test/functional/apps/canvas/custom_elements.ts index 33db56751285ef..1a05560aaf9313 100644 --- a/x-pack/test/functional/apps/canvas/custom_elements.ts +++ b/x-pack/test/functional/apps/canvas/custom_elements.ts @@ -12,24 +12,20 @@ export default function canvasCustomElementTest({ getService, getPageObjects, }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); const retry = getService('retry'); const PageObjects = getPageObjects(['canvas', 'common']); const find = getService('find'); + const esArchiver = getService('esArchiver'); describe('custom elements', function () { this.tags('skipFirefox'); before(async () => { - // init data - await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('canvas/default'); - // open canvas home await PageObjects.common.navigateToApp('canvas'); - // load test workpad await PageObjects.common.navigateToApp('canvas', { hash: '/workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31/page/1', diff --git a/x-pack/test/functional/apps/canvas/expression.ts b/x-pack/test/functional/apps/canvas/expression.ts index c184dca8366be9..548321243d4fb2 100644 --- a/x-pack/test/functional/apps/canvas/expression.ts +++ b/x-pack/test/functional/apps/canvas/expression.ts @@ -9,22 +9,18 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function canvasExpressionTest({ getService, getPageObjects }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); - // const browser = getService('browser'); const retry = getService('retry'); const PageObjects = getPageObjects(['canvas', 'common']); const find = getService('find'); + const esArchiver = getService('esArchiver'); describe('expression editor', function () { // there is an issue with FF not properly clicking on workpad elements this.tags('skipFirefox'); before(async () => { - // init data - await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('canvas/default'); - // load test workpad await PageObjects.common.navigateToApp('canvas', { hash: '/workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31/page/1', diff --git a/x-pack/test/functional/apps/canvas/index.js b/x-pack/test/functional/apps/canvas/index.js index d6ded9b20b1ad8..7ee48beaabb2ae 100644 --- a/x-pack/test/functional/apps/canvas/index.js +++ b/x-pack/test/functional/apps/canvas/index.js @@ -4,8 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -export default function canvasApp({ loadTestFile }) { +export default function canvasApp({ loadTestFile, getService }) { + const security = getService('security'); + const esArchiver = getService('esArchiver'); + describe('Canvas app', function canvasAppTestSuite() { + before(async () => { + // init data + await security.testUser.setRoles(['test_logstash_reader', 'global_canvas_all']); + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); + this.tags('ciGroup2'); // CI requires tags ヽ(゜Q。)ノ? loadTestFile(require.resolve('./smoke_test')); loadTestFile(require.resolve('./expression')); diff --git a/x-pack/test/functional/apps/canvas/smoke_test.js b/x-pack/test/functional/apps/canvas/smoke_test.js index 056713a5dacfab..596d34e7c1df53 100644 --- a/x-pack/test/functional/apps/canvas/smoke_test.js +++ b/x-pack/test/functional/apps/canvas/smoke_test.js @@ -8,11 +8,11 @@ import expect from '@kbn/expect'; import { parse } from 'url'; export default function canvasSmokeTest({ getService, getPageObjects }) { - const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); const retry = getService('retry'); const PageObjects = getPageObjects(['common']); + const esArchiver = getService('esArchiver'); describe('smoke test', function () { this.tags('includeFirefox'); @@ -20,12 +20,7 @@ export default function canvasSmokeTest({ getService, getPageObjects }) { const testWorkpadId = 'workpad-1705f884-6224-47de-ba49-ca224fe6ec31'; before(async () => { - // init data - await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('canvas/default'); - - // load canvas - // see also navigateToUrl(app, hash) await PageObjects.common.navigateToApp('canvas'); }); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 003d842cc3d6f2..cdc6292ba808a6 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -221,6 +221,17 @@ export default async function ({ readConfigFile }) { kibana: [], }, + global_canvas_all: { + kibana: [ + { + feature: { + canvas: ['all'], + }, + spaces: ['*'], + }, + ], + }, + global_discover_read: { kibana: [ { From 42942327e52fc9cb1671f7c938158991d62c2659 Mon Sep 17 00:00:00 2001 From: Brent Kimmel Date: Wed, 26 Aug 2020 21:06:38 -0400 Subject: [PATCH 056/216] =?UTF-8?q?[Security=20Solution][Resolver]=20Word-?= =?UTF-8?q?break=20long=20titles=20in=20related=20event=E2=80=A6=20(#75926?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Security Solution][Resolver] Word-break long titles in related event description lists * word-break long titles at non-word boundaries Co-authored-by: Elastic Machine --- .../resolver/store/data/reducer.test.ts | 19 ++ .../public/resolver/store/data/selectors.ts | 98 ++++++++++ .../public/resolver/store/selectors.ts | 9 + .../public/resolver/types.ts | 16 ++ .../view/panels/event_counts_for_process.tsx | 3 +- .../view/panels/panel_content_error.tsx | 3 +- .../view/panels/panel_content_utilities.tsx | 8 - .../resolver/view/panels/process_details.tsx | 3 +- .../view/panels/process_event_list.tsx | 9 +- .../view/panels/process_list_with_counts.tsx | 3 +- .../view/panels/related_event_detail.tsx | 174 ++++++++---------- .../view/use_resolver_query_params.ts | 2 +- 12 files changed, 225 insertions(+), 122 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts index edda2ef984a9e3..e087db9f74685a 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts @@ -11,6 +11,7 @@ import * as selectors from './selectors'; import { DataState } from '../../types'; import { DataAction } from './action'; import { ResolverChildNode, ResolverTree } from '../../../../common/endpoint/types'; +import * as eventModel from '../../../../common/endpoint/models/event'; /** * Test the data reducer and selector. @@ -175,6 +176,24 @@ describe('Resolver Data Middleware', () => { eventStatsForFirstChildNode.byCategory[categoryToOverCount] - 1 ); }); + it('should return the correct related event detail metadata for a given related event', () => { + const relatedEventsByCategory = selectors.relatedEventsByCategory(store.getState()); + const someRelatedEventForTheFirstChild = relatedEventsByCategory(firstChildNodeInTree.id)( + categoryToOverCount + )[0]; + const relatedEventID = eventModel.eventId(someRelatedEventForTheFirstChild)!; + const relatedDisplayInfo = selectors.relatedEventDisplayInfoByEntityAndSelfID( + store.getState() + )(firstChildNodeInTree.id, relatedEventID); + const [, countOfSameType, , sectionData] = relatedDisplayInfo; + const hostEntries = sectionData.filter((section) => { + return section.sectionTitle === 'host'; + })[0].entries; + expect(hostEntries).toContainEqual({ title: 'os.platform', description: 'Windows' }); + expect(countOfSameType).toBe( + eventStatsForFirstChildNode.byCategory[categoryToOverCount] - 1 + ); + }); it('should indicate the limit has been exceeded because the number of related events received for the category is less than what the stats count said it would be', () => { const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState()); const shouldShowLimit = selectedRelatedInfo(firstChildNodeInTree.id) diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 569a24bb8537e3..965547f1e309a9 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -14,6 +14,7 @@ import { IndexedProcessNode, AABB, VisibleEntites, + SectionData, } from '../../types'; import { isGraphableProcess, @@ -29,11 +30,14 @@ import { ResolverNodeStats, ResolverRelatedEvents, SafeResolverEvent, + EndpointEvent, + LegacyEndpointEvent, } from '../../../../common/endpoint/types'; import * as resolverTreeModel from '../../models/resolver_tree'; import * as isometricTaxiLayoutModel from '../../models/indexed_process_tree/isometric_taxi_layout'; import * as eventModel from '../../../../common/endpoint/models/event'; import * as vector2 from '../../models/vector2'; +import { formatDate } from '../../view/panels/panel_content_utilities'; /** * If there is currently a request. @@ -173,6 +177,100 @@ export function relatedEventsByEntityId(data: DataState): Map
` entries + */ +const objectToDescriptionListEntries = function* ( + obj: object, + prefix = '' +): Generator<{ title: string; description: string }> { + const nextPrefix = prefix.length ? `${prefix}.` : ''; + for (const [metaKey, metaValue] of Object.entries(obj)) { + if (typeof metaValue === 'number' || typeof metaValue === 'string') { + yield { title: nextPrefix + metaKey, description: `${metaValue}` }; + } else if (metaValue instanceof Array) { + yield { + title: nextPrefix + metaKey, + description: metaValue + .filter((arrayEntry) => { + return typeof arrayEntry === 'number' || typeof arrayEntry === 'string'; + }) + .join(','), + }; + } else if (typeof metaValue === 'object') { + yield* objectToDescriptionListEntries(metaValue, nextPrefix + metaKey); + } + } +}; + +/** + * Returns a function that returns the information needed to display related event details based on + * the related event's entityID and its own ID. + */ +export const relatedEventDisplayInfoByEntityAndSelfID: ( + state: DataState +) => ( + entityId: string, + relatedEventId: string | number +) => [ + EndpointEvent | LegacyEndpointEvent | undefined, + number, + string | undefined, + SectionData, + string +] = createSelector(relatedEventsByEntityId, function relatedEventDetails( + /* eslint-disable no-shadow */ + relatedEventsByEntityId + /* eslint-enable no-shadow */ +) { + return defaultMemoize((entityId: string, relatedEventId: string | number) => { + const relatedEventsForThisProcess = relatedEventsByEntityId.get(entityId); + if (!relatedEventsForThisProcess) { + return [undefined, 0, undefined, [], '']; + } + const specificEvent = relatedEventsForThisProcess.events.find( + (evt) => eventModel.eventId(evt) === relatedEventId + ); + // For breadcrumbs: + const specificCategory = specificEvent && eventModel.primaryEventCategory(specificEvent); + const countOfCategory = relatedEventsForThisProcess.events.reduce((sumtotal, evt) => { + return eventModel.primaryEventCategory(evt) === specificCategory ? sumtotal + 1 : sumtotal; + }, 0); + + // Assuming these details (agent, ecs, process) aren't as helpful, can revisit + const { agent, ecs, process, ...relevantData } = specificEvent as ResolverEvent & { + // Type this with various unknown keys so that ts will let us delete those keys + ecs: unknown; + process: unknown; + }; + + let displayDate = ''; + const sectionData: SectionData = Object.entries(relevantData) + .map(([sectionTitle, val]) => { + if (sectionTitle === '@timestamp') { + displayDate = formatDate(val); + return { sectionTitle: '', entries: [] }; + } + if (typeof val !== 'object') { + return { sectionTitle, entries: [{ title: sectionTitle, description: `${val}` }] }; + } + return { sectionTitle, entries: [...objectToDescriptionListEntries(val)] }; + }) + .filter((v) => v.sectionTitle !== '' && v.entries.length); + + return [specificEvent, countOfCategory, specificCategory, sectionData, displayDate]; + }); +}); + /** * Returns a function that returns a function (when supplied with an entity id for a node) * that returns related events for a node that match an event.category (when supplied with the category) diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 70a461909a99bd..f50aeed3f4d48d 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -122,6 +122,15 @@ export const relatedEventsByEntityId = composeSelectors( dataSelectors.relatedEventsByEntityId ); +/** + * Returns a function that returns the information needed to display related event details based on + * the related event's entityID and its own ID. + */ +export const relatedEventDisplayInfoByEntityAndSelfId = composeSelectors( + dataStateSelector, + dataSelectors.relatedEventDisplayInfoByEntityAndSelfID +); + /** * Returns a function that returns a function (when supplied with an entity id for a node) * that returns related events for a node that match an event.category (when supplied with the category) diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 33f7a1d97db139..9ebe3fa14e8425 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -160,6 +160,22 @@ export interface IndexedProcessNode extends BBox { position: Vector2; } +/** + * A type describing the shape of section titles and entries for description lists + */ +export type SectionData = Array<{ + sectionTitle: string; + entries: Array<{ title: string; description: string }>; +}>; + +/** + * The two query parameters we read/write on to control which view the table presents: + */ +export interface CrumbInfo { + crumbId: string; + crumbEvent: string; +} + /** * A type containing all things to actually be rendered to the DOM. */ diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/event_counts_for_process.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/event_counts_for_process.tsx index 129aff776808ad..c528ba547e6ae3 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/event_counts_for_process.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/event_counts_for_process.tsx @@ -8,10 +8,11 @@ import React, { memo, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiBasicTableColumn, EuiButtonEmpty, EuiSpacer, EuiInMemoryTable } from '@elastic/eui'; import { FormattedMessage } from 'react-intl'; -import { CrumbInfo, StyledBreadcrumbs } from './panel_content_utilities'; +import { StyledBreadcrumbs } from './panel_content_utilities'; import * as event from '../../../../common/endpoint/models/event'; import { ResolverEvent, ResolverNodeStats } from '../../../../common/endpoint/types'; +import { CrumbInfo } from '../../types'; /** * This view gives counts for all the related events of a process grouped by related event type. diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_error.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_error.tsx index c9a536fd5932de..b93ef6146f1cf5 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_error.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_error.tsx @@ -7,7 +7,8 @@ import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiText, EuiButtonEmpty } from '@elastic/eui'; import React, { memo, useMemo } from 'react'; -import { CrumbInfo, StyledBreadcrumbs } from './panel_content_utilities'; +import { StyledBreadcrumbs } from './panel_content_utilities'; +import { CrumbInfo } from '../../types'; /** * Display an error in the panel when something goes wrong and give the user a way to "retreat" back to a default state. diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx index 55b5be21fb4a45..5c7a4a476efbab 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx @@ -23,14 +23,6 @@ const BetaHeader = styled(`header`)` margin-bottom: 1em; `; -/** - * The two query parameters we read/write on to control which view the table presents: - */ -export interface CrumbInfo { - crumbId: string; - crumbEvent: string; -} - const ThemedBreadcrumbs = styled(EuiBreadcrumbs)<{ background: string; text: string }>` &.euiBreadcrumbs { background-color: ${(props) => props.background}; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx index adfcc4cc44d1f7..15711909c4c9be 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx @@ -19,7 +19,7 @@ import { FormattedMessage } from 'react-intl'; import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list'; import * as selectors from '../../store/selectors'; import * as event from '../../../../common/endpoint/models/event'; -import { CrumbInfo, formatDate, StyledBreadcrumbs } from './panel_content_utilities'; +import { formatDate, StyledBreadcrumbs } from './panel_content_utilities'; import { processPath, processPid, @@ -31,6 +31,7 @@ import { import { CubeForProcess } from './cube_for_process'; import { ResolverEvent } from '../../../../common/endpoint/types'; import { useResolverTheme } from '../assets'; +import { CrumbInfo } from '../../types'; const StyledDescriptionList = styled(EuiDescriptionList)` &.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title { diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/process_event_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/process_event_list.tsx index 101711475c9381..a710d3ad846b35 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/process_event_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/process_event_list.tsx @@ -10,18 +10,13 @@ import { EuiTitle, EuiSpacer, EuiText, EuiButtonEmpty, EuiHorizontalRule } from import { useSelector } from 'react-redux'; import { FormattedMessage } from 'react-intl'; import styled from 'styled-components'; -import { - CrumbInfo, - formatDate, - StyledBreadcrumbs, - BoldCode, - StyledTime, -} from './panel_content_utilities'; +import { formatDate, StyledBreadcrumbs, BoldCode, StyledTime } from './panel_content_utilities'; import * as event from '../../../../common/endpoint/models/event'; import { ResolverEvent, ResolverNodeStats } from '../../../../common/endpoint/types'; import * as selectors from '../../store/selectors'; import { useResolverDispatch } from '../use_resolver_dispatch'; import { RelatedEventLimitWarning } from '../limit_warnings'; +import { CrumbInfo } from '../../types'; /** * This view presents a list of related events of a given type for a given process. diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/process_list_with_counts.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/process_list_with_counts.tsx index 1be4b4b0552437..e42140feb928bd 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/process_list_with_counts.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/process_list_with_counts.tsx @@ -16,12 +16,13 @@ import { useSelector } from 'react-redux'; import styled from 'styled-components'; import * as event from '../../../../common/endpoint/models/event'; import * as selectors from '../../store/selectors'; -import { CrumbInfo, formatter, StyledBreadcrumbs } from './panel_content_utilities'; +import { formatter, StyledBreadcrumbs } from './panel_content_utilities'; import { useResolverDispatch } from '../use_resolver_dispatch'; import { SideEffectContext } from '../side_effect_context'; import { CubeForProcess } from './cube_for_process'; import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { LimitWarning } from '../limit_warnings'; +import { CrumbInfo } from '../../types'; const StyledLimitWarning = styled(LimitWarning)` flex-flow: row wrap; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx index 3579b1b2f69b88..da4cd3c9dacade 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx @@ -10,58 +10,19 @@ import { EuiSpacer, EuiText, EuiDescriptionList, EuiTextColor, EuiTitle } from ' import styled from 'styled-components'; import { useSelector } from 'react-redux'; import { FormattedMessage } from 'react-intl'; -import { - CrumbInfo, - formatDate, - StyledBreadcrumbs, - BoldCode, - StyledTime, -} from './panel_content_utilities'; +import { StyledBreadcrumbs, BoldCode, StyledTime } from './panel_content_utilities'; import * as event from '../../../../common/endpoint/models/event'; import { ResolverEvent } from '../../../../common/endpoint/types'; import * as selectors from '../../store/selectors'; import { useResolverDispatch } from '../use_resolver_dispatch'; import { PanelContentError } from './panel_content_error'; - -/** - * A helper function to turn objects into EuiDescriptionList entries. - * This reflects the strategy of more or less "dumping" metadata for related processes - * in description lists with little/no 'prettification'. This has the obvious drawback of - * data perhaps appearing inscrutable/daunting, but the benefit of presenting these fields - * to the user "as they occur" in ECS, which may help them with e.g. EQL queries. - * - * Given an object like: {a:{b: 1}, c: 'd'} it will yield title/description entries like so: - * {title: "a.b", description: "1"}, {title: "c", description: "d"} - * - * @param {object} obj The object to turn into `
` entries - */ -const objectToDescriptionListEntries = function* ( - obj: object, - prefix = '' -): Generator<{ title: string; description: string }> { - const nextPrefix = prefix.length ? `${prefix}.` : ''; - for (const [metaKey, metaValue] of Object.entries(obj)) { - if (typeof metaValue === 'number' || typeof metaValue === 'string') { - yield { title: nextPrefix + metaKey, description: `${metaValue}` }; - } else if (metaValue instanceof Array) { - yield { - title: nextPrefix + metaKey, - description: metaValue - .filter((arrayEntry) => { - return typeof arrayEntry === 'number' || typeof arrayEntry === 'string'; - }) - .join(','), - }; - } else if (typeof metaValue === 'object') { - yield* objectToDescriptionListEntries(metaValue, nextPrefix + metaKey); - } - } -}; +import { CrumbInfo } from '../../types'; // Adding some styles to prevent horizontal scrollbars, per request from UX review const StyledDescriptionList = memo(styled(EuiDescriptionList)` &.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title { max-width: 8em; + overflow-wrap: break-word; } &.euiDescriptionList.euiDescriptionList--column dd.euiDescriptionList__description { max-width: calc(100% - 8.5em); @@ -69,6 +30,12 @@ const StyledDescriptionList = memo(styled(EuiDescriptionList)` } `); +// Also prevents horizontal scrollbars on long descriptive names +const StyledDescriptiveName = memo(styled(EuiText)` + padding-right: 1em; + overflow-wrap: break-word; +`); + // Styling subtitles, per UX review: const StyledFlexTitle = memo(styled('h3')` display: flex; @@ -90,6 +57,49 @@ const TitleHr = memo(() => { }); TitleHr.displayName = 'TitleHR'; +const GeneratedText = React.memo(function ({ children }) { + return <>{processedValue()}; + + function processedValue() { + return React.Children.map(children, (child) => { + if (typeof child === 'string') { + const valueSplitByWordBoundaries = child.split(/\b/); + + if (valueSplitByWordBoundaries.length < 2) { + return valueSplitByWordBoundaries[0]; + } + + return [ + valueSplitByWordBoundaries[0], + ...valueSplitByWordBoundaries + .splice(1) + .reduce(function (generatedTextMemo: Array, value, index) { + return [...generatedTextMemo, value, ]; + }, []), + ]; + } else { + return child; + } + }); + } +}); +GeneratedText.displayName = 'GeneratedText'; + +/** + * Take description list entries and prepare them for display by + * seeding with `` tags. + * + * @param entries {title: string, description: string}[] + */ +function entriesForDisplay(entries: Array<{ title: string; description: string }>) { + return entries.map((entry) => { + return { + description: {entry.description}, + title: {entry.title}, + }; + }); +} + /** * This view presents a detailed view of all the available data for a related event, split and titled by the "section" * it appears in the underlying ResolverEvent @@ -138,60 +148,17 @@ export const RelatedEventDetail = memo(function RelatedEventDetail({ } }, [relatedsReady, dispatch, processEntityId]); - const relatedEventsForThisProcess = useSelector(selectors.relatedEventsByEntityId).get( - processEntityId! + const [ + relatedEventToShowDetailsFor, + countBySameCategory, + relatedEventCategory = naString, + sections, + formattedDate, + ] = useSelector(selectors.relatedEventDisplayInfoByEntityAndSelfId)( + processEntityId, + relatedEventId ); - const [relatedEventToShowDetailsFor, countBySameCategory, relatedEventCategory] = useMemo(() => { - if (!relatedEventsForThisProcess) { - return [undefined, 0]; - } - const specificEvent = relatedEventsForThisProcess.events.find( - (evt) => event.eventId(evt) === relatedEventId - ); - // For breadcrumbs: - const specificCategory = specificEvent && event.primaryEventCategory(specificEvent); - const countOfCategory = relatedEventsForThisProcess.events.reduce((sumtotal, evt) => { - return event.primaryEventCategory(evt) === specificCategory ? sumtotal + 1 : sumtotal; - }, 0); - return [specificEvent, countOfCategory, specificCategory || naString]; - }, [relatedEventsForThisProcess, naString, relatedEventId]); - - const [sections, formattedDate] = useMemo(() => { - if (!relatedEventToShowDetailsFor) { - // This could happen if user relaods from URL param and requests an eventId that no longer exists - return [[], naString]; - } - // Assuming these details (agent, ecs, process) aren't as helpful, can revisit - const { - agent, - ecs, - process, - ...relevantData - } = relatedEventToShowDetailsFor as ResolverEvent & { - // Type this with various unknown keys so that ts will let us delete those keys - ecs: unknown; - process: unknown; - }; - let displayDate = ''; - const sectionData: Array<{ - sectionTitle: string; - entries: Array<{ title: string; description: string }>; - }> = Object.entries(relevantData) - .map(([sectionTitle, val]) => { - if (sectionTitle === '@timestamp') { - displayDate = formatDate(val); - return { sectionTitle: '', entries: [] }; - } - if (typeof val !== 'object') { - return { sectionTitle, entries: [{ title: sectionTitle, description: `${val}` }] }; - } - return { sectionTitle, entries: [...objectToDescriptionListEntries(val)] }; - }) - .filter((v) => v.sectionTitle !== '' && v.entries.length); - return [sectionData, displayDate]; - }, [relatedEventToShowDetailsFor, naString]); - const waitCrumbs = useMemo(() => { return [ { @@ -338,15 +305,18 @@ export const RelatedEventDetail = memo(function RelatedEventDetail({ - - - + + + + + {sections.map(({ sectionTitle, entries }, index) => { + const displayEntries = entriesForDisplay(entries); return ( {index === 0 ? null : } @@ -364,7 +334,7 @@ export const RelatedEventDetail = memo(function RelatedEventDetail({ align="left" titleProps={{ className: 'desc-title' }} compressed - listItems={entries} + listItems={displayEntries} /> {index === sections.length - 1 ? null : } diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts b/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts index aa0851916a7b4e..b6c229181e9f7a 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts @@ -7,7 +7,7 @@ import { useCallback, useMemo } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { useQueryStringKeys } from './use_query_string_keys'; -import { CrumbInfo } from './panels/panel_content_utilities'; +import { CrumbInfo } from '../types'; export function useResolverQueryParams() { /** From a358c5768ea4f378270140d4e480fece98e8e103 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 27 Aug 2020 07:02:28 +0200 Subject: [PATCH 057/216] [Uptime] One click simple monitor down alert (#73835) * WIP * added anomaly alert * update types * update types * update * types * types * update ML part * update ML part * update ML part * unnecessary change * icon for disable * update test * update api * update labels * resolve conflicts * fix types * fix editing alert * fix types * added actions column * added code to add alert * update anomaly message * added anomaly alert test * update * update type * fix ml legacy scoped client * update * WIP * fix conflict * added aria label * Added deleteion loading * fix type * update * update tests * update * update type * fix types * WIP * added enabled alerts section * add data * update * update tests * fix test * update i18n * update i18n * update i18n * fix * update message * update * update * update * revert * update types * added component * update test * incorporate PR feedback * fix focus * update drawer * handle edge case * improve btn text * improve btn text * use switch instead of icons * update snapshot * use compressed form * fix type * update snapshot * update snapshot * update test * update test * PR feedback * fix test and type * remove delete action * remove unnecessary function Co-authored-by: Elastic Machine --- .../triggers_actions_ui/public/index.ts | 1 + .../uptime/common/constants/rest_api.ts | 3 + .../common/constants/settings_defaults.ts | 1 + .../runtime_types/alerts/status_check.ts | 2 + .../common/runtime_types/dynamic_settings.ts | 1 + .../common/runtime_types/monitor/details.ts | 3 +- .../__tests__/link_for_eui.test.tsx | 4 +- .../components/monitor/ml/manage_ml_job.tsx | 4 +- .../monitor/ml/use_anomaly_alert.ts | 8 +- .../__mocks__/{mock.ts => poly_layer_mock.ts} | 0 .../embeddables/__tests__/map_config.test.ts | 2 +- .../__snapshots__/monitor_list.test.tsx.snap | 111 ++++++++++- .../__snapshots__/enable_alert.test.tsx.snap | 89 +++++++++ .../columns/__tests__/enable_alert.test.tsx | 90 +++++++++ .../columns/define_connectors.tsx | 55 ++++++ .../monitor_list/columns/enable_alert.tsx | 130 +++++++++++++ .../monitor_list/columns/translations.ts | 15 ++ .../overview/monitor_list/monitor_list.tsx | 24 ++- .../monitor_list_drawer.test.tsx.snap | 2 + .../most_recent_error.test.tsx.snap | 2 +- .../__tests__/monitor_list_drawer.test.tsx | 24 ++- .../monitor_list_drawer/enabled_alerts.tsx | 57 ++++++ .../list_drawer_container.tsx | 58 +++--- .../monitor_list_drawer.tsx | 13 +- .../overview/monitor_list/translations.ts | 4 + .../certificate_form.test.tsx.snap | 1 + .../__snapshots__/indices_form.test.tsx.snap | 1 + .../__tests__/certificate_form.test.tsx | 3 + .../settings/__tests__/indices_form.test.tsx | 1 + .../settings/add_connector_flyout.tsx | 70 +++++++ .../settings/alert_defaults_form.tsx | 182 ++++++++++++++++++ .../components/settings/translations.ts | 9 + .../use_url_params.test.tsx.snap | 4 +- .../uptime/public/hooks/use_init_app.ts | 18 ++ .../uptime/public/lib/__mocks__/index.ts | 7 - .../public/lib/__mocks__/uptime_store.mock.ts | 120 ++++++++++++ ...tory.mock.ts => ut_router_history.mock.ts} | 0 .../__tests__/monitor_status.test.ts | 10 +- .../public/lib/alert_types/alert_messages.tsx | 28 +++ .../public/lib/helper/helper_with_redux.tsx | 5 +- .../public/lib/helper/helper_with_router.tsx | 37 +++- .../get_supported_url_params.test.ts.snap | 5 + .../get_supported_url_params.test.ts | 2 + .../url_params/get_supported_url_params.ts | 3 + x-pack/plugins/uptime/public/lib/index.ts | 2 +- .../__snapshots__/page_header.test.tsx.snap | 46 ++--- .../plugins/uptime/public/pages/monitor.tsx | 23 ++- .../plugins/uptime/public/pages/overview.tsx | 12 ++ .../uptime/public/pages/page_header.tsx | 10 +- .../plugins/uptime/public/pages/settings.tsx | 17 +- .../uptime/public/state/actions/monitor.ts | 12 +- .../uptime/public/state/actions/types.ts | 8 + .../uptime/public/state/alerts/alerts.ts | 165 ++++++++++++++++ .../plugins/uptime/public/state/api/alerts.ts | 77 +++++++- .../public/state/certificates/certificates.ts | 8 +- .../uptime/public/state/effects/alerts.ts | 4 +- .../uptime/public/state/effects/index.ts | 2 +- .../uptime/public/state/effects/ml_anomaly.ts | 5 +- .../uptime/public/state/effects/monitor.ts | 8 +- .../uptime/public/state/reducers/alerts.ts | 29 --- .../uptime/public/state/reducers/index.ts | 2 +- .../public/state/reducers/index_status.ts | 8 +- .../public/state/reducers/ml_anomaly.ts | 24 +-- .../uptime/public/state/reducers/monitor.ts | 9 +- .../uptime/public/state/reducers/types.ts | 2 +- .../uptime/public/state/reducers/utils.ts | 22 ++- .../state/selectors/__tests__/index.test.ts | 6 +- .../uptime/public/state/selectors/index.ts | 4 +- x-pack/plugins/uptime/server/kibana.index.ts | 8 +- .../lib/adapters/framework/adapter_types.ts | 10 +- .../telemetry/kibana_telemetry_adapter.ts | 6 +- .../lib/alerts/__tests__/status_check.test.ts | 8 + .../uptime/server/lib/alerts/status_check.ts | 74 ++++--- .../lib/requests/__tests__/get_certs.test.ts | 1 + .../__tests__/get_latest_monitor.test.ts | 2 +- .../server/lib/requests/get_latest_monitor.ts | 10 +- .../lib/requests/get_monitor_details.ts | 75 +++++++- .../server/lib/requests/get_monitor_status.ts | 2 +- .../uptime/server/lib/saved_objects.ts | 3 + .../__tests__/dynamic_settings.test.ts | 5 + .../server/rest_api/dynamic_settings.ts | 1 + .../rest_api/monitors/monitors_details.ts | 6 +- .../rest_api/monitors/monitors_durations.ts | 1 + .../apis/uptime/rest/dynamic_settings.ts | 1 + x-pack/test/functional/apps/uptime/index.ts | 42 ++-- .../test/functional/apps/uptime/settings.ts | 1 + .../functional/page_objects/uptime_page.ts | 1 + .../test/functional/services/uptime/common.ts | 6 +- .../functional/services/uptime/navigation.ts | 1 + .../functional/services/uptime/overview.ts | 4 + .../functional/services/uptime/settings.ts | 1 + .../apps/uptime/index.ts | 1 + .../apps/uptime/simple_down_alert.ts | 109 +++++++++++ 93 files changed, 1828 insertions(+), 265 deletions(-) rename x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__tests__/__mocks__/{mock.ts => poly_layer_mock.ts} (100%) create mode 100644 x-pack/plugins/uptime/public/components/overview/monitor_list/columns/__tests__/__snapshots__/enable_alert.test.tsx.snap create mode 100644 x-pack/plugins/uptime/public/components/overview/monitor_list/columns/__tests__/enable_alert.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/overview/monitor_list/columns/define_connectors.tsx create mode 100644 x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.tsx create mode 100644 x-pack/plugins/uptime/public/components/overview/monitor_list/columns/translations.ts create mode 100644 x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/enabled_alerts.tsx create mode 100644 x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx create mode 100644 x-pack/plugins/uptime/public/components/settings/alert_defaults_form.tsx create mode 100644 x-pack/plugins/uptime/public/hooks/use_init_app.ts delete mode 100644 x-pack/plugins/uptime/public/lib/__mocks__/index.ts create mode 100644 x-pack/plugins/uptime/public/lib/__mocks__/uptime_store.mock.ts rename x-pack/plugins/uptime/public/lib/__mocks__/{react_router_history.mock.ts => ut_router_history.mock.ts} (100%) create mode 100644 x-pack/plugins/uptime/public/lib/alert_types/alert_messages.tsx create mode 100644 x-pack/plugins/uptime/public/state/alerts/alerts.ts delete mode 100644 x-pack/plugins/uptime/public/state/reducers/alerts.ts create mode 100644 x-pack/test/functional_with_es_ssl/apps/uptime/simple_down_alert.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 7808e2a7f608d1..f73fac22590671 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -21,6 +21,7 @@ export { AlertTypeParamsExpressionProps, ValidationResult, ActionVariable, + ActionConnector, } from './types'; export { ConnectorAddFlyout, diff --git a/x-pack/plugins/uptime/common/constants/rest_api.ts b/x-pack/plugins/uptime/common/constants/rest_api.ts index f3f06f776260dc..be1f498c2e75dd 100644 --- a/x-pack/plugins/uptime/common/constants/rest_api.ts +++ b/x-pack/plugins/uptime/common/constants/rest_api.ts @@ -24,6 +24,9 @@ export enum API_URLS { ML_DELETE_JOB = `/api/ml/jobs/delete_jobs`, ML_CAPABILITIES = '/api/ml/ml_capabilities', ML_ANOMALIES_RESULT = `/api/ml/results/anomalies_table_data`, + + ALERT_ACTIONS = '/api/actions', + CREATE_ALERT = '/api/alerts/alert', ALERT = '/api/alerts/alert/', ALERTS_FIND = '/api/alerts/_find', } diff --git a/x-pack/plugins/uptime/common/constants/settings_defaults.ts b/x-pack/plugins/uptime/common/constants/settings_defaults.ts index b9e99a54b3b11a..6eb2a1913b8714 100644 --- a/x-pack/plugins/uptime/common/constants/settings_defaults.ts +++ b/x-pack/plugins/uptime/common/constants/settings_defaults.ts @@ -10,4 +10,5 @@ export const DYNAMIC_SETTINGS_DEFAULTS: DynamicSettings = { heartbeatIndices: 'heartbeat-8*', certAgeThreshold: 730, certExpirationThreshold: 30, + defaultConnectors: [], }; diff --git a/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts b/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts index 5a355dc576c0aa..971a9f51bfae15 100644 --- a/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts +++ b/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts @@ -25,6 +25,7 @@ export const AtomicStatusCheckParamsType = t.intersection([ search: t.string, filters: StatusCheckFiltersType, shouldCheckStatus: t.boolean, + isAutoGenerated: t.boolean, }), ]); @@ -34,6 +35,7 @@ export const StatusCheckParamsType = t.intersection([ t.partial({ filters: t.string, shouldCheckStatus: t.boolean, + isAutoGenerated: t.boolean, }), t.type({ locations: t.array(t.string), diff --git a/x-pack/plugins/uptime/common/runtime_types/dynamic_settings.ts b/x-pack/plugins/uptime/common/runtime_types/dynamic_settings.ts index a0ec92f7d869b7..3621827b294a6e 100644 --- a/x-pack/plugins/uptime/common/runtime_types/dynamic_settings.ts +++ b/x-pack/plugins/uptime/common/runtime_types/dynamic_settings.ts @@ -10,6 +10,7 @@ export const DynamicSettingsType = t.type({ heartbeatIndices: t.string, certAgeThreshold: t.number, certExpirationThreshold: t.number, + defaultConnectors: t.array(t.string), }); export const DynamicSettingsSaveType = t.intersection([ diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor/details.ts b/x-pack/plugins/uptime/common/runtime_types/monitor/details.ts index bf81c91bae6339..c622d4f19bade4 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor/details.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor/details.ts @@ -18,7 +18,6 @@ export type MonitorError = t.TypeOf; export const MonitorDetailsType = t.intersection([ t.type({ monitorId: t.string }), - t.partial({ error: MonitorErrorType }), - t.partial({ timestamp: t.string }), + t.partial({ error: MonitorErrorType, timestamp: t.string, alerts: t.unknown }), ]); export type MonitorDetails = t.TypeOf; diff --git a/x-pack/plugins/uptime/public/components/common/react_router_helpers/__tests__/link_for_eui.test.tsx b/x-pack/plugins/uptime/public/components/common/react_router_helpers/__tests__/link_for_eui.test.tsx index 4a681f6fa60bf1..845b597a8ad18c 100644 --- a/x-pack/plugins/uptime/public/components/common/react_router_helpers/__tests__/link_for_eui.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/react_router_helpers/__tests__/link_for_eui.test.tsx @@ -8,10 +8,10 @@ import React from 'react'; import { shallow, mount } from 'enzyme'; import { EuiLink, EuiButton } from '@elastic/eui'; -import '../../../../lib/__mocks__/react_router_history.mock'; +import '../../../../lib/__mocks__/ut_router_history.mock'; import { ReactRouterEuiLink, ReactRouterEuiButton } from '../link_for_eui'; -import { mockHistory } from '../../../../lib/__mocks__'; +import { mockHistory } from '../../../../lib/__mocks__/ut_router_history.mock'; describe('EUI & React Router Component Helpers', () => { beforeEach(() => { diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx index f4382b37b3d308..7971c4eb583504 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx @@ -22,7 +22,7 @@ import { useMonitorId } from '../../../hooks'; import { setAlertFlyoutType, setAlertFlyoutVisible } from '../../../state/actions'; import { useAnomalyAlert } from './use_anomaly_alert'; import { ConfirmAlertDeletion } from './confirm_alert_delete'; -import { deleteAlertAction } from '../../../state/actions/alerts'; +import { deleteAnomalyAlertAction } from '../../../state/alerts/alerts'; interface Props { hasMLJob: boolean; @@ -52,7 +52,7 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro const [isConfirmAlertDeleteOpen, setIsConfirmAlertDeleteOpen] = useState(false); const deleteAnomalyAlert = () => - dispatch(deleteAlertAction.get({ alertId: anomalyAlert?.id as string })); + dispatch(deleteAnomalyAlertAction.get({ alertId: anomalyAlert?.id as string })); const showLoading = isMLJobCreating || isMLJobLoading; diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/use_anomaly_alert.ts b/x-pack/plugins/uptime/public/components/monitor/ml/use_anomaly_alert.ts index d204cdf10012a5..949bbadfc9d26a 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/use_anomaly_alert.ts +++ b/x-pack/plugins/uptime/public/components/monitor/ml/use_anomaly_alert.ts @@ -6,10 +6,10 @@ import { useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { getExistingAlertAction } from '../../../state/actions/alerts'; -import { alertSelector, selectAlertFlyoutVisibility } from '../../../state/selectors'; +import { selectAlertFlyoutVisibility } from '../../../state/selectors'; import { UptimeRefreshContext } from '../../../contexts'; import { useMonitorId } from '../../../hooks'; +import { anomalyAlertSelector, getAnomalyAlertAction } from '../../../state/alerts/alerts'; export const useAnomalyAlert = () => { const { lastRefresh } = useContext(UptimeRefreshContext); @@ -18,12 +18,12 @@ export const useAnomalyAlert = () => { const monitorId = useMonitorId(); - const { data: anomalyAlert } = useSelector(alertSelector); + const { data: anomalyAlert } = useSelector(anomalyAlertSelector); const alertFlyoutVisible = useSelector(selectAlertFlyoutVisibility); useEffect(() => { - dispatch(getExistingAlertAction.get({ monitorId })); + dispatch(getAnomalyAlertAction.get({ monitorId })); }, [monitorId, lastRefresh, dispatch, alertFlyoutVisible]); return anomalyAlert; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__tests__/__mocks__/mock.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__tests__/__mocks__/poly_layer_mock.ts similarity index 100% rename from x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__tests__/__mocks__/mock.ts rename to x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__tests__/__mocks__/poly_layer_mock.ts diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__tests__/map_config.test.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__tests__/map_config.test.ts index 18b43434da24b1..582c60f048bed6 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__tests__/map_config.test.ts +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__tests__/map_config.test.ts @@ -5,7 +5,7 @@ */ import { getLayerList } from '../map_config'; -import { mockLayerList } from './__mocks__/mock'; +import { mockLayerList } from './__mocks__/poly_layer_mock'; import { LocationPoint } from '../embedded_map'; import { UptimeAppColors } from '../../../../../../apps/uptime_app'; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index 4898ec00b38e24..e177f1cf011479 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -1055,9 +1055,26 @@ exports[`MonitorList component renders the monitor list 1`] = `
+
+ + + ) : ( + ); } @@ -154,7 +251,7 @@ export class FeatureProperties extends React.Component { const rows = this.state.properties.map((tooltipProperty) => { const label = tooltipProperty.getPropertyName(); return ( - + '); - fragments.push(''); - rowStarted = true; - } - - fragments.push( - '' + - '' - ); - } - - if (rowStarted) - fragments.push(''); - - if (fragments.length == 0) - return; - - var table = '
+
+ + Status alert + +
+
+
+ Status alert +
+
+
+
+
+ +
+
+
+
+
+
+ Status alert +
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+ +`; + +exports[`EnableAlertComponent shallow renders without errors for valid props 1`] = ` + + + + + +`; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/__tests__/enable_alert.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/__tests__/enable_alert.test.tsx new file mode 100644 index 00000000000000..4f41ea4c0b8958 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/__tests__/enable_alert.test.tsx @@ -0,0 +1,90 @@ +/* + * 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 { EnableMonitorAlert } from '../enable_alert'; +import * as redux from 'react-redux'; +import { + mountWithRouterRedux, + renderWithRouterRedux, + shallowWithRouterRedux, +} from '../../../../../lib'; +import { EuiPopover, EuiText } from '@elastic/eui'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../common/constants'; + +describe('EnableAlertComponent', () => { + let defaultConnectors: string[] = []; + let alerts: any = []; + + beforeEach(() => { + jest.spyOn(redux, 'useDispatch').mockReturnValue(jest.fn()); + + jest.spyOn(redux, 'useSelector').mockImplementation((fn, d) => { + if (fn.name === 'selectDynamicSettings') { + return { + settings: Object.assign(DYNAMIC_SETTINGS_DEFAULTS, { + defaultConnectors, + }), + }; + } + if (fn.name === 'alertsSelector') { + return { + data: { + data: alerts, + }, + loading: false, + }; + } + return {}; + }); + }); + + it('shallow renders without errors for valid props', () => { + const wrapper = shallowWithRouterRedux( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders without errors for valid props', () => { + const wrapper = renderWithRouterRedux( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + it('displays define connectors when there is none', () => { + defaultConnectors = []; + const wrapper = mountWithRouterRedux( + + ); + expect(wrapper.find(EuiPopover)).toHaveLength(1); + wrapper.find('button').simulate('click'); + expect(wrapper.find(EuiText).text()).toBe( + 'To start enabling alerts, please define a default alert action connector in Settings' + ); + }); + + it('does not displays define connectors when there is connector', () => { + defaultConnectors = ['infra-slack-connector-id']; + const wrapper = mountWithRouterRedux( + + ); + + expect(wrapper.find(EuiPopover)).toHaveLength(0); + }); + + it('displays disable when alert is there', () => { + alerts = [{ id: 'test-alert', params: { search: 'testMonitor' } }]; + defaultConnectors = ['infra-slack-connector-id']; + + const wrapper = mountWithRouterRedux( + + ); + + expect(wrapper.find('button').prop('aria-label')).toBe('Disable status alert'); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/define_connectors.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/define_connectors.tsx new file mode 100644 index 00000000000000..673588688db84f --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/define_connectors.tsx @@ -0,0 +1,55 @@ +/* + * 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 { EuiPopover, EuiSwitch, EuiText } from '@elastic/eui'; +import { useRouteMatch } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ReactRouterEuiLink } from '../../../common/react_router_helpers'; +import { MONITOR_ROUTE, SETTINGS_ROUTE } from '../../../../../common/constants'; +import { ENABLE_STATUS_ALERT } from './translations'; +import { SETTINGS_LINK_TEXT } from '../../../../pages/page_header'; + +export const DefineAlertConnectors = () => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onButtonClick = () => setIsPopoverOpen((val) => !val); + const closePopover = () => setIsPopoverOpen(false); + + const isMonitorPage = useRouteMatch(MONITOR_ROUTE); + + return ( + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + > + + {' '} + + {SETTINGS_LINK_TEXT} + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.tsx new file mode 100644 index 00000000000000..8a5a72891c3e76 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.tsx @@ -0,0 +1,130 @@ +/* + * 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, { useEffect, useState } from 'react'; +import { EuiLoadingSpinner, EuiToolTip, EuiSwitch } from '@elastic/eui'; +import { useRouteMatch } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; +import { selectDynamicSettings } from '../../../../state/selectors'; +import { + alertsSelector, + connectorsSelector, + createAlertAction, + deleteAlertAction, + isAlertDeletedSelector, + newAlertSelector, +} from '../../../../state/alerts/alerts'; +import { MONITOR_ROUTE } from '../../../../../common/constants'; +import { DefineAlertConnectors } from './define_connectors'; +import { DISABLE_STATUS_ALERT, ENABLE_STATUS_ALERT } from './translations'; + +interface Props { + monitorId: string; + monitorName?: string; +} + +export const EnableMonitorAlert = ({ monitorId, monitorName }: Props) => { + const [isLoading, setIsLoading] = useState(false); + + const { settings } = useSelector(selectDynamicSettings); + + const isMonitorPage = useRouteMatch(MONITOR_ROUTE); + + const dispatch = useDispatch(); + + const { data: actionConnectors } = useSelector(connectorsSelector); + + const { data: alerts, loading: alertsLoading } = useSelector(alertsSelector); + + const { data: deletedAlertId } = useSelector(isAlertDeletedSelector); + + const { data: newAlert } = useSelector(newAlertSelector); + + const isNewAlert = newAlert?.params.search.includes(monitorId); + + let hasAlert = (alerts?.data ?? []).find((alert) => alert.params.search.includes(monitorId)); + + if (isNewAlert) { + // if it's newly created alert, we assign that quickly without waiting for find alert result + hasAlert = newAlert!; + } + if (deletedAlertId === hasAlert?.id) { + // if it just got deleted, we assign that quickly without waiting for find alert result + hasAlert = undefined; + } + + const defaultActions = (actionConnectors ?? []).filter((act) => + settings?.defaultConnectors?.includes(act.id) + ); + + const enableAlert = () => { + dispatch( + createAlertAction.get({ + defaultActions, + monitorId, + monitorName, + }) + ); + setIsLoading(true); + }; + + const disableAlert = () => { + if (hasAlert) { + dispatch( + deleteAlertAction.get({ + alertId: hasAlert.id, + }) + ); + setIsLoading(true); + } + }; + + useEffect(() => { + setIsLoading(false); + }, [hasAlert, deletedAlertId]); + + const hasDefaultConnectors = (settings?.defaultConnectors ?? []).length > 0; + + const showSpinner = isLoading || (alertsLoading && !alerts); + + const onAlertClick = () => { + if (hasAlert) { + disableAlert(); + } else { + enableAlert(); + } + }; + const btnLabel = hasAlert ? DISABLE_STATUS_ALERT : ENABLE_STATUS_ALERT; + + return hasDefaultConnectors || hasAlert ? ( +
+ { + + <> + {' '} + {showSpinner && } + + + } +
+ ) : ( + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/translations.ts b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/translations.ts new file mode 100644 index 00000000000000..421072ab603c2c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/translations.ts @@ -0,0 +1,15 @@ +/* + * 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 ENABLE_STATUS_ALERT = i18n.translate('xpack.uptime.monitorList.enableDownAlert', { + defaultMessage: 'Enable status alert', +}); + +export const DISABLE_STATUS_ALERT = i18n.translate('xpack.uptime.monitorList.disableDownAlert', { + defaultMessage: 'Disable status alert', +}); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx index ce4c518d82255c..718e9e99480811 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx @@ -31,6 +31,8 @@ import { MonitorList } from '../../../state/reducers/monitor_list'; import { CertStatusColumn } from './cert_status_column'; import { MonitorListHeader } from './monitor_list_header'; import { URL_LABEL } from '../../common/translations'; +import { EnableMonitorAlert } from './columns/enable_alert'; +import { STATUS_ALERT_COLUMN } from './translations'; interface Props extends MonitorListProps { pageSize: number; @@ -49,7 +51,13 @@ export const noItemsMessage = (loading: boolean, filters?: string) => { return !!filters ? labels.NO_MONITOR_ITEM_SELECTED : labels.NO_DATA_MESSAGE; }; -export const MonitorListComponent: React.FC = ({ +export const MonitorListComponent: ({ + filters, + monitorList: { list, error, loading }, + linkParameters, + pageSize, + setPageSize, +}: Props) => any = ({ filters, monitorList: { list, error, loading }, linkParameters, @@ -69,7 +77,7 @@ export const MonitorListComponent: React.FC = ({ ...map, [id]: ( monitorId === id)} + summary={items.find(({ monitor_id: monitorId }) => monitorId === id)!} /> ), }; @@ -135,6 +143,18 @@ export const MonitorListComponent: React.FC = ({ ), }, + { + align: 'center' as const, + field: '', + name: STATUS_ALERT_COLUMN, + width: '150px', + render: (item: MonitorSummary) => ( + + ), + }, { align: 'right' as const, field: 'monitor_id', diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap index 42c885dfaf515f..e4450e67ae5b3b 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap @@ -86,6 +86,7 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there are } > Get https://expired.badssl.com: x509: certificate has expired or is not yet valid diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx index 502ccd53ef80ca..302137199276bf 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx @@ -52,7 +52,11 @@ describe('MonitorListDrawer component', () => { it('renders nothing when no summary data is present', () => { const component = shallowWithRouter( - + ); expect(component).toEqual({}); }); @@ -60,14 +64,22 @@ describe('MonitorListDrawer component', () => { it('renders nothing when no check data is present', () => { delete summary.state.summaryPings; const component = shallowWithRouter( - + ); expect(component).toEqual({}); }); it('renders a MonitorListDrawer when there is only one check', () => { const component = shallowWithRouter( - + ); expect(component).toMatchSnapshot(); }); @@ -88,7 +100,11 @@ describe('MonitorListDrawer component', () => { } const component = shallowWithRouter( - + ); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/enabled_alerts.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/enabled_alerts.tsx new file mode 100644 index 00000000000000..d869c6d78ec11d --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/enabled_alerts.tsx @@ -0,0 +1,57 @@ +/* + * 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, { useContext } from 'react'; +import { EuiCallOut, EuiListGroup, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiListGroupItemProps } from '@elastic/eui/src/components/list_group/list_group_item'; +import { i18n } from '@kbn/i18n'; +import { UptimeSettingsContext } from '../../../../contexts'; +import { Alert } from '../../../../../../triggers_actions_ui/public'; + +interface Props { + monitorAlerts: Alert[]; + loading: boolean; +} + +export const EnabledAlerts = ({ monitorAlerts, loading }: Props) => { + const { basePath } = useContext(UptimeSettingsContext); + + const listItems: EuiListGroupItemProps[] = []; + + (monitorAlerts ?? []).forEach((alert, ind) => { + listItems.push({ + label: alert.name, + href: basePath + '/app/management/insightsAndAlerting/triggersActions/alert/' + alert.id, + size: 's', + 'data-test-subj': 'uptimeMonitorListDrawerAlert' + ind, + }); + }); + + return ( + <> + + + +

+ {i18n.translate('xpack.uptime.monitorList.enabledAlerts.title', { + defaultMessage: 'Enabled alerts:', + description: 'Alerts enabled for this monitor', + })} +

+
+
+ {listItems.length === 0 && !loading && ( + + )} + {loading ? : } + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/list_drawer_container.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/list_drawer_container.tsx index bec32ace27f2b2..fd68a487a21e40 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/list_drawer_container.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/list_drawer_container.tsx @@ -4,44 +4,52 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect } from 'react'; -import { connect } from 'react-redux'; +import React, { useContext, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { AppState } from '../../../../state'; -import { monitorDetailsSelector } from '../../../../state/selectors'; -import { MonitorDetailsActionPayload } from '../../../../state/actions/types'; +import { monitorDetailsLoadingSelector, monitorDetailsSelector } from '../../../../state/selectors'; import { getMonitorDetailsAction } from '../../../../state/actions/monitor'; import { MonitorListDrawerComponent } from './monitor_list_drawer'; import { useGetUrlParams } from '../../../../hooks'; -import { MonitorDetails, MonitorSummary } from '../../../../../common/runtime_types'; +import { MonitorSummary } from '../../../../../common/runtime_types'; +import { alertsSelector } from '../../../../state/alerts/alerts'; +import { UptimeRefreshContext } from '../../../../contexts'; interface ContainerProps { summary: MonitorSummary; - monitorDetails: MonitorDetails; - loadMonitorDetails: typeof getMonitorDetailsAction; } -const Container: React.FC = ({ summary, loadMonitorDetails, monitorDetails }) => { +export const MonitorListDrawer: React.FC = ({ summary }) => { + const { lastRefresh } = useContext(UptimeRefreshContext); + const monitorId = summary?.monitor_id; const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = useGetUrlParams(); - useEffect(() => { - loadMonitorDetails({ - dateStart, - dateEnd, - monitorId, - }); - }, [dateStart, dateEnd, monitorId, loadMonitorDetails]); - return ; -}; + const monitorDetails = useSelector((state: AppState) => monitorDetailsSelector(state, summary)); + + const isLoading = useSelector(monitorDetailsLoadingSelector); -const mapStateToProps = (state: AppState, { summary }: any) => ({ - monitorDetails: monitorDetailsSelector(state, summary), -}); + const dispatch = useDispatch(); -const mapDispatchToProps = (dispatch: any) => ({ - loadMonitorDetails: (actionPayload: MonitorDetailsActionPayload) => - dispatch(getMonitorDetailsAction(actionPayload)), -}); + const { data: alerts, loading: alertsLoading } = useSelector(alertsSelector); -export const MonitorListDrawer = connect(mapStateToProps, mapDispatchToProps)(Container); + const hasAlert = (alerts?.data ?? []).find((alert) => alert.params.search.includes(monitorId)); + + useEffect(() => { + dispatch( + getMonitorDetailsAction.get({ + dateStart, + dateEnd, + monitorId, + }) + ); + }, [dateStart, dateEnd, monitorId, dispatch, hasAlert, lastRefresh]); + return ( + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx index 305455c8ba573f..4b359099bc58c9 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx @@ -6,11 +6,13 @@ import React from 'react'; import styled from 'styled-components'; -import { EuiLink, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; +import { EuiLink, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; import { MostRecentError } from './most_recent_error'; import { MonitorStatusList } from './monitor_status_list'; import { MonitorDetails, MonitorSummary } from '../../../../../common/runtime_types'; import { ActionsPopover } from './actions_popover/actions_popover_container'; +import { EnabledAlerts } from './enabled_alerts'; +import { Alert } from '../../../../../../triggers_actions_ui/public'; const ContainerDiv = styled.div` padding: 10px; @@ -27,13 +29,18 @@ interface MonitorListDrawerProps { * Monitor details to be fetched from rest api using monitorId */ monitorDetails: MonitorDetails; + loading: boolean; } /** * The elements shown when the user expands the monitor list rows. */ -export function MonitorListDrawerComponent({ summary, monitorDetails }: MonitorListDrawerProps) { +export function MonitorListDrawerComponent({ + summary, + monitorDetails, + loading, +}: MonitorListDrawerProps) { const monitorUrl = summary?.state?.url?.full || ''; return summary && summary.state.summaryPings ? ( @@ -51,8 +58,8 @@ export function MonitorListDrawerComponent({ summary, monitorDetails }: MonitorL - + {monitorDetails && monitorDetails.error && ( { heartbeatIndices: 'heartbeat-8*', certExpirationThreshold: 7, certAgeThreshold: 36, + defaultConnectors: [], }} fieldErrors={null} isDisabled={false} @@ -37,6 +38,7 @@ describe('CertificateForm', () => { heartbeatIndices: 'heartbeat-8*', certExpirationThreshold: 7, certAgeThreshold: 36, + defaultConnectors: [], }} fieldErrors={null} isDisabled={false} @@ -90,6 +92,7 @@ describe('CertificateForm', () => { heartbeatIndices: 'heartbeat-8*', certExpirationThreshold: 7, certAgeThreshold: 36, + defaultConnectors: [], }} fieldErrors={null} isDisabled={false} diff --git a/x-pack/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx b/x-pack/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx index 68a0d96d491b64..01b66263d3e933 100644 --- a/x-pack/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx +++ b/x-pack/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx @@ -19,6 +19,7 @@ describe('CertificateForm', () => { heartbeatIndices: 'heartbeat-8*', certAgeThreshold: 36, certExpirationThreshold: 7, + defaultConnectors: [], }} fieldErrors={null} isDisabled={false} diff --git a/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx b/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx new file mode 100644 index 00000000000000..60c0807ae89a89 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx @@ -0,0 +1,70 @@ +/* + * 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, { useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useDispatch } from 'react-redux'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { + ActionsConnectorsContextProvider, + ConnectorAddFlyout, +} from '../../../../triggers_actions_ui/public'; + +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { getConnectorsAction } from '../../state/alerts/alerts'; + +interface Props { + focusInput: () => void; +} +export const AddConnectorFlyout = ({ focusInput }: Props) => { + const [addFlyoutVisible, setAddFlyoutVisibility] = useState(false); + + const { + services: { + triggers_actions_ui: { actionTypeRegistry }, + application, + docLinks, + http, + notifications, + }, + } = useKibana(); + + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(getConnectorsAction.get()); + focusInput(); + }, [addFlyoutVisible, dispatch, focusInput]); + + return ( + <> + setAddFlyoutVisibility(true)} + > + + + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/settings/alert_defaults_form.tsx b/x-pack/plugins/uptime/public/components/settings/alert_defaults_form.tsx new file mode 100644 index 00000000000000..b3b38a84e4f22a --- /dev/null +++ b/x-pack/plugins/uptime/public/components/settings/alert_defaults_form.tsx @@ -0,0 +1,182 @@ +/* + * 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, { useEffect, useState, useRef, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiDescribedFormGroup, + EuiFormRow, + EuiTitle, + EuiSpacer, + EuiComboBox, + EuiIcon, + EuiComboBoxOptionOption, +} from '@elastic/eui'; +import { useSelector } from 'react-redux'; +import styled from 'styled-components'; +import { SettingsFormProps } from '../../pages/settings'; +import { connectorsSelector } from '../../state/alerts/alerts'; +import { AddConnectorFlyout } from './add_connector_flyout'; +import { useGetUrlParams, useUrlParams } from '../../hooks'; +import { alertFormI18n } from './translations'; +import { useInitApp } from '../../hooks/use_init_app'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; + +type ConnectorOption = EuiComboBoxOptionOption; + +const ConnectorSpan = styled.span` + .euiIcon { + margin-right: 5px; + } + > img { + width: 16px; + height: 20px; + } +`; + +export const AlertDefaultsForm: React.FC = ({ + onChange, + loading, + formFields, + fieldErrors, + isDisabled, +}) => { + const { + services: { + triggers_actions_ui: { actionTypeRegistry }, + }, + } = useKibana(); + const { focusConnectorField } = useGetUrlParams(); + + const updateUrlParams = useUrlParams()[1]; + + const inputRef = useRef(null); + + useInitApp(); + + useEffect(() => { + if (focusConnectorField && inputRef.current && !loading) { + inputRef.current.focus(); + } + }, [focusConnectorField, inputRef, loading]); + + const { data = [] } = useSelector(connectorsSelector); + + const [error, setError] = useState(undefined); + + const onBlur = () => { + if (inputRef.current) { + const { value } = inputRef.current; + setError(value.length === 0 ? undefined : `"${value}" is not a valid option`); + } + if (inputRef.current && !loading && focusConnectorField) { + updateUrlParams({ focusConnectorField: undefined }); + } + }; + + const onSearchChange = (value: string, hasMatchingOptions?: boolean) => { + setError( + value.length === 0 || hasMatchingOptions ? undefined : `"${value}" is not a valid option` + ); + }; + + const options = (data ?? []).map((connectorAction) => ({ + value: connectorAction.id, + label: connectorAction.name, + 'data-test-subj': connectorAction.name, + })); + + const renderOption = (option: ConnectorOption) => { + const { label, value } = option; + + const { actionTypeId: type } = data?.find((dt) => dt.id === value) ?? {}; + return ( + + + {label} + + ); + }; + + const onOptionChange = (selectedOptions: ConnectorOption[]) => { + onChange({ + defaultConnectors: selectedOptions.map((item) => { + const conOpt = data?.find((dt) => dt.id === item.value)!; + return conOpt.id; + }), + }); + }; + + return ( + <> + +

+ +

+
+ + + + + } + description={ + + } + > + + } + > + + formFields?.defaultConnectors?.includes(opt.value) + )} + inputRef={(input) => { + inputRef.current = input; + }} + onSearchChange={onSearchChange} + onBlur={onBlur} + isLoading={loading} + isDisabled={isDisabled} + onChange={onOptionChange} + data-test-subj={`default-connectors-input-${loading ? 'loading' : 'loaded'}`} + renderOption={renderOption} + /> + + + { + if (inputRef.current) { + inputRef.current.focus(); + } + }, [])} + /> + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/settings/translations.ts b/x-pack/plugins/uptime/public/components/settings/translations.ts index 2de25a44165c6c..f9f3b0b6af9a99 100644 --- a/x-pack/plugins/uptime/public/components/settings/translations.ts +++ b/x-pack/plugins/uptime/public/components/settings/translations.ts @@ -22,3 +22,12 @@ export const certificateFormTranslations = { } ), }; + +export const alertFormI18n = { + inputPlaceHolder: i18n.translate( + 'xpack.uptime.sourceConfiguration.alertDefaultForm.selectConnector', + { + defaultMessage: 'Please select one or more connectors', + } + ), +}; diff --git a/x-pack/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap b/x-pack/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap index 5d2565b7210da8..5bbb606b6142fe 100644 --- a/x-pack/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap +++ b/x-pack/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap @@ -209,7 +209,7 @@ exports[`useUrlParams deletes keys that do not have truthy values 1`] = ` } >
- {"absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-12","dateRangeEnd":"now","filters":"","search":"","selectedPingStatus":"","statusFilter":"","pagination":"foo"} + {"absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-12","dateRangeEnd":"now","filters":"","search":"","selectedPingStatus":"","statusFilter":"","pagination":"foo","focusConnectorField":false}
+ {isOn && ( + + {(formData) => { + onFormData(formData); + return null; + }} + + )} + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + const { + form: { setInputValue }, + find, + } = setup() as TestBed; + + expect(onFormData.mock.calls.length).toBe(0); // Not present in the DOM yet + + // Make some changes to the form fields + await act(async () => { + setInputValue('nameField', 'updated value'); + }); + + // Update state to trigger the mounting of the FormDataProvider + await act(async () => { + find('btn').simulate('click').update(); + }); + + expect(onFormData.mock.calls.length).toBe(1); + + const [formDataUpdated] = onFormData.mock.calls[onFormData.mock.calls.length - 1] as Parameters< + OnUpdateHandler + >; + + expect(formDataUpdated).toEqual({ + name: 'updated value', + }); + }); + test('props.pathsToWatch (string): should not re-render the children when the field that changed is not the one provided', async () => { const onFormData = jest.fn(); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts index 4c8e91b13b1b76..3630b902f05649 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts @@ -31,6 +31,7 @@ export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) = const form = useFormContext(); const { subscribe } = form; const previousRawData = useRef(form.__getFormData$().value); + const isMounted = useRef(false); const [formData, setFormData] = useState(previousRawData.current); const onFormData = useCallback( @@ -59,5 +60,17 @@ export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) = return subscription.unsubscribe; }, [subscribe, onFormData]); + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); + + if (!isMounted.current && Object.keys(formData).length === 0) { + // No field has mounted yet, don't render anything + return null; + } + return children(formData); }); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index 01d9f8a59129a6..9d22e4eb2ee5e5 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -43,38 +43,41 @@ export const useField = ( deserializer, } = config; - const { getFormData, __removeField, __updateFormDataAt, __validateFields } = form; + const { + getFormData, + getFields, + __addField, + __removeField, + __updateFormDataAt, + __validateFields, + } = form; - /** - * This callback is both used as the initial "value" state getter, **and** for when we reset the form - * (and thus reset the field value). When we reset the form, we can provide a new default value (which will be - * passed through this "initialValueGetter" handler). - */ - const initialValueGetter = useCallback( - (updatedDefaultValue = initialValue) => { - if (typeof updatedDefaultValue === 'function') { - return deserializer ? deserializer(updatedDefaultValue()) : updatedDefaultValue(); + const deserializeValue = useCallback( + (rawValue = initialValue) => { + if (typeof rawValue === 'function') { + return deserializer ? deserializer(rawValue()) : rawValue(); } - return deserializer ? deserializer(updatedDefaultValue) : updatedDefaultValue; + return deserializer ? deserializer(rawValue) : rawValue; }, [initialValue, deserializer] ); - const [value, setStateValue] = useState(initialValueGetter); + const [value, setStateValue] = useState(deserializeValue); const [errors, setErrors] = useState([]); const [isPristine, setPristine] = useState(true); const [isValidating, setValidating] = useState(false); const [isChangingValue, setIsChangingValue] = useState(false); const [isValidated, setIsValidated] = useState(false); + const validateCounter = useRef(0); const changeCounter = useRef(0); const inflightValidation = useRef | null>(null); const debounceTimeout = useRef(null); - const isUnmounted = useRef(false); + const isMounted = useRef(false); // -- HELPERS // ---------------------------------- - const serializeOutput: FieldHook['__serializeOutput'] = useCallback( + const serializeValue: FieldHook['__serializeValue'] = useCallback( (rawValue = value) => { return serializer ? serializer(rawValue) : rawValue; }, @@ -121,8 +124,11 @@ export const useField = ( if (debounceTimeout.current) { clearTimeout(debounceTimeout.current); + debounceTimeout.current = null; } + setPristine(false); + if (errorDisplayDelay > 0) { setIsChangingValue(true); } @@ -135,10 +141,14 @@ export const useField = ( // Update the form data observable __updateFormDataAt(path, value); - // Validate field(s) and update form.isValid state - await __validateFields(fieldsToValidateOnChange ?? [path]); + // Validate field(s) (that will update form.isValid state) + // We only validate if the value is different than the initial or default value + // to avoid validating after a form.reset() call. + if (value !== initialValue && value !== defaultValue) { + await __validateFields(fieldsToValidateOnChange ?? [path]); + } - if (isUnmounted.current) { + if (isMounted.current === false) { return; } @@ -160,10 +170,12 @@ export const useField = ( } } }, [ - valueChangeListener, - errorDisplayDelay, path, value, + defaultValue, + initialValue, + valueChangeListener, + errorDisplayDelay, fieldsToValidateOnChange, __updateFormDataAt, __validateFields, @@ -229,7 +241,7 @@ export const useField = ( inflightValidation.current = validator({ value: (valueToValidate as unknown) as string, errors: validationErrors, - form, + form: { getFormData, getFields }, formData, path, }) as Promise; @@ -273,7 +285,7 @@ export const useField = ( const validationResult = validator({ value: (valueToValidate as unknown) as string, errors: validationErrors, - form, + form: { getFormData, getFields }, formData, path, }); @@ -308,7 +320,7 @@ export const useField = ( // We first try to run the validations synchronously return runSync(); }, - [clearErrors, cancelInflightValidation, validations, form, path] + [clearErrors, cancelInflightValidation, validations, getFormData, getFields, path] ); // -- API @@ -331,12 +343,12 @@ export const useField = ( setValidating(true); // By the time our validate function has reached completion, it’s possible - // that validate() will have been called again. If this is the case, we need + // that we have called validate() again. If this is the case, we need // to ignore the results of this invocation and only use the results of // the most recent invocation to update the error state for a field const validateIteration = ++validateCounter.current; - const onValidationErrors = (_validationErrors: ValidationError[]): FieldValidateResponse => { + const onValidationResult = (_validationErrors: ValidationError[]): FieldValidateResponse => { if (validateIteration === validateCounter.current) { // This is the most recent invocation setValidating(false); @@ -360,9 +372,9 @@ export const useField = ( }); if (Reflect.has(validationErrors, 'then')) { - return (validationErrors as Promise).then(onValidationErrors); + return (validationErrors as Promise).then(onValidationResult); } - return onValidationErrors(validationErrors as ValidationError[]); + return onValidationResult(validationErrors as ValidationError[]); }, [getFormData, value, runValidations] ); @@ -374,15 +386,11 @@ export const useField = ( */ const setValue: FieldHook['setValue'] = useCallback( (newValue) => { - if (isPristine) { - setPristine(false); - } - const formattedValue = formatInputValue(newValue); setStateValue(formattedValue); return formattedValue; }, - [formatInputValue, isPristine] + [formatInputValue] ); const _setErrors: FieldHook['setErrors'] = useCallback((_errors) => { @@ -447,32 +455,17 @@ export const useField = ( setErrors([]); if (resetValue) { - const newValue = initialValueGetter(updatedDefaultValue ?? defaultValue); + const newValue = deserializeValue(updatedDefaultValue ?? defaultValue); setValue(newValue); return newValue; } }, - [setValue, initialValueGetter, defaultValue] + [setValue, deserializeValue, defaultValue] ); - // -- EFFECTS - // ---------------------------------- - useEffect(() => { - if (isPristine) { - // Avoid validate on mount - return; - } - - onValueChange(); + const isValid = errors.length === 0; - return () => { - if (debounceTimeout.current) { - clearTimeout(debounceTimeout.current); - } - }; - }, [isPristine, onValueChange]); - - const field: FieldHook = useMemo(() => { + const field = useMemo>(() => { return { path, type, @@ -481,9 +474,8 @@ export const useField = ( helpText, value, errors, - form, isPristine, - isValid: errors.length === 0, + isValid, isValidating, isValidated, isChangingValue, @@ -494,7 +486,7 @@ export const useField = ( clearErrors, validate, reset, - __serializeOutput: serializeOutput, + __serializeValue: serializeValue, }; }, [ path, @@ -503,9 +495,9 @@ export const useField = ( labelAppend, helpText, value, - form, isPristine, errors, + isValid, isValidating, isValidated, isChangingValue, @@ -516,18 +508,43 @@ export const useField = ( clearErrors, validate, reset, - serializeOutput, + serializeValue, ]); - form.__addField(field as FieldHook); + // ---------------------------------- + // -- EFFECTS + // ---------------------------------- + useEffect(() => { + __addField(field as FieldHook); + }, [field, __addField]); useEffect(() => { return () => { - // Remove field from the form when it is unmounted or if its path changes. - isUnmounted.current = true; __removeField(path); }; }, [path, __removeField]); + useEffect(() => { + if (!isMounted.current) { + return; + } + + onValueChange(); + + return () => { + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); + } + }; + }, [onValueChange]); + + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + }; + }, []); + return field; }; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index c3f6ecc7f48312..35bac5b9a58c65 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -40,19 +40,26 @@ export function useForm( const { onSubmit, schema, serializer, deserializer, options, id = 'default', defaultValue } = formConfig ?? {}; - const formDefaultValue = useMemo<{ [key: string]: any }>(() => { - if (defaultValue === undefined || Object.keys(defaultValue).length === 0) { - return {}; - } + const initDefaultValue = useCallback( + (_defaultValue?: Partial): { [key: string]: any } => { + if (_defaultValue === undefined || Object.keys(_defaultValue).length === 0) { + return {}; + } - const defaultValueFiltered = Object.entries(defaultValue as object) - .filter(({ 1: value }) => value !== undefined) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); + const filtered = Object.entries(_defaultValue as object) + .filter(({ 1: value }) => value !== undefined) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); - return deserializer ? (deserializer(defaultValueFiltered) as any) : defaultValueFiltered; - }, [defaultValue, deserializer]); + return deserializer ? (deserializer(filtered) as any) : filtered; + }, + [deserializer] + ); - const defaultValueDeserialized = useRef(formDefaultValue); + const defaultValueMemoized = useMemo<{ [key: string]: any }>(() => { + return initDefaultValue(defaultValue); + }, [defaultValue, initDefaultValue]); + + const defaultValueDeserialized = useRef(defaultValueMemoized); const { errorDisplayDelay, stripEmptyFields: doStripEmptyFields } = options ?? {}; const formOptions = useMemo( @@ -68,7 +75,7 @@ export function useForm( const [isValid, setIsValid] = useState(undefined); const fieldsRefs = useRef({}); const formUpdateSubscribers = useRef([]); - const isUnmounted = useRef(false); + const isMounted = useRef(false); // formData$ is an observable we can subscribe to in order to receive live // update of the raw form data. As an observable it does not trigger any React @@ -77,14 +84,6 @@ export function useForm( // and updating its state to trigger the necessary view render. const formData$ = useRef | null>(null); - useEffect(() => { - return () => { - formUpdateSubscribers.current.forEach((subscription) => subscription.unsubscribe()); - formUpdateSubscribers.current = []; - isUnmounted.current = true; - }; - }, []); - // -- HELPERS // ---------------------------------- const getFormData$ = useCallback((): Subject => { @@ -135,7 +134,7 @@ export function useForm( (getDataOptions: Parameters['getFormData']>[0] = { unflatten: true }) => { if (getDataOptions.unflatten) { const nonEmptyFields = stripEmptyFields(fieldsRefs.current); - const fieldsValue = mapFormFields(nonEmptyFields, (field) => field.__serializeOutput()); + const fieldsValue = mapFormFields(nonEmptyFields, (field) => field.__serializeValue()); return serializer ? (serializer(unflattenObject(fieldsValue)) as T) : (unflattenObject(fieldsValue) as T); @@ -168,45 +167,53 @@ export function useForm( const isFieldValid = (field: FieldHook) => field.isValid && !field.isValidating; - const updateFormValidity = useCallback(() => { - if (isUnmounted.current) { - return; - } - - const fieldsArray = fieldsToArray(); - const areAllFieldsValidated = fieldsArray.every((field) => field.isValidated); - - if (!areAllFieldsValidated) { - // If *not* all the fiels have been validated, the validity of the form is unknown, thus still "undefined" - return undefined; - } - - const isFormValid = fieldsArray.every(isFieldValid); - - setIsValid(isFormValid); - return isFormValid; - }, [fieldsToArray]); - const validateFields: FormHook['__validateFields'] = useCallback( async (fieldNames) => { const fieldsToValidate = fieldNames .map((name) => fieldsRefs.current[name]) .filter((field) => field !== undefined); - if (fieldsToValidate.length === 0) { - // Nothing to validate + const formData = getFormData({ unflatten: false }); + const validationResult = await Promise.all( + fieldsToValidate.map((field) => field.validate({ formData })) + ); + + if (isMounted.current === false) { return { areFieldsValid: true, isFormValid: true }; } - const formData = getFormData({ unflatten: false }); - await Promise.all(fieldsToValidate.map((field) => field.validate({ formData }))); + const areFieldsValid = validationResult.every(Boolean); - const isFormValid = updateFormValidity(); - const areFieldsValid = fieldsToValidate.every(isFieldValid); + const validationResultByPath = fieldsToValidate.reduce((acc, field, i) => { + acc[field.path] = validationResult[i].isValid; + return acc; + }, {} as { [key: string]: boolean }); + + // At this stage we have an updated field validation state inside the "validationResultByPath" object. + // The fields we have in our "fieldsRefs.current" have not been updated yet with the new validation state + // (isValid, isValidated...) as this will happen _after_, when the "useEffect" triggers and calls "addField()". + // This means that we have **stale state value** in our fieldsRefs. + // To know the current form validity, we will then merge the "validationResult" _with_ the fieldsRefs object state, + // the "validationResult" taking presedence over the fieldsRefs values. + const formFieldsValidity = fieldsToArray().map((field) => { + const _isValid = validationResultByPath[field.path] ?? field.isValid; + const _isValidated = + validationResultByPath[field.path] !== undefined ? true : field.isValidated; + return [_isValid, _isValidated]; + }); + + const areAllFieldsValidated = formFieldsValidity.every(({ 1: isValidated }) => isValidated); + + // If *not* all the fiels have been validated, the validity of the form is unknown, thus still "undefined" + const isFormValid = areAllFieldsValidated + ? formFieldsValidity.every(([_isValid]) => _isValid) + : undefined; + + setIsValid(isFormValid); return { areFieldsValid, isFormValid }; }, - [getFormData, updateFormValidity] + [getFormData, fieldsToArray] ); const validateAllFields = useCallback(async (): Promise => { @@ -216,19 +223,12 @@ export function useForm( let isFormValid: boolean | undefined; if (fieldsToValidate.length === 0) { - // We should never enter this condition as the form validity is updated each time - // a field is validated. But sometimes, during tests or race conditions it does not happen and we need - // to wait the next tick (hooks lifecycle being tricky) to make sure the "isValid" state is updated. - // In order to avoid this unintentional behaviour, we add this if condition here. - - // TODO: Fix this when adding tests to the form lib. isFormValid = fieldsArray.every(isFieldValid); - setIsValid(isFormValid); - return isFormValid; + } else { + ({ isFormValid } = await validateFields(fieldsToValidate.map((field) => field.path))); } - ({ isFormValid } = await validateFields(fieldsToValidate.map((field) => field.path))); - + setIsValid(isFormValid); return isFormValid!; }, [fieldsToArray, validateFields]); @@ -236,11 +236,13 @@ export function useForm( (field) => { fieldsRefs.current[field.path] = field; - if (!{}.hasOwnProperty.call(getFormData$().value, field.path)) { - updateFormDataAt(field.path, field.value); + updateFormDataAt(field.path, field.value); + + if (!field.isValidated) { + setIsValid(undefined); } }, - [getFormData$, updateFormDataAt] + [updateFormDataAt] ); const removeField: FormHook['__removeField'] = useCallback( @@ -259,9 +261,16 @@ export function useForm( * After removing a field, the form validity might have changed * (an invalid field might have been removed and now the form is valid) */ - updateFormValidity(); + setIsValid((prev) => { + if (prev === false) { + const isFormValid = fieldsToArray().every(isFieldValid); + return isFormValid; + } + // If the form validity is "true" or "undefined", it does not change after removing a field + return prev; + }); }, - [getFormData$, updateFormValidity] + [getFormData$, fieldsToArray] ); const setFieldValue: FormHook['setFieldValue'] = useCallback((fieldName, value) => { @@ -310,7 +319,7 @@ export function useForm( await onSubmit(formData, isFormValid!); } - if (isUnmounted.current === false) { + if (isMounted.current) { setSubmitting(false); } @@ -322,9 +331,7 @@ export function useForm( const subscribe: FormHook['subscribe'] = useCallback( (handler) => { const subscription = getFormData$().subscribe((raw) => { - if (!isUnmounted.current) { - handler({ isValid, data: { raw, format: getFormData }, validate: validateAllFields }); - } + handler({ isValid, data: { raw, format: getFormData }, validate: validateAllFields }); }); formUpdateSubscribers.current.push(subscription); @@ -351,9 +358,7 @@ export function useForm( const currentFormData = { ...getFormData$().value } as FormData; if (updatedDefaultValue) { - defaultValueDeserialized.current = deserializer - ? (deserializer(updatedDefaultValue) as any) - : updatedDefaultValue; + defaultValueDeserialized.current = initDefaultValue(updatedDefaultValue); } Object.entries(fieldsRefs.current).forEach(([path, field]) => { @@ -374,7 +379,7 @@ export function useForm( setSubmitting(false); setIsValid(undefined); }, - [getFormData$, deserializer, getFieldDefaultValue] + [getFormData$, initDefaultValue, getFieldDefaultValue] ); const form = useMemo>(() => { @@ -425,6 +430,25 @@ export function useForm( validateFields, ]); + useEffect(() => { + if (!isMounted.current) { + return; + } + + // Whenever the "defaultValue" prop changes, reinitialize our ref + defaultValueDeserialized.current = defaultValueMemoized; + }, [defaultValueMemoized]); + + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + formUpdateSubscribers.current.forEach((subscription) => subscription.unsubscribe()); + formUpdateSubscribers.current = []; + }; + }, []); + return { form, }; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index 4b203c3927ffd1..dc495f6eb56b4e 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -38,7 +38,7 @@ export interface FormHook { getFieldDefaultValue: (fieldName: string) => unknown; /* Returns a list of all errors in the form */ getErrors: () => string[]; - reset: (options?: { resetValues?: boolean; defaultValue?: FormData }) => void; + reset: (options?: { resetValues?: boolean; defaultValue?: Partial }) => void; readonly __options: Required; __getFormData$: () => Subject; __addField: (field: FieldHook) => void; @@ -102,7 +102,6 @@ export interface FieldHook { readonly isValidating: boolean; readonly isValidated: boolean; readonly isChangingValue: boolean; - readonly form: FormHook; getErrorsMessages: (args?: { validationType?: 'field' | string; errorCode?: string; @@ -117,7 +116,7 @@ export interface FieldHook { validationType?: string; }) => FieldValidateResponse | Promise; reset: (options?: { resetValue?: boolean; defaultValue?: T }) => unknown | undefined; - __serializeOutput: (rawValue?: unknown) => unknown; + __serializeValue: (rawValue?: unknown) => unknown; } export interface FieldConfig { @@ -154,7 +153,10 @@ export interface ValidationError { export interface ValidationFuncArg { path: string; value: V; - form: FormHook; + form: { + getFormData: FormHook['getFormData']; + getFields: FormHook['getFields']; + }; formData: T; errors: readonly ValidationError[]; } diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts index f92f46d71e7c79..870b8b7ec5509f 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts @@ -85,6 +85,9 @@ export const getFormActions = (testBed: TestBed) => { value: type, }, ]); + }); + + await act(async () => { find('createFieldForm.addButton').simulate('click'); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx index 0320f2ff51da3e..9b27b930b47c45 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx @@ -4,33 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { TextField, UseField, FieldConfig } from '../../../shared_imports'; import { validateUniqueName } from '../../../lib'; import { PARAMETERS_DEFINITION } from '../../../constants'; import { useMappingsState } from '../../../mappings_state_context'; +const { validations, ...rest } = PARAMETERS_DEFINITION.name.fieldConfig as FieldConfig; + export const NameParameter = () => { const { fields: { rootLevelFields, byId }, documentFields: { fieldToAddFieldTo, fieldToEdit }, } = useMappingsState(); - const { validations, ...rest } = PARAMETERS_DEFINITION.name.fieldConfig as FieldConfig; const initialName = fieldToEdit ? byId[fieldToEdit].source.name : undefined; const parentId = fieldToEdit ? byId[fieldToEdit].parentId : fieldToAddFieldTo; - const uniqueNameValidator = validateUniqueName({ rootLevelFields, byId }, initialName, parentId); + const uniqueNameValidator = useCallback( + (arg: any) => { + return validateUniqueName({ rootLevelFields, byId }, initialName, parentId)(arg); + }, + [rootLevelFields, byId, initialName, parentId] + ); - const nameConfig: FieldConfig = { - ...rest, - validations: [ - ...validations!, - { - validator: uniqueNameValidator, - }, - ], - }; + const nameConfig: FieldConfig = useMemo( + () => ({ + ...rest, + validations: [ + ...validations!, + { + validator: uniqueNameValidator, + }, + ], + }), + [uniqueNameValidator] + ); return ( { const suggestedFields = getSuggestedFields(allFields, field); + const fieldConfig = useMemo( + () => ({ + ...getFieldConfig('path'), + deserializer: getDeserializer(allFields), + }), + [allFields] + ); + return ( - + {(pathField) => { const error = pathField.getErrorsMessages(); const isInvalid = error ? Boolean(error.length) : false; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx index 95575124b6abdd..6b5a848ce85d32 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx @@ -163,7 +163,7 @@ export const EditField = React.memo(({ form, field, allFields, exitEdit, updateF - {form.isSubmitted && !form.isValid && ( + {form.isSubmitted && form.isValid === false && ( <> {i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldUpdateButtonLabel', { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx index f2ad37cb45818b..3b55c5ac076c29 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx @@ -59,6 +59,9 @@ export const EditFieldHeaderForm = React.memo( {({ type, subType }) => { + if (!type) { + return null; + } const typeDefinition = TYPE_DEFINITION[type[0].value as MainType]; const hasSubType = typeDefinition.subTypes !== undefined; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx index 6a70592bc2f70d..9adb3957ea9f45 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx @@ -35,7 +35,7 @@ export const ProcessorSettingsFields: FunctionComponent = ({ processor }) if (formDescriptor?.FieldsComponent) { return ( <> - + diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx index 09d0981adf1c29..23425297f34208 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx @@ -21,6 +21,7 @@ const { emptyField } = fieldValidators; const fieldsConfig: FieldsConfig = { value: { + defaultValue: [], type: FIELD_TYPES.COMBO_BOX, deserializer: to.arrayOfStrings, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.appendForm.valueFieldLabel', { diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index 9ead8171bfef66..c7810af13eb74f 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -23,7 +23,7 @@ import { createKibanaContextProviderMock, createStartServicesMock, } from '../lib/kibana/kibana_react.mock'; -import { FieldHook, useForm } from '../../shared_imports'; +import { FieldHook } from '../../shared_imports'; import { SUB_PLUGINS_REDUCER } from './utils'; import { createSecuritySolutionStorageMock, localStorageMock } from './mock_local_storage'; @@ -78,8 +78,6 @@ const TestProvidersComponent: React.FC = ({ export const TestProviders = React.memo(TestProvidersComponent); export const useFormFieldMock = (options?: Partial): FieldHook => { - const { form } = useForm(); - return { path: 'path', type: 'type', @@ -88,7 +86,6 @@ export const useFormFieldMock = (options?: Partial): FieldHook => { isValidating: false, isValidated: false, isChangingValue: false, - form, errors: [], isValid: true, getErrorsMessages: jest.fn(), @@ -98,7 +95,7 @@ export const useFormFieldMock = (options?: Partial): FieldHook => { clearErrors: jest.fn(), validate: jest.fn(), reset: jest.fn(), - __serializeOutput: jest.fn(), + __serializeValue: jest.fn(), ...options, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx index a0384ef52a841b..cdeca54bfc39bb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx @@ -13,13 +13,13 @@ import { EuiFormLabel, EuiIcon, EuiSpacer, + EuiRange, } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { noop } from 'lodash/fp'; import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -import { CommonUseField } from '../../../../cases/components/create'; import { AboutStepRiskScore } from '../../../pages/detection_engine/rules/types'; import { FieldComponent } from '../../../../common/components/autocomplete/field'; import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; @@ -59,11 +59,12 @@ export const RiskScoreField = ({ placeholder, }: RiskScoreFieldProps) => { const fieldTypeFilter = useMemo(() => ['number'], []); + const { value: fieldValue, setValue } = field; const handleFieldChange = useCallback( ([newField]: IFieldType[]): void => { - const values = field.value as AboutStepRiskScore; - field.setValue({ + const values = fieldValue as AboutStepRiskScore; + setValue({ value: values.value, isMappingChecked: values.isMappingChecked, mapping: [ @@ -76,25 +77,37 @@ export const RiskScoreField = ({ ], }); }, - [field] + [setValue, fieldValue] + ); + + const handleRangeFieldChange = useCallback( + (e: React.ChangeEvent | React.MouseEvent): void => { + const range = (e.target as HTMLInputElement).value; + setValue({ + value: range.trim() === '' ? '' : +range, + isMappingChecked: (fieldValue as AboutStepRiskScore).isMappingChecked, + mapping: (fieldValue as AboutStepRiskScore).mapping, + }); + }, + [fieldValue, setValue] ); const selectedField = useMemo(() => { - const existingField = (field.value as AboutStepRiskScore).mapping?.[0]?.field ?? ''; + const existingField = (fieldValue as AboutStepRiskScore).mapping?.[0]?.field ?? ''; const [newSelectedField] = indices.fields.filter( ({ name }) => existingField != null && existingField === name ); return newSelectedField; - }, [field.value, indices]); + }, [fieldValue, indices]); const handleRiskScoreMappingChecked = useCallback(() => { - const values = field.value as AboutStepRiskScore; - field.setValue({ + const values = fieldValue as AboutStepRiskScore; + setValue({ value: values.value, mapping: [...values.mapping], isMappingChecked: !values.isMappingChecked, }); - }, [field]); + }, [fieldValue, setValue]); const riskScoreLabel = useMemo(() => { return ( @@ -119,7 +132,7 @@ export const RiskScoreField = ({ @@ -132,7 +145,7 @@ export const RiskScoreField = ({ ); - }, [field.value, handleRiskScoreMappingChecked, isDisabled]); + }, [fieldValue, handleRiskScoreMappingChecked, isDisabled]); return ( @@ -144,24 +157,20 @@ export const RiskScoreField = ({ error={'errorMessage'} isInvalid={false} fullWidth - data-test-subj={dataTestSubj} - describedByIds={idAria ? [idAria] : undefined} + data-test-subj="detectionEngineStepAboutRuleRiskScore" + describedByIds={['detectionEngineStepAboutRuleRiskScore']} > - @@ -170,7 +179,7 @@ export const RiskScoreField = ({ label={riskScoreMappingLabel} labelAppend={field.labelAppend} helpText={ - (field.value as AboutStepRiskScore).isMappingChecked ? ( + (fieldValue as AboutStepRiskScore).isMappingChecked ? ( {i18n.RISK_SCORE_MAPPING_DETAILS} ) : ( '' @@ -184,7 +193,7 @@ export const RiskScoreField = ({ > - {(field.value as AboutStepRiskScore).isMappingChecked && ( + {(fieldValue as AboutStepRiskScore).isMappingChecked && ( diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx index a9bde76126b6e1..20c3073789b2af 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { RuleActionsField } from './index'; +import { useForm, Form } from '../../../../shared_imports'; import { useKibana } from '../../../../common/lib/kibana'; import { useFormFieldMock } from '../../../../common/mock'; jest.mock('../../../../common/lib/kibana'); @@ -32,8 +33,13 @@ describe('RuleActionsField', () => { }); const Component = () => { const field = useFormFieldMock(); + const { form } = useForm(); - return ; + return ( +
+ + + ); }; const wrapper = shallow(); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx index c6ff25f311d9cb..b9097949bd20a1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx @@ -12,7 +12,7 @@ import ReactMarkdown from 'react-markdown'; import styled from 'styled-components'; import { NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS } from '../../../../../common/constants'; -import { SelectField } from '../../../../shared_imports'; +import { SelectField, useFormContext } from '../../../../shared_imports'; import { ActionForm, ActionType, @@ -37,6 +37,8 @@ const FieldErrorsContainer = styled.div` export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables }) => { const [fieldErrors, setFieldErrors] = useState(null); const [supportedActionTypes, setSupportedActionTypes] = useState(); + const form = useFormContext(); + const { isSubmitted, isSubmitting, isValid } = form; const { http, triggers_actions_ui: { actionTypeRegistry }, @@ -88,26 +90,14 @@ export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables }, []); useEffect(() => { - if (field.form.isSubmitting || !field.errors.length) { + if (isSubmitting || !field.errors.length) { return setFieldErrors(null); } - if ( - field.form.isSubmitted && - !field.form.isSubmitting && - field.form.isValid === false && - field.errors.length - ) { + if (isSubmitted && !isSubmitting && isValid === false && field.errors.length) { const errorsString = field.errors.map(({ message }) => message).join('\n'); return setFieldErrors(errorsString); } - }, [ - field.form.isSubmitted, - field.form.isSubmitting, - field.isChangingValue, - field.form.isValid, - field.errors, - setFieldErrors, - ]); + }, [isSubmitted, isSubmitting, field.isChangingValue, isValid, field.errors, setFieldErrors]); if (!supportedActionTypes) return <>; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx index 733e701cff2047..70e66af25f69e4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx @@ -13,6 +13,7 @@ import { EuiFormLabel, EuiIcon, EuiSpacer, + EuiSuperSelect, } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; @@ -20,7 +21,6 @@ import styled from 'styled-components'; import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { SeverityOptionItem } from '../step_about_rule/data'; -import { CommonUseField } from '../../../../cases/components/create'; import { AboutStepSeverity } from '../../../pages/detection_engine/rules/types'; import { IFieldType, @@ -68,58 +68,61 @@ export const SeverityField = ({ options, }: SeverityFieldProps) => { const fieldValueInputWidth = 160; + const { setValue } = field; + const { value, isMappingChecked, mapping } = field.value as AboutStepSeverity; const handleFieldValueChange = useCallback( (newMappingItems: SeverityMapping, index: number): void => { - const values = field.value as AboutStepSeverity; - field.setValue({ - value: values.value, - isMappingChecked: values.isMappingChecked, - mapping: [ - ...values.mapping.slice(0, index), - ...newMappingItems, - ...values.mapping.slice(index + 1), - ], + setValue({ + value, + isMappingChecked, + mapping: [...mapping.slice(0, index), ...newMappingItems, ...mapping.slice(index + 1)], }); }, - [field] + [value, isMappingChecked, mapping, setValue] ); const handleFieldChange = useCallback( (index: number, severity: Severity, [newField]: IFieldType[]): void => { - const values = field.value as AboutStepSeverity; const newMappingItems: SeverityMapping = [ { - ...values.mapping[index], + ...mapping[index], field: newField?.name ?? '', - value: newField != null ? values.mapping[index].value : '', + value: newField != null ? mapping[index].value : '', operator: 'equals', severity, }, ]; handleFieldValueChange(newMappingItems, index); }, - [field, handleFieldValueChange] + [mapping, handleFieldValueChange] + ); + + const handleSecurityLevelChange = useCallback( + (newValue: string) => { + setValue({ + value: newValue, + isMappingChecked, + mapping, + }); + }, + [isMappingChecked, mapping, setValue] ); const handleFieldMatchValueChange = useCallback( (index: number, severity: Severity, newMatchValue: string): void => { - const values = field.value as AboutStepSeverity; const newMappingItems: SeverityMapping = [ { - ...values.mapping[index], - field: values.mapping[index].field, - value: - values.mapping[index].field != null && values.mapping[index].field !== '' - ? newMatchValue - : '', + ...mapping[index], + field: mapping[index].field, + value: mapping[index].field != null && mapping[index].field !== '' ? newMatchValue : '', operator: 'equals', severity, }, ]; handleFieldValueChange(newMappingItems, index); }, - [field, handleFieldValueChange] + [mapping, handleFieldValueChange] ); const getIFieldTypeFromFieldName = ( @@ -131,13 +134,12 @@ export const SeverityField = ({ }; const handleSeverityMappingChecked = useCallback(() => { - const values = field.value as AboutStepSeverity; - field.setValue({ - value: values.value, - mapping: [...values.mapping], - isMappingChecked: !values.isMappingChecked, + setValue({ + value, + mapping: [...mapping], + isMappingChecked: !isMappingChecked, }); - }, [field]); + }, [isMappingChecked, mapping, value, setValue]); const severityLabel = useMemo(() => { return ( @@ -162,7 +164,7 @@ export const SeverityField = ({ @@ -175,7 +177,7 @@ export const SeverityField = ({
); - }, [field.value, handleSeverityMappingChecked, isDisabled]); + }, [handleSeverityMappingChecked, isDisabled, isMappingChecked]); return ( @@ -187,21 +189,16 @@ export const SeverityField = ({ error={'errorMessage'} isInvalid={false} fullWidth - data-test-subj={dataTestSubj} - describedByIds={idAria ? [idAria] : undefined} + data-test-subj="detectionEngineStepAboutRuleSeverity" + describedByIds={['detectionEngineStepAboutRuleSeverity']} > - @@ -211,11 +208,7 @@ export const SeverityField = ({ label={severityMappingLabel} labelAppend={field.labelAppend} helpText={ - (field.value as AboutStepSeverity).isMappingChecked ? ( - {i18n.SEVERITY_MAPPING_DETAILS} - ) : ( - '' - ) + isMappingChecked ? {i18n.SEVERITY_MAPPING_DETAILS} : '' } error={'errorMessage'} isInvalid={false} @@ -225,7 +218,7 @@ export const SeverityField = ({ > - {(field.value as AboutStepSeverity).isMappingChecked && ( + {isMappingChecked && ( @@ -242,71 +235,69 @@ export const SeverityField = ({ - {(field.value as AboutStepSeverity).mapping.map( - (severityMappingItem: SeverityMappingItem, index) => ( - - - - - + {mapping.map((severityMappingItem: SeverityMappingItem, index) => ( + + + + + - - - - - - - - { - options.find((o) => o.value === severityMappingItem.severity) - ?.inputDisplay - } - - - - ) - )} + + + + + + + + { + options.find((o) => o.value === severityMappingItem.severity) + ?.inputDisplay + } + + + + ))} )} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx index cb3fd5e5bec32f..0c834b9fff33af 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { mount, shallow } from 'enzyme'; import { ThemeProvider } from 'styled-components'; import euiDarkVars from '@elastic/eui/dist/eui_theme_light.json'; @@ -223,32 +224,33 @@ describe('StepAboutRuleComponent', () => { .first() .simulate('change', { target: { value: '80' } }); - wrapper.find('[data-test-subj="about-continue"]').first().simulate('click').update(); - await waitFor(() => { - const expected: Omit = { - author: [], - isAssociatedToEndpointList: false, - isBuildingBlock: false, - license: '', - ruleNameOverride: '', - timestampOverride: '', - description: 'Test description text', - falsePositives: [''], - name: 'Test name text', - note: '', - references: [''], - riskScore: { value: 80, mapping: [], isMappingChecked: false }, - severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false }, - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { id: 'none', name: 'none', reference: 'none' }, - technique: [], - }, - ], - }; - expect(stepDataMock.mock.calls[1][1]).toEqual(expected); + await act(async () => { + wrapper.find('[data-test-subj="about-continue"]').first().simulate('click').update(); }); + + const expected: Omit = { + author: [], + isAssociatedToEndpointList: false, + isBuildingBlock: false, + license: '', + ruleNameOverride: '', + timestampOverride: '', + description: 'Test description text', + falsePositives: [''], + name: 'Test name text', + note: '', + references: [''], + riskScore: { value: 80, mapping: [], isMappingChecked: false }, + severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false }, + tags: [], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { id: 'none', name: 'none', reference: 'none' }, + technique: [], + }, + ], + }; + expect(stepDataMock.mock.calls[1][1]).toEqual(expected); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx index a3db8fe659d848..2264a11341eb85 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx @@ -102,29 +102,12 @@ export const schema: FormSchema = { labelAppend: OptionalFieldLabel, }, severity: { - value: { - type: FIELD_TYPES.SUPER_SELECT, - validations: [ - { - validator: emptyField( - i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.severityFieldRequiredError', - { - defaultMessage: 'A severity is required.', - } - ) - ), - }, - ], - }, + value: {}, mapping: {}, isMappingChecked: {}, }, riskScore: { - value: { - type: FIELD_TYPES.RANGE, - serializer: (input: string) => Number(input), - }, + value: {}, mapping: {}, isMappingChecked: {}, }, diff --git a/x-pack/plugins/security_solution/public/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts index b2c7319b94576e..097166a9c866a9 100644 --- a/x-pack/plugins/security_solution/public/shared_imports.ts +++ b/x-pack/plugins/security_solution/public/shared_imports.ts @@ -20,6 +20,7 @@ export { UseField, UseMultiFields, useForm, + useFormContext, ValidationFunc, VALIDATION_TYPES, } from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d6e611e65154b7..c99980fe6205cb 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15631,7 +15631,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideLabel": "調査ガイド", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "名前が必要です。", "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "ルール調査ガイドを追加...", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.severityFieldRequiredError": "深刻度が必要です。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addFalsePositiveDescription": "誤検出の例を追加します", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addReferenceDescription": "参照URLを追加します", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.advancedSettingsButton": "高度な設定", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 54c69d849e3a9f..9ffa81a921ba81 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15637,7 +15637,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideLabel": "调查指南", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "名称必填。", "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "添加规则调查指南......", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.severityFieldRequiredError": "严重性必填。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addFalsePositiveDescription": "添加误报示例", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addReferenceDescription": "添加引用 URL", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.advancedSettingsButton": "高级设置", From b802af800268bfa8a008d129b99dfd496b87b276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Thu, 27 Aug 2020 14:15:59 +0200 Subject: [PATCH 062/216] [ILM] Fix json in request flyout (#75971) Co-authored-by: Elastic Machine --- .../__jest__/components/edit_policy.test.js | 28 ++++++++++++++++ .../components/policy_json_flyout.tsx | 32 +++++++++++-------- .../sections/edit_policy/edit_policy.tsx | 3 +- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js index 81c30579cd4dde..e4227bac520fef 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js @@ -187,6 +187,34 @@ describe('edit policy', () => { save(rendered); expectedErrorMessages(rendered, [policyNameStartsWithUnderscoreErrorMessage]); }); + test('should show correct json in policy flyout', () => { + const rendered = mountWithIntl(component); + findTestSubject(rendered, 'requestButton').simulate('click'); + const json = rendered.find(`code`).text(); + const expected = `PUT _ilm/policy/\n${JSON.stringify( + { + policy: { + phases: { + hot: { + min_age: '0ms', + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + set_priority: { + priority: 100, + }, + }, + }, + }, + }, + }, + null, + 2 + )}`; + expect(json).toBe(expected); + }); }); describe('hot phase', () => { test('should show errors when trying to save with no max size and no max age', () => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx index 66cb4ad9fba32f..2f246f21aaf2ef 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx @@ -18,29 +18,35 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { Policy } from '../../../services/policies/types'; +import { Policy, PolicyFromES } from '../../../services/policies/types'; +import { serializePolicy } from '../../../services/policies/policy_serialization'; interface Props { close: () => void; policy: Policy; + existingPolicy?: PolicyFromES; policyName: string; } -export const PolicyJsonFlyout: React.FunctionComponent = ({ close, policy, policyName }) => { - const getEsJson = ({ phases }: Policy) => { - return JSON.stringify( - { - policy: { - phases, - }, +export const PolicyJsonFlyout: React.FunctionComponent = ({ + close, + policy, + policyName, + existingPolicy, +}) => { + const { phases } = serializePolicy(policy, existingPolicy?.policy); + const json = JSON.stringify( + { + policy: { + phases, }, - null, - 2 - ); - }; + }, + null, + 2 + ); const endpoint = `PUT _ilm/policy/${policyName || ''}`; - const request = `${endpoint}\n${getEsJson(policy)}`; + const request = `${endpoint}\n${json}`; return ( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index 6cffde577b35ee..c99d01b5466792 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -352,7 +352,7 @@ export const EditPolicy: React.FunctionComponent = ({ - + {isShowingPolicyJsonFlyout ? ( = ({ {isShowingPolicyJsonFlyout ? ( setIsShowingPolicyJsonFlyout(false)} /> From f065191a750639179a63b77df0cc95261c920812 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Thu, 27 Aug 2020 08:44:41 -0400 Subject: [PATCH 063/216] [Enterprise Search] Added an App Search route for listing Credentials (#75487) In addition to a route for listing Credentials, this also adds a utility function which helps create API routes which simply proxy the App Search API. The reasoning for this is as follows; 1. Creating new routes takes less effort and cognitive load if we can simply just create proxy routes that use the APIs as is. 2. It keeps the App Search API as the source of truth. All logic is implemented in the underlying API. 3. It makes unit testing routes much simpler. We do not need to verify any connectivity to the underlying App Search API, because that is already tested as part of the utility. --- .../server/{routes => }/__mocks__/index.ts | 0 .../{routes => }/__mocks__/router.mock.ts | 0 .../__mocks__/routerDependencies.mock.ts | 2 +- .../collectors/app_search/telemetry.test.ts | 2 +- .../server/collectors/lib/telemetry.test.ts | 2 +- .../workplace_search/telemetry.test.ts | 2 +- .../enterprise_search_request_handler.test.ts | 133 ++++++++++++++++++ .../lib/enterprise_search_request_handler.ts | 69 +++++++++ .../enterprise_search/server/plugin.ts | 2 + .../routes/app_search/credentials.test.ts | 93 ++++++++++++ .../server/routes/app_search/credentials.ts | 50 +++++++ .../server/routes/app_search/engines.test.ts | 2 +- .../enterprise_search/config_data.test.ts | 2 +- .../enterprise_search/telemetry.test.ts | 2 +- .../routes/workplace_search/overview.test.ts | 2 +- 15 files changed, 355 insertions(+), 8 deletions(-) rename x-pack/plugins/enterprise_search/server/{routes => }/__mocks__/index.ts (100%) rename x-pack/plugins/enterprise_search/server/{routes => }/__mocks__/router.mock.ts (100%) rename x-pack/plugins/enterprise_search/server/{routes => }/__mocks__/routerDependencies.mock.ts (95%) create mode 100644 x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts diff --git a/x-pack/plugins/enterprise_search/server/routes/__mocks__/index.ts b/x-pack/plugins/enterprise_search/server/__mocks__/index.ts similarity index 100% rename from x-pack/plugins/enterprise_search/server/routes/__mocks__/index.ts rename to x-pack/plugins/enterprise_search/server/__mocks__/index.ts diff --git a/x-pack/plugins/enterprise_search/server/routes/__mocks__/router.mock.ts b/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts similarity index 100% rename from x-pack/plugins/enterprise_search/server/routes/__mocks__/router.mock.ts rename to x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts diff --git a/x-pack/plugins/enterprise_search/server/routes/__mocks__/routerDependencies.mock.ts b/x-pack/plugins/enterprise_search/server/__mocks__/routerDependencies.mock.ts similarity index 95% rename from x-pack/plugins/enterprise_search/server/routes/__mocks__/routerDependencies.mock.ts rename to x-pack/plugins/enterprise_search/server/__mocks__/routerDependencies.mock.ts index 9b6fa30271d613..7a244be96cfc45 100644 --- a/x-pack/plugins/enterprise_search/server/routes/__mocks__/routerDependencies.mock.ts +++ b/x-pack/plugins/enterprise_search/server/__mocks__/routerDependencies.mock.ts @@ -5,7 +5,7 @@ */ import { loggingSystemMock } from 'src/core/server/mocks'; -import { ConfigType } from '../../'; +import { ConfigType } from '../'; export const mockLogger = loggingSystemMock.createLogger().get(); diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts index 53c6dee61cd1dc..189f8278f1b070 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mockLogger } from '../../routes/__mocks__'; +import { mockLogger } from '../../__mocks__'; import { registerTelemetryUsageCollector } from './telemetry'; diff --git a/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts index 3ab3b03dd77252..aae162c23ccb42 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mockLogger } from '../../routes/__mocks__'; +import { mockLogger } from '../../__mocks__'; jest.mock('../../../../../../src/core/server', () => ({ SavedObjectsErrorHelpers: { diff --git a/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.test.ts index 496b2f254f9a6e..8960d6fa9b67b4 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mockLogger } from '../../routes/__mocks__'; +import { mockLogger } from '../../__mocks__'; import { registerTelemetryUsageCollector } from './telemetry'; diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts new file mode 100644 index 00000000000000..f0c003936996e9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts @@ -0,0 +1,133 @@ +/* + * 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 { mockConfig, mockLogger } from '../__mocks__'; + +import { createEnterpriseSearchRequestHandler } from './enterprise_search_request_handler'; + +jest.mock('node-fetch'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const fetchMock = require('node-fetch') as jest.Mock; +const { Response } = jest.requireActual('node-fetch'); + +const responseMock = { + ok: jest.fn(), + customError: jest.fn(), +}; +const KibanaAuthHeader = 'Basic 123'; + +describe('createEnterpriseSearchRequestHandler', () => { + beforeEach(() => { + jest.clearAllMocks(); + fetchMock.mockReset(); + }); + + it('makes an API call and returns the response', async () => { + const responseBody = { + results: [{ name: 'engine1' }], + meta: { page: { total_results: 1 } }, + }; + + EnterpriseSearchAPI.mockReturn(responseBody); + + const requestHandler = createEnterpriseSearchRequestHandler({ + config: mockConfig, + log: mockLogger, + path: '/as/credentials/collection', + }); + + await makeAPICall(requestHandler, { + query: { + type: 'indexed', + pageIndex: 1, + }, + }); + + EnterpriseSearchAPI.shouldHaveBeenCalledWith( + 'http://localhost:3002/as/credentials/collection?type=indexed&pageIndex=1' + ); + + expect(responseMock.ok).toHaveBeenCalledWith({ + body: responseBody, + }); + }); + + describe('when an API request fails', () => { + it('should return 502 with a message', async () => { + EnterpriseSearchAPI.mockReturnError(); + + const requestHandler = createEnterpriseSearchRequestHandler({ + config: mockConfig, + log: mockLogger, + path: '/as/credentials/collection', + }); + + await makeAPICall(requestHandler); + + EnterpriseSearchAPI.shouldHaveBeenCalledWith( + 'http://localhost:3002/as/credentials/collection' + ); + + expect(responseMock.customError).toHaveBeenCalledWith({ + body: 'Error connecting or fetching data from Enterprise Search', + statusCode: 502, + }); + }); + }); + + describe('when `hasValidData` fails', () => { + it('should return 502 with a message', async () => { + const responseBody = { + foo: 'bar', + }; + + EnterpriseSearchAPI.mockReturn(responseBody); + + const requestHandler = createEnterpriseSearchRequestHandler({ + config: mockConfig, + log: mockLogger, + path: '/as/credentials/collection', + hasValidData: (body?: any) => + Array.isArray(body?.results) && typeof body?.meta?.page?.total_results === 'number', + }); + + await makeAPICall(requestHandler); + + EnterpriseSearchAPI.shouldHaveBeenCalledWith( + 'http://localhost:3002/as/credentials/collection' + ); + + expect(responseMock.customError).toHaveBeenCalledWith({ + body: 'Error connecting or fetching data from Enterprise Search', + statusCode: 502, + }); + }); + }); +}); + +const makeAPICall = (handler: Function, params = {}) => { + const request = { headers: { authorization: KibanaAuthHeader }, ...params }; + return handler(null, request, responseMock); +}; + +const EnterpriseSearchAPI = { + shouldHaveBeenCalledWith(expectedUrl: string, expectedParams = {}) { + expect(fetchMock).toHaveBeenCalledWith(expectedUrl, { + headers: { Authorization: KibanaAuthHeader }, + ...expectedParams, + }); + }, + mockReturn(response: object) { + fetchMock.mockImplementation(() => { + return Promise.resolve(new Response(JSON.stringify(response))); + }); + }, + mockReturnError() { + fetchMock.mockImplementation(() => { + return Promise.reject('Failed'); + }); + }, +}; diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts new file mode 100644 index 00000000000000..11152aa651743e --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts @@ -0,0 +1,69 @@ +/* + * 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 fetch from 'node-fetch'; +import querystring from 'querystring'; +import { + RequestHandlerContext, + KibanaRequest, + KibanaResponseFactory, + Logger, +} from 'src/core/server'; +import { ConfigType } from '../index'; + +interface IEnterpriseSearchRequestParams { + config: ConfigType; + log: Logger; + path: string; + hasValidData?: (body?: ResponseBody) => boolean; +} + +/** + * This helper function creates a single standard DRY way of handling + * Enterprise Search API requests. + * + * This handler assumes that it will essentially just proxy the + * Enterprise Search API request, so the request body and request + * parameters are simply passed through. + */ +export function createEnterpriseSearchRequestHandler({ + config, + log, + path, + hasValidData = () => true, +}: IEnterpriseSearchRequestParams) { + return async ( + _context: RequestHandlerContext, + request: KibanaRequest, unknown>, + response: KibanaResponseFactory + ) => { + try { + const enterpriseSearchUrl = config.host as string; + const params = request.query ? `?${querystring.stringify(request.query)}` : ''; + const url = `${encodeURI(enterpriseSearchUrl)}${path}${params}`; + + const apiResponse = await fetch(url, { + headers: { Authorization: request.headers.authorization as string }, + }); + + const body = await apiResponse.json(); + + if (hasValidData(body)) { + return response.ok({ body }); + } else { + throw new Error(`Invalid data received: ${JSON.stringify(body)}`); + } + } catch (e) { + log.error(`Cannot connect to Enterprise Search: ${e.toString()}`); + if (e instanceof Error) log.debug(e.stack as string); + + return response.customError({ + statusCode: 502, + body: 'Error connecting or fetching data from Enterprise Search', + }); + } + }; +} diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index a0d3a57eabb7a0..ef8c72f0cbca53 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -32,6 +32,7 @@ import { registerTelemetryRoute } from './routes/enterprise_search/telemetry'; import { appSearchTelemetryType } from './saved_objects/app_search/telemetry'; import { registerTelemetryUsageCollector as registerASTelemetryUsageCollector } from './collectors/app_search/telemetry'; import { registerEnginesRoute } from './routes/app_search/engines'; +import { registerCredentialsRoutes } from './routes/app_search/credentials'; import { workplaceSearchTelemetryType } from './saved_objects/workplace_search/telemetry'; import { registerTelemetryUsageCollector as registerWSTelemetryUsageCollector } from './collectors/workplace_search/telemetry'; @@ -108,6 +109,7 @@ export class EnterpriseSearchPlugin implements Plugin { registerConfigDataRoute(dependencies); registerEnginesRoute(dependencies); + registerCredentialsRoutes(dependencies); registerWSOverviewRoute(dependencies); /** diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts new file mode 100644 index 00000000000000..682c17aea6d52a --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts @@ -0,0 +1,93 @@ +/* + * 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 { MockRouter, mockConfig, mockLogger } from '../../__mocks__'; + +import { registerCredentialsRoutes } from './credentials'; + +jest.mock('../../lib/enterprise_search_request_handler', () => ({ + createEnterpriseSearchRequestHandler: jest.fn(), +})); +import { createEnterpriseSearchRequestHandler } from '../../lib/enterprise_search_request_handler'; + +describe('credentials routes', () => { + describe('GET /api/app_search/credentials', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ method: 'get', payload: 'query' }); + + registerCredentialsRoutes({ + router: mockRouter.router, + log: mockLogger, + config: mockConfig, + }); + }); + + it('creates a handler with createEnterpriseSearchRequestHandler', () => { + expect(createEnterpriseSearchRequestHandler).toHaveBeenCalledWith({ + config: mockConfig, + log: mockLogger, + path: '/as/credentials/collection', + hasValidData: expect.any(Function), + }); + }); + + describe('hasValidData', () => { + it('should correctly validate that a response has data', () => { + const response = { + meta: { + page: { + current: 1, + total_pages: 1, + total_results: 1, + size: 25, + }, + }, + results: [ + { + id: 'loco_moco_account_id:5f3575de2b76ff13405f3155|name:asdfasdf', + key: 'search-fe49u2z8d5gvf9s4ekda2ad4', + name: 'asdfasdf', + type: 'search', + access_all_engines: true, + }, + ], + }; + + const { + hasValidData, + } = (createEnterpriseSearchRequestHandler as jest.Mock).mock.calls[0][0]; + + expect(hasValidData(response)).toBe(true); + }); + + it('should correctly validate that a response does not have data', () => { + const response = { + foo: 'bar', + }; + + const hasValidData = (createEnterpriseSearchRequestHandler as jest.Mock).mock.calls[0][0] + .hasValidData; + + expect(hasValidData(response)).toBe(false); + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { query: { 'page[current]': 1 } }; + mockRouter.shouldValidate(request); + }); + + it('missing page[current]', () => { + const request = { query: {} }; + mockRouter.shouldThrow(request); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts new file mode 100644 index 00000000000000..d9539692069f0d --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.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 { schema } from '@kbn/config-schema'; + +import { IRouteDependencies } from '../../plugin'; +import { createEnterpriseSearchRequestHandler } from '../../lib/enterprise_search_request_handler'; + +interface ICredential { + id: string; + key: string; + name: string; + type: string; + access_all_engines: boolean; +} +interface ICredentialsResponse { + results: ICredential[]; + meta?: { + page?: { + current: number; + total_results: number; + total_pages: number; + size: number; + }; + }; +} + +export function registerCredentialsRoutes({ router, config, log }: IRouteDependencies) { + router.get( + { + path: '/api/app_search/credentials', + validate: { + query: schema.object({ + 'page[current]': schema.number(), + }), + }, + }, + createEnterpriseSearchRequestHandler({ + config, + log, + path: '/as/credentials/collection', + hasValidData: (body?: ICredentialsResponse) => { + return Array.isArray(body?.results) && typeof body?.meta?.page?.total_results === 'number'; + }, + }) + ); +} diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts index 1ea023ecacdbe3..03edab89d1b991 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MockRouter, mockConfig, mockLogger } from '../__mocks__'; +import { MockRouter, mockConfig, mockLogger } from '../../__mocks__'; import { registerEnginesRoute } from './engines'; diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.test.ts index 7484e27594df4c..253c9a418d60b6 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.test.ts @@ -5,7 +5,7 @@ */ import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; -import { MockRouter, mockDependencies } from '../__mocks__'; +import { MockRouter, mockDependencies } from '../../__mocks__'; jest.mock('../../lib/enterprise_search_config_api', () => ({ callEnterpriseSearchConfigAPI: jest.fn(), diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts index ebd84d3e0e79ab..daf0a1e895a61a 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts @@ -5,7 +5,7 @@ */ import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; -import { MockRouter, mockConfig, mockLogger } from '../__mocks__'; +import { MockRouter, mockConfig, mockLogger } from '../../__mocks__'; jest.mock('../../collectors/lib/telemetry', () => ({ incrementUICounter: jest.fn(), diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts index f6534b27b5da05..69e8354e8b2f70 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MockRouter, mockConfig, mockLogger } from '../__mocks__'; +import { MockRouter, mockConfig, mockLogger } from '../../__mocks__'; import { registerWSOverviewRoute } from './overview'; From d457d530017e086064ed174fc74f9a8d738bd6bb Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 27 Aug 2020 09:02:32 -0400 Subject: [PATCH 064/216] [Uptime] Translate bare strings (#75918) * Translate a bare string. * Remove unneeded translation. --- .../plugins/uptime/public/state/effects/dynamic_settings.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts b/x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts index 4b41862649b55d..57be818c928dce 100644 --- a/x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts +++ b/x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts @@ -47,7 +47,11 @@ export function* setDynamicSettingsEffect() { } yield call(setDynamicSettingsAPI, { settings: action.payload }); yield put(setDynamicSettingsSuccess(action.payload)); - kibanaService.core.notifications.toasts.addSuccess('Settings saved!'); + kibanaService.core.notifications.toasts.addSuccess( + i18n.translate('xpack.uptime.settings.saveSuccess', { + defaultMessage: 'Settings saved!', + }) + ); } catch (err) { kibanaService.core.notifications.toasts.addError(err, { title: couldNotSaveSettingsText, From 54bbd6a91097246552c17847864edde6b4b61915 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 27 Aug 2020 10:09:20 -0400 Subject: [PATCH 065/216] Adds `authenticaton_type` as expected property on ES authentication response (#75808) Co-authored-by: Elastic Machine --- .../common/model/authenticated_user.mock.ts | 1 + .../common/model/authenticated_user.ts | 7 + .../apis/security/basic_login.js | 17 +- .../apis/security/kerberos_login.ts | 14 +- .../apis/login_selector.ts | 182 ++++++++++++++++-- .../apis/authorization_code_flow/oidc_auth.ts | 12 +- .../apis/implicit_flow/oidc_auth.ts | 7 +- .../apis/security/pki_auth.ts | 5 +- .../apis/security/saml_login.ts | 8 +- 9 files changed, 221 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/security/common/model/authenticated_user.mock.ts b/x-pack/plugins/security/common/model/authenticated_user.mock.ts index f8b0d27efcbf4b..0393c94da8d406 100644 --- a/x-pack/plugins/security/common/model/authenticated_user.mock.ts +++ b/x-pack/plugins/security/common/model/authenticated_user.mock.ts @@ -16,6 +16,7 @@ export function mockAuthenticatedUser(user: Partial = {}) { authentication_realm: { name: 'native1', type: 'native' }, lookup_realm: { name: 'native1', type: 'native' }, authentication_provider: 'basic1', + authentication_type: 'realm', ...user, }; } diff --git a/x-pack/plugins/security/common/model/authenticated_user.ts b/x-pack/plugins/security/common/model/authenticated_user.ts index 6465b4a23eb48a..5ea420af202dc2 100644 --- a/x-pack/plugins/security/common/model/authenticated_user.ts +++ b/x-pack/plugins/security/common/model/authenticated_user.ts @@ -31,6 +31,13 @@ export interface AuthenticatedUser extends User { * Name of the Kibana authentication provider that used to authenticate user. */ authentication_provider: string; + + /** + * The AuthenticationType used by ES to authenticate the user. + * + * @example "realm" | "api_key" | "token" | "anonymous" | "internal" + */ + authentication_type: string; } export function canUserChangePassword(user: AuthenticatedUser) { diff --git a/x-pack/test/api_integration/apis/security/basic_login.js b/x-pack/test/api_integration/apis/security/basic_login.js index db58f17582c60d..4b39b1bf32d5b4 100644 --- a/x-pack/test/api_integration/apis/security/basic_login.js +++ b/x-pack/test/api_integration/apis/security/basic_login.js @@ -15,8 +15,7 @@ export default function ({ getService }) { const validUsername = kibanaServerConfig.username; const validPassword = kibanaServerConfig.password; - // Failing: See https://github.com/elastic/kibana/issues/75707 - describe.skip('Basic authentication', () => { + describe('Basic authentication', () => { it('should redirect non-AJAX requests to the login page if not authenticated', async () => { const response = await supertest.get('/abc/xyz').expect(302); @@ -145,8 +144,15 @@ export default function ({ getService }) { 'authentication_realm', 'lookup_realm', 'authentication_provider', + 'authentication_type', ]); expect(apiResponse.body.username).to.be(validUsername); + expect(apiResponse.body.authentication_provider).to.eql('__http__'); + expect(apiResponse.body.authentication_realm).to.eql({ + name: 'reserved', + type: 'reserved', + }); + expect(apiResponse.body.authentication_type).to.be('realm'); }); describe('with session cookie', () => { @@ -187,8 +193,15 @@ export default function ({ getService }) { 'authentication_realm', 'lookup_realm', 'authentication_provider', + 'authentication_type', ]); expect(apiResponse.body.username).to.be(validUsername); + expect(apiResponse.body.authentication_provider).to.eql('basic'); + expect(apiResponse.body.authentication_realm).to.eql({ + name: 'reserved', + type: 'reserved', + }); + expect(apiResponse.body.authentication_type).to.be('realm'); }); it('should extend cookie on every successful non-system API call', async () => { diff --git a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts index 3c211dca2a783c..1f4428e1985390 100644 --- a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts +++ b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts @@ -37,8 +37,7 @@ export default function ({ getService }: FtrProviderContext) { expect(cookie.maxAge).to.be(0); } - // FAILING: https://github.com/elastic/kibana/issues/75707 - describe.skip('Kerberos authentication', () => { + describe('Kerberos authentication', () => { before(async () => { await getService('esSupertest') .post('/_security/role_mapping/krb5') @@ -82,6 +81,7 @@ export default function ({ getService }: FtrProviderContext) { expect(user.username).to.eql(username); expect(user.authentication_realm).to.eql({ name: 'reserved', type: 'reserved' }); expect(user.authentication_provider).to.eql('basic'); + expect(user.authentication_type).to.eql('realm'); }); describe('initiating SPNEGO', () => { @@ -121,7 +121,14 @@ export default function ({ getService }: FtrProviderContext) { const sessionCookie = request.cookie(cookies[0])!; checkCookieIsSet(sessionCookie); - const expectedUserRoles = ['kibana_admin']; + const isAnonymousAccessEnabled = (config.get( + 'esTestCluster.serverArgs' + ) as string[]).some((setting) => setting.startsWith('xpack.security.authc.anonymous')); + + // `superuser_anonymous` role is derived from the enabled anonymous access. + const expectedUserRoles = isAnonymousAccessEnabled + ? ['kibana_admin', 'superuser_anonymous'] + : ['kibana_admin']; await supertest .get('/internal/security/me') @@ -140,6 +147,7 @@ export default function ({ getService }: FtrProviderContext) { authentication_realm: { name: 'kerb1', type: 'kerberos' }, lookup_realm: { name: 'kerb1', type: 'kerberos' }, authentication_provider: 'kerberos', + authentication_type: 'token', }); }); diff --git a/x-pack/test/login_selector_api_integration/apis/login_selector.ts b/x-pack/test/login_selector_api_integration/apis/login_selector.ts index 63084d3bfc9e96..7eb1f07d67506d 100644 --- a/x-pack/test/login_selector_api_integration/apis/login_selector.ts +++ b/x-pack/test/login_selector_api_integration/apis/login_selector.ts @@ -32,7 +32,13 @@ export default function ({ getService }: FtrProviderContext) { resolve(__dirname, '../../pki_api_integration/fixtures/first_client.p12') ); - async function checkSessionCookie(sessionCookie: Cookie, username: string, providerName: string) { + async function checkSessionCookie( + sessionCookie: Cookie, + username: string, + providerName: string, + authenticationRealm: { name: string; type: string }, + authenticationType: string + ) { expect(sessionCookie.key).to.be('sid'); expect(sessionCookie.value).to.not.be.empty(); expect(sessionCookie.path).to.be('/'); @@ -56,14 +62,16 @@ export default function ({ getService }: FtrProviderContext) { 'authentication_realm', 'lookup_realm', 'authentication_provider', + 'authentication_type', ]); expect(apiResponse.body.username).to.be(username); expect(apiResponse.body.authentication_provider).to.be(providerName); + expect(apiResponse.body.authentication_realm).to.eql(authenticationRealm); + expect(apiResponse.body.authentication_type).to.be(authenticationType); } - // FAILING: https://github.com/elastic/kibana/issues/75707 - describe.skip('Login Selector', () => { + describe('Login Selector', () => { it('should redirect user to a login selector', async () => { const response = await supertest .get('/abc/xyz/handshake?one=two three') @@ -121,7 +129,16 @@ export default function ({ getService }: FtrProviderContext) { const cookies = authenticationResponse.headers['set-cookie']; expect(cookies).to.have.length(1); - await checkSessionCookie(request.cookie(cookies[0])!, 'a@b.c', providerName); + await checkSessionCookie( + request.cookie(cookies[0])!, + 'a@b.c', + providerName, + { + name: providerName, + type: 'saml', + }, + 'token' + ); } }); @@ -148,7 +165,16 @@ export default function ({ getService }: FtrProviderContext) { const cookies = authenticationResponse.headers['set-cookie']; expect(cookies).to.have.length(1); - await checkSessionCookie(request.cookie(cookies[0])!, 'a@b.c', providerName); + await checkSessionCookie( + request.cookie(cookies[0])!, + 'a@b.c', + providerName, + { + name: providerName, + type: 'saml', + }, + 'token' + ); } }); @@ -172,7 +198,16 @@ export default function ({ getService }: FtrProviderContext) { const cookies = authenticationResponse.headers['set-cookie']; expect(cookies).to.have.length(1); - await checkSessionCookie(request.cookie(cookies[0])!, 'a@b.c', providerName); + await checkSessionCookie( + request.cookie(cookies[0])!, + 'a@b.c', + providerName, + { + name: providerName, + type: 'saml', + }, + 'token' + ); } }); @@ -193,7 +228,16 @@ export default function ({ getService }: FtrProviderContext) { const basicSessionCookie = request.cookie( basicAuthenticationResponse.headers['set-cookie'][0] )!; - await checkSessionCookie(basicSessionCookie, 'elastic', 'basic1'); + await checkSessionCookie( + basicSessionCookie, + 'elastic', + 'basic1', + { + name: 'reserved', + type: 'reserved', + }, + 'realm' + ); const authenticationResponse = await supertest .post('/api/security/saml/callback') @@ -213,7 +257,16 @@ export default function ({ getService }: FtrProviderContext) { const cookies = authenticationResponse.headers['set-cookie']; expect(cookies).to.have.length(1); - await checkSessionCookie(request.cookie(cookies[0])!, 'a@b.c', providerName); + await checkSessionCookie( + request.cookie(cookies[0])!, + 'a@b.c', + providerName, + { + name: providerName, + type: 'saml', + }, + 'token' + ); } }); @@ -230,7 +283,16 @@ export default function ({ getService }: FtrProviderContext) { const saml1SessionCookie = request.cookie( saml1AuthenticationResponse.headers['set-cookie'][0] )!; - await checkSessionCookie(saml1SessionCookie, 'a@b.c', 'saml1'); + await checkSessionCookie( + saml1SessionCookie, + 'a@b.c', + 'saml1', + { + name: 'saml1', + type: 'saml', + }, + 'token' + ); // And now try to login with `saml2`. const saml2AuthenticationResponse = await supertest @@ -249,7 +311,16 @@ export default function ({ getService }: FtrProviderContext) { const saml2SessionCookie = request.cookie( saml2AuthenticationResponse.headers['set-cookie'][0] )!; - await checkSessionCookie(saml2SessionCookie, 'a@b.c', 'saml2'); + await checkSessionCookie( + saml2SessionCookie, + 'a@b.c', + 'saml2', + { + name: 'saml2', + type: 'saml', + }, + 'token' + ); }); it('should redirect to URL from relay state in case of IdP initiated login even if session with other SAML provider exists', async () => { @@ -265,7 +336,16 @@ export default function ({ getService }: FtrProviderContext) { const saml1SessionCookie = request.cookie( saml1AuthenticationResponse.headers['set-cookie'][0] )!; - await checkSessionCookie(saml1SessionCookie, 'a@b.c', 'saml1'); + await checkSessionCookie( + saml1SessionCookie, + 'a@b.c', + 'saml1', + { + name: 'saml1', + type: 'saml', + }, + 'token' + ); // And now try to login with `saml2`. const saml2AuthenticationResponse = await supertest @@ -286,7 +366,16 @@ export default function ({ getService }: FtrProviderContext) { const saml2SessionCookie = request.cookie( saml2AuthenticationResponse.headers['set-cookie'][0] )!; - await checkSessionCookie(saml2SessionCookie, 'a@b.c', 'saml2'); + await checkSessionCookie( + saml2SessionCookie, + 'a@b.c', + 'saml2', + { + name: 'saml2', + type: 'saml', + }, + 'token' + ); }); // Ideally we should be able to abandon intermediate session and let user log in, but for the @@ -367,7 +456,16 @@ export default function ({ getService }: FtrProviderContext) { const cookies = authenticationResponse.headers['set-cookie']; expect(cookies).to.have.length(1); - await checkSessionCookie(request.cookie(cookies[0])!, 'a@b.c', providerName); + await checkSessionCookie( + request.cookie(cookies[0])!, + 'a@b.c', + providerName, + { + name: providerName, + type: 'saml', + }, + 'token' + ); } }); @@ -429,7 +527,16 @@ export default function ({ getService }: FtrProviderContext) { const saml2SessionCookie = request.cookie( saml2AuthenticationResponse.headers['set-cookie'][0] )!; - await checkSessionCookie(saml2SessionCookie, 'a@b.c', 'saml2'); + await checkSessionCookie( + saml2SessionCookie, + 'a@b.c', + 'saml2', + { + name: 'saml2', + type: 'saml', + }, + 'token' + ); }); }); @@ -472,7 +579,12 @@ export default function ({ getService }: FtrProviderContext) { await checkSessionCookie( request.cookie(cookies[0])!, 'tester@TEST.ELASTIC.CO', - 'kerberos1' + 'kerberos1', + { + name: 'kerb1', + type: 'kerberos', + }, + 'token' ); }); @@ -516,7 +628,12 @@ export default function ({ getService }: FtrProviderContext) { await checkSessionCookie( request.cookie(cookies[0])!, 'tester@TEST.ELASTIC.CO', - 'kerberos1' + 'kerberos1', + { + name: 'kerb1', + type: 'kerberos', + }, + 'token' ); }); }); @@ -550,7 +667,16 @@ export default function ({ getService }: FtrProviderContext) { const cookies = authenticationResponse.headers['set-cookie']; expect(cookies).to.have.length(1); - await checkSessionCookie(request.cookie(cookies[0])!, 'user2', 'oidc1'); + await checkSessionCookie( + request.cookie(cookies[0])!, + 'user2', + 'oidc1', + { + name: 'oidc1', + type: 'oidc', + }, + 'token' + ); }); it('should be able to log in via SP initiated login', async () => { @@ -601,7 +727,16 @@ export default function ({ getService }: FtrProviderContext) { const cookies = authenticationResponse.headers['set-cookie']; expect(cookies).to.have.length(1); - await checkSessionCookie(request.cookie(cookies[0])!, 'user1', 'oidc1'); + await checkSessionCookie( + request.cookie(cookies[0])!, + 'user1', + 'oidc1', + { + name: 'oidc1', + type: 'oidc', + }, + 'token' + ); }); }); @@ -634,7 +769,16 @@ export default function ({ getService }: FtrProviderContext) { const cookies = authenticationResponse.headers['set-cookie']; expect(cookies).to.have.length(1); - await checkSessionCookie(request.cookie(cookies[0])!, 'first_client', 'pki1'); + await checkSessionCookie( + request.cookie(cookies[0])!, + 'first_client', + 'pki1', + { + name: 'pki1', + type: 'pki', + }, + 'token' + ); }); }); }); diff --git a/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.ts b/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.ts index 1b37d60436ddcc..0a230ac84d9917 100644 --- a/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.ts +++ b/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.ts @@ -15,8 +15,7 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const config = getService('config'); - // FAILING: https://github.com/elastic/kibana/issues/75707 - describe.skip('OpenID Connect authentication', () => { + describe('OpenID Connect authentication', () => { it('should reject API requests if client is not authenticated', async () => { await supertest.get('/internal/security/me').set('kbn-xsrf', 'xxx').expect(401); }); @@ -46,6 +45,7 @@ export default function ({ getService }: FtrProviderContext) { expect(user.username).to.eql(username); expect(user.authentication_realm).to.eql({ name: 'reserved', type: 'reserved' }); expect(user.authentication_provider).to.eql('basic'); + expect(user.authentication_type).to.be('realm'); }); describe('initiating handshake', () => { @@ -230,9 +230,13 @@ export default function ({ getService }: FtrProviderContext) { 'authentication_realm', 'lookup_realm', 'authentication_provider', + 'authentication_type', ]); expect(apiResponse.body.username).to.be('user1'); + expect(apiResponse.body.authentication_realm).to.eql({ name: 'oidc1', type: 'oidc' }); + expect(apiResponse.body.authentication_provider).to.eql('oidc'); + expect(apiResponse.body.authentication_type).to.be('token'); }); }); @@ -280,9 +284,13 @@ export default function ({ getService }: FtrProviderContext) { 'authentication_realm', 'lookup_realm', 'authentication_provider', + 'authentication_type', ]); expect(apiResponse.body.username).to.be('user2'); + expect(apiResponse.body.authentication_realm).to.eql({ name: 'oidc1', type: 'oidc' }); + expect(apiResponse.body.authentication_provider).to.eql('oidc'); + expect(apiResponse.body.authentication_type).to.be('token'); }); }); diff --git a/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts b/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts index 43d9d680e102af..e4e194a619a954 100644 --- a/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts +++ b/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts @@ -15,8 +15,7 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const config = getService('config'); - // FAILING: https://github.com/elastic/kibana/issues/75707 - describe.skip('OpenID Connect Implicit Flow authentication', () => { + describe('OpenID Connect Implicit Flow authentication', () => { describe('finishing handshake', () => { let stateAndNonce: ReturnType; let handshakeCookie: Cookie; @@ -152,9 +151,13 @@ export default function ({ getService }: FtrProviderContext) { 'authentication_realm', 'lookup_realm', 'authentication_provider', + 'authentication_type', ]); expect(apiResponse.body.username).to.be('user1'); + expect(apiResponse.body.authentication_realm).to.eql({ name: 'oidc1', type: 'oidc' }); + expect(apiResponse.body.authentication_provider).to.eql('oidc'); + expect(apiResponse.body.authentication_type).to.be('token'); }); }); }); diff --git a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts index a2090a8c2cc481..2f6b088ab71905 100644 --- a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts +++ b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts @@ -41,8 +41,7 @@ export default function ({ getService }: FtrProviderContext) { expect(cookie.maxAge).to.be(0); } - // FAILING: https://github.com/elastic/kibana/issues/75707 - describe.skip('PKI authentication', () => { + describe('PKI authentication', () => { before(async () => { await getService('esSupertest') .post('/_security/role_mapping/first_client_pki') @@ -125,6 +124,7 @@ export default function ({ getService }: FtrProviderContext) { authentication_realm: { name: 'pki1', type: 'pki' }, lookup_realm: { name: 'pki1', type: 'pki' }, authentication_provider: 'pki', + authentication_type: 'token', }); // Cookie should be accepted. @@ -169,6 +169,7 @@ export default function ({ getService }: FtrProviderContext) { authentication_realm: { name: 'pki1', type: 'pki' }, lookup_realm: { name: 'pki1', type: 'pki' }, authentication_provider: 'pki', + authentication_type: 'token', }); checkCookieIsSet(request.cookie(response.headers['set-cookie'][0])!); diff --git a/x-pack/test/saml_api_integration/apis/security/saml_login.ts b/x-pack/test/saml_api_integration/apis/security/saml_login.ts index 13b541f75e5bd0..501e1e5f2c2037 100644 --- a/x-pack/test/saml_api_integration/apis/security/saml_login.ts +++ b/x-pack/test/saml_api_integration/apis/security/saml_login.ts @@ -56,13 +56,16 @@ export default function ({ getService }: FtrProviderContext) { 'authentication_realm', 'lookup_realm', 'authentication_provider', + 'authentication_type', ]); expect(apiResponse.body.username).to.be(username); + expect(apiResponse.body.authentication_realm).to.eql({ name: 'saml1', type: 'saml' }); + expect(apiResponse.body.authentication_provider).to.eql('saml'); + expect(apiResponse.body.authentication_type).to.be('token'); } - // FAILING: https://github.com/elastic/kibana/issues/75707 - describe.skip('SAML authentication', () => { + describe('SAML authentication', () => { it('should reject API requests if client is not authenticated', async () => { await supertest.get('/internal/security/me').set('kbn-xsrf', 'xxx').expect(401); }); @@ -92,6 +95,7 @@ export default function ({ getService }: FtrProviderContext) { expect(user.username).to.eql(username); expect(user.authentication_realm).to.eql({ name: 'reserved', type: 'reserved' }); expect(user.authentication_provider).to.eql('basic'); + expect(user.authentication_type).to.be('realm'); }); describe('initiating handshake', () => { From b2939618f499914a1abd151fa0fcb0eb928cb139 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 27 Aug 2020 08:15:37 -0600 Subject: [PATCH 066/216] do not advance beneathMbLayerId if bottomMbLayer could not be found for a layer (#76007) Co-authored-by: Elastic Machine --- .../map/mb/sort_layers.test.ts | 19 +++++++++++++++++++ .../map/mb/sort_layers.ts | 6 ++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.test.ts b/x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.test.ts index 273611e94ee40a..e26a1e43509c81 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.test.ts +++ b/x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.test.ts @@ -200,6 +200,25 @@ describe('sortLayer', () => { ]); }); + // Hidden map layers on map load may not add mbLayers to mbStyle. + test('Should sort with missing mblayers to expected order', () => { + // Notice there are no bravo mbLayers in initial style. + const initialMbStyle = { + version: 0, + layers: [ + { id: `${CHARLIE_LAYER_ID}_fill`, type: 'fill' } as MbLayer, + { id: `${ALPHA_LAYER_ID}_circle`, type: 'circle' } as MbLayer, + ], + }; + const mbMap = new MockMbMap(initialMbStyle); + syncLayerOrder((mbMap as unknown) as MbMap, spatialFilterLayer, mapLayers); + const sortedMbStyle = mbMap.getStyle(); + const sortedMbLayerIds = sortedMbStyle.layers!.map((mbLayer) => { + return mbLayer.id; + }); + expect(sortedMbLayerIds).toEqual(['charlie_fill', 'alpha_circle']); + }); + test('Should not call move layers when layers are in expected order', () => { const initialMbStyle = { version: 0, diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.ts b/x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.ts index 4752eeba2376aa..0c970fe6635576 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.ts +++ b/x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.ts @@ -128,7 +128,8 @@ export function syncLayerOrder(mbMap: MbMap, spatialFiltersLayer: ILayer, layerL if (!isLayerInOrder(mbMap, mapLayer, LAYER_CLASS.LABEL, beneathMbLayerId)) { moveMapLayer(mbMap, mbLayers, mapLayer, LAYER_CLASS.LABEL, beneathMbLayerId); } - beneathMbLayerId = getBottomMbLayerId(mbLayers, mapLayer, LAYER_CLASS.LABEL); + const bottomMbLayerId = getBottomMbLayerId(mbLayers, mapLayer, LAYER_CLASS.LABEL); + if (bottomMbLayerId) beneathMbLayerId = bottomMbLayerId; }); // Sort map layers @@ -137,6 +138,7 @@ export function syncLayerOrder(mbMap: MbMap, spatialFiltersLayer: ILayer, layerL if (!isLayerInOrder(mbMap, mapLayer, layerClass, beneathMbLayerId)) { moveMapLayer(mbMap, mbLayers, mapLayer, layerClass, beneathMbLayerId); } - beneathMbLayerId = getBottomMbLayerId(mbLayers, mapLayer, layerClass); + const bottomMbLayerId = getBottomMbLayerId(mbLayers, mapLayer, layerClass); + if (bottomMbLayerId) beneathMbLayerId = bottomMbLayerId; }); } From 8671db155937c39821091af99ae5cc91f0146a95 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 27 Aug 2020 10:44:59 -0400 Subject: [PATCH 067/216] [Docs] Add `server.xsrf.disableProtection` to settings docs (#76022) --- docs/api/using-api.asciidoc | 6 ++---- docs/apm/api.asciidoc | 4 ++-- docs/setup/settings.asciidoc | 5 ++++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/api/using-api.asciidoc b/docs/api/using-api.asciidoc index c61edfb62b0795..c796aac3d6b271 100644 --- a/docs/api/using-api.asciidoc +++ b/docs/api/using-api.asciidoc @@ -61,10 +61,8 @@ For all APIs, you must use a request header. The {kib} APIs support the `kbn-xsr By default, you must use `kbn-xsrf` for all API calls, except in the following scenarios: * The API endpoint uses the `GET` or `HEAD` operations - -* The path is whitelisted using the <> setting - -* XSRF protections are disabled using the `server.xsrf.disableProtection` setting +* The path is whitelisted using the <> setting +* XSRF protections are disabled using the <> setting `Content-Type: application/json`:: Applicable only when you send a payload in the API request. {kib} API requests and responses use JSON. diff --git a/docs/apm/api.asciidoc b/docs/apm/api.asciidoc index 97fdcd3e13de94..01ba084b9e9e7a 100644 --- a/docs/apm/api.asciidoc +++ b/docs/apm/api.asciidoc @@ -40,8 +40,8 @@ users interacting with APM APIs must have <> setting -* XSRF protections are disabled using the `server.xsrf.disableProtection` setting +* The path is whitelisted using the <> setting +* XSRF protections are disabled using the <> setting `Content-Type: application/json`:: Applicable only when you send a payload in the API request. diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 018cc656362b8e..e1fb1802b2a210 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -577,7 +577,7 @@ all http requests to https over the port configured as `server.port`. | An array of supported protocols with versions. Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2`. *Default: TLSv1.1, TLSv1.2* -| `server.xsrf.whitelist:` +| [[settings-xsrf-whitelist]] `server.xsrf.whitelist:` | It is not recommended to disable protections for arbitrary API endpoints. Instead, supply the `kbn-xsrf` header. The `server.xsrf.whitelist` setting requires the following format: @@ -592,6 +592,9 @@ The `server.xsrf.whitelist` setting requires the following format: [cols="2*<"] |=== +| [[settings-xsrf-disableProtection]] `status.xsrf.disableProtection:` + | Setting this to `true` will completely disable Cross-site request forgery protection in Kibana. This is not recommended. *Default: `false`* + | `status.allowAnonymous:` | If authentication is enabled, setting this to `true` enables unauthenticated users to access the {kib} From b98e2e4f3ddf70f5a55b7a5aa66c4177486b680d Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 27 Aug 2020 10:06:55 -0500 Subject: [PATCH 068/216] [Metrics UI] Replace uses of `any` introduced by Lodash 4 (#75507) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Felix Stürmer Co-authored-by: Elastic Machine --- .../inventory/components/expression.tsx | 4 ++-- .../components/expression.test.tsx | 1 + .../components/expression.tsx | 2 +- .../components/expression_chart.tsx | 22 ++++++++++--------- .../lib/transform_metrics_explorer_data.ts | 6 ++--- .../public/alerting/metric_threshold/types.ts | 2 +- .../components/waffle/legend_controls.tsx | 4 ++-- .../inventory_view/lib/color_from_value.ts | 15 ++++++------- .../inventory_view/lib/nodes_to_wafflemap.ts | 5 +++-- .../metrics_explorer/components/chart.tsx | 17 ++++++-------- .../evaluate_condition.ts | 6 ++--- .../inventory_metric_threshold_executor.ts | 4 +++- ...review_inventory_metric_threshold_alert.ts | 4 +++- .../metric_threshold_executor.ts | 20 ++++++++++------- .../preview_metric_threshold_alert.ts | 15 ++++++++----- .../server/lib/snapshot/response_helpers.ts | 3 ++- .../infra/server/lib/snapshot/snapshot.ts | 7 ++++-- .../infra/server/routes/ip_to_hostname.ts | 2 +- .../server/utils/create_afterkey_handler.ts | 7 ++++-- 19 files changed, 82 insertions(+), 64 deletions(-) diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx index 78cabcf3544372..5ac2f407839e40 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx @@ -85,7 +85,7 @@ interface Props { nodeType: InventoryItemType; filterQuery?: string; filterQueryText?: string; - sourceId?: string; + sourceId: string; alertOnNoData?: boolean; }; alertInterval: string; @@ -379,7 +379,7 @@ export const Expressions: React.FC = (props) => { { criteria: [], groupBy: undefined, filterQueryText: '', + sourceId: 'default', }; const mocks = coreMock.createSetup(); 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 8031f7a03731a4..6b102045fa516f 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 @@ -400,7 +400,7 @@ export const Expressions: React.FC = (props) => { = ({ }; const isDarkMode = context.uiSettings?.get('theme:darkMode') || false; const dateFormatter = useMemo(() => { - const firstSeries = data ? first(data.series) : null; - return firstSeries && firstSeries.rows.length > 0 - ? niceTimeFormatter([ - (first(firstSeries.rows) as any).timestamp, - (last(firstSeries.rows) as any).timestamp, - ]) - : (value: number) => `${value}`; - }, [data]); + const firstSeries = first(data?.series); + const firstTimestamp = first(firstSeries?.rows)?.timestamp; + const lastTimestamp = last(firstSeries?.rows)?.timestamp; + + if (firstTimestamp == null || lastTimestamp == null) { + return (value: number) => `${value}`; + } + + return niceTimeFormatter([firstTimestamp, lastTimestamp]); + }, [data?.series]); /* eslint-disable-next-line react-hooks/exhaustive-deps */ const yAxisFormater = useCallback(createFormatterForMetric(metric), [expression]); @@ -138,8 +140,8 @@ export const ExpressionChart: React.FC = ({ }), }; - const firstTimestamp = (first(firstSeries.rows) as any).timestamp; - const lastTimestamp = (last(firstSeries.rows) as any).timestamp; + const firstTimestamp = first(firstSeries.rows)!.timestamp; + const lastTimestamp = last(firstSeries.rows)!.timestamp; const dataDomain = calculateDomain(series, [metric], false); const domain = { max: Math.max(dataDomain.max, last(thresholds) || dataDomain.max) * 1.1, // add 10% headroom. diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.ts index f46a7f3e5a5e4d..d65a33d68a1fd0 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.ts @@ -13,9 +13,9 @@ export const transformMetricsExplorerData = ( data: MetricsExplorerResponse | null ) => { const { criteria } = params; - if (criteria && data) { - const firstSeries = first(data.series) as any; - const series = firstSeries.rows.reduce((acc: any, row: any) => { + const firstSeries = first(data?.series); + if (criteria && firstSeries) { + const series = firstSeries.rows.reduce((acc, row) => { const { timestamp } = row; criteria.forEach((item, index) => { if (!acc[index]) { diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts index b2317c558be44f..b898f58e695659 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts @@ -55,7 +55,7 @@ export interface AlertParams { criteria: MetricExpression[]; groupBy?: string[]; filterQuery?: string; - sourceId?: string; + sourceId: string; filterQueryText?: string; alertOnNoData?: boolean; } diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx index 3997a7eab44e81..ba56d8b82feeb1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx @@ -266,7 +266,7 @@ export const LegendControls = ({ fullWidth label={ { @@ -79,7 +80,7 @@ export const calculateSteppedGradientColor = ( return rule.color; } return color; - }, (first(rules) as any).color || defaultColor); + }, first(rules)?.color ?? defaultColor); }; export const calculateStepColor = ( @@ -106,7 +107,7 @@ export const calculateGradientColor = ( return defaultColor; } if (rules.length === 1) { - return (last(rules) as any).color; + return last(rules)!.color; } const { min, max } = bounds; const sortedRules = sortBy(rules, 'value'); @@ -116,10 +117,8 @@ export const calculateGradientColor = ( return rule; } return acc; - }, first(sortedRules)) as any; - const endRule = sortedRules - .filter((r) => r !== startRule) - .find((r) => r.value >= normValue) as any; + }, first(sortedRules))!; + const endRule = sortedRules.filter((r) => r !== startRule).find((r) => r.value >= normValue); if (!endRule) { return startRule.color; } diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/nodes_to_wafflemap.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/nodes_to_wafflemap.ts index b56b409717cc6f..95da994c246169 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/nodes_to_wafflemap.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/nodes_to_wafflemap.ts @@ -29,8 +29,9 @@ function findOrCreateGroupWithNodes( * look for the full id. Otherwise we need to find the parent group and * then look for the group in it's sub groups. */ - if (path.length === 2) { - const parentId = (first(path) as any).value; + const firstPath = first(path); + if (path.length === 2 && firstPath) { + const parentId = firstPath.value; const existingParentGroup = groups.find((g) => g.id === parentId); if (isWaffleMapGroupWithGroups(existingParentGroup)) { const existingSubGroup = existingParentGroup.groups.find((g) => g.id === id); diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx index 3802366fe2ac5a..41d8014b4a5c1b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx @@ -74,16 +74,13 @@ export const MetricsExplorerChart = ({ const [from, to] = x; onTimeChange(moment(from).toISOString(), moment(to).toISOString()); }; - const dateFormatter = useMemo( - () => - series.rows.length > 0 - ? niceTimeFormatter([ - (first(series.rows) as any).timestamp, - (last(series.rows) as any).timestamp, - ]) - : (value: number) => `${value}`, - [series.rows] - ); + const dateFormatter = useMemo(() => { + const firstRow = first(series.rows); + const lastRow = last(series.rows); + return firstRow && lastRow + ? niceTimeFormatter([firstRow.timestamp, lastRow.timestamp]) + : (value: number) => `${value}`; + }, [series.rows]); const tooltipProps = { headerFormatter: useCallback( (data: TooltipValue) => moment(data.value).format(dateFormat || 'Y-MM-DD HH:mm:ss.SSS'), diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index 9be6a4b52157c9..2f3593a11f6643 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts @@ -122,14 +122,14 @@ const getData = async ( if (!nodes.length) return { [UNGROUPED_FACTORY_KEY]: null }; // No Data state return nodes.reduce((acc, n) => { - const nodePathItem = last(n.path) as any; + const { name: nodeName } = n; const m = first(n.metrics); if (m && m.value && m.timeseries) { const { timeseries } = m; const values = timeseries.rows.map((row) => row.metric_0) as Array; - acc[nodePathItem.label] = values; + acc[nodeName] = values; } else { - acc[nodePathItem.label] = m && m.value; + acc[nodeName] = m && m.value; } return acc; }, {} as Record | undefined | null>); diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index db1ff26ee1810d..bdac9dcd1dee8c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -42,6 +42,8 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = alertOnNoData, } = params as InventoryMetricThresholdParams; + if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); + const source = await libs.sources.getSourceConfiguration( services.savedObjectsClient, sourceId || 'default' @@ -53,7 +55,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = ) ); - const inventoryItems = Object.keys(first(results) as any); + const inventoryItems = Object.keys(first(results)!); for (const item of inventoryItems) { const alertInstance = services.alertInstanceFactory(`${item}`); // AND logic; all criteria must be across the threshold diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts index 562f344dbd0601..755c395818f5a7 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts @@ -40,6 +40,8 @@ export const previewInventoryMetricThresholdAlert = async ({ }: PreviewInventoryMetricThresholdAlertParams) => { const { criteria, filterQuery, nodeType } = params as InventoryMetricThresholdParams; + if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); + const { timeSize, timeUnit } = criteria[0]; const bucketInterval = `${timeSize}${timeUnit}`; const bucketIntervalInSeconds = getIntervalInSeconds(bucketInterval); @@ -57,7 +59,7 @@ export const previewInventoryMetricThresholdAlert = async ({ ) ); - const inventoryItems = Object.keys(first(results) as any); + const inventoryItems = Object.keys(first(results)!); const previewResults = inventoryItems.map((item) => { const numberOfResultBuckets = lookbackSize; const numberOfExecutionBuckets = Math.floor(numberOfResultBuckets / alertResultsPerExecution); 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 9265e8089e9154..c85685b4cdca8e 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 @@ -22,6 +22,8 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => async function (options: AlertExecutorOptions) { const { services, params } = options; const { criteria } = params; + if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); + const { sourceId, alertOnNoData } = params as { sourceId?: string; alertOnNoData: boolean; @@ -34,8 +36,8 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => const config = source.configuration; const alertResults = await evaluateAlert(services.callCluster, params, config); - // Because each alert result has the same group definitions, just grap the groups from the first one. - const groups = Object.keys(first(alertResults) as any); + // Because each alert result has the same group definitions, just grab the groups from the first one. + const groups = Object.keys(first(alertResults)!); for (const group of groups) { const alertInstance = services.alertInstanceFactory(`${group}`); @@ -60,7 +62,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => let reason; if (nextState === AlertStates.ALERT) { reason = alertResults - .map((result) => buildFiredAlertReason(formatAlertResult(result[group]) as any)) + .map((result) => buildFiredAlertReason(formatAlertResult(result[group]))) .join('\n'); } if (alertOnNoData) { @@ -121,11 +123,13 @@ const mapToConditionsLookup = ( {} ); -const formatAlertResult = (alertResult: { - metric: string; - currentValue: number; - threshold: number[]; -}) => { +const formatAlertResult = ( + alertResult: { + metric: string; + currentValue: number; + threshold: number[]; + } & AlertResult +) => { const { metric, currentValue, threshold } = alertResult; if (!metric.endsWith('.pct')) return alertResult; const formatter = createFormatter('percent'); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts index 5aca7f0890940a..0f2afda663da81 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts @@ -49,6 +49,8 @@ export const previewMetricThresholdAlert: ( iterations = 0, precalculatedNumberOfGroups ) => { + if (params.criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); + // There are three different "intervals" we're dealing with here, so to disambiguate: // - The lookback interval, which is how long of a period of time we want to examine to count // how many times the alert fired @@ -70,7 +72,7 @@ export const previewMetricThresholdAlert: ( // Get a date histogram using the bucket interval and the lookback interval try { const alertResults = await evaluateAlert(callCluster, params, config, timeframe); - const groups = Object.keys(first(alertResults) as any); + const groups = Object.keys(first(alertResults)!); // Now determine how to interpolate this histogram based on the alert interval const alertIntervalInSeconds = getIntervalInSeconds(alertInterval); @@ -81,7 +83,7 @@ export const previewMetricThresholdAlert: ( // buckets would have fired the alert. If the alert interval and bucket interval are the same, // this will be a 1:1 evaluation of the alert results. If these are different, the interpolation // will skip some buckets or read some buckets more than once, depending on the differential - const numberOfResultBuckets = (first(alertResults) as any)[group].shouldFire.length; + const numberOfResultBuckets = first(alertResults)![group].shouldFire.length; const numberOfExecutionBuckets = Math.floor( numberOfResultBuckets / alertResultsPerExecution ); @@ -120,8 +122,7 @@ export const previewMetricThresholdAlert: ( ? await evaluateAlert(callCluster, params, config) : []; const numberOfGroups = - precalculatedNumberOfGroups ?? - Math.max(Object.keys(first(currentAlertResults) as any).length, 1); + precalculatedNumberOfGroups ?? Math.max(Object.keys(first(currentAlertResults)!).length, 1); const estimatedTotalBuckets = (lookbackIntervalInSeconds / bucketIntervalInSeconds) * numberOfGroups; // The minimum number of slices is 2. In case we underestimate the total number of buckets @@ -152,14 +153,16 @@ export const previewMetricThresholdAlert: ( // `undefined` values occur if there is no data at all in a certain slice, and that slice // returns an empty array. This is different from an error or no data state, // so filter these results out entirely and only regard the resultA portion - .filter((value) => typeof value !== 'undefined') + .filter( + (value: Value): value is NonNullable => typeof value !== 'undefined' + ) .reduce((a, b) => { if (!a) return b; if (!b) return a; return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]; }) ); - return zippedResult as any; + return zippedResult; } else throw e; } }; diff --git a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts b/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts index 646ce9f2409af9..2652e362b7eff8 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts +++ b/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts @@ -127,7 +127,8 @@ export const getNodeMetrics = ( avg: null, })); } - const lastBucket = findLastFullBucket(nodeBuckets, options) as any; + const lastBucket = findLastFullBucket(nodeBuckets, options); + if (!lastBucket) return []; return options.metrics.map((metric, index) => { const metricResult: SnapshotNodeMetric = { name: metric.type, diff --git a/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts b/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts index 5f359b0523d9ff..33d8e738a717ec 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts +++ b/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts @@ -27,6 +27,8 @@ import { InfraSnapshotRequestOptions } from './types'; import { createTimeRangeWithInterval } from './create_timerange_with_interval'; import { SnapshotNode } from '../../../common/http_api/snapshot_api'; +type NamedSnapshotNode = SnapshotNode & { name: string }; + export type ESSearchClient = ( options: CallWithRequestParams ) => Promise>; @@ -34,7 +36,7 @@ export class InfraSnapshot { public async getNodes( client: ESSearchClient, options: InfraSnapshotRequestOptions - ): Promise<{ nodes: SnapshotNode[]; interval: string }> { + ): Promise<{ nodes: NamedSnapshotNode[]; interval: string }> { // Both requestGroupedNodes and requestNodeMetrics may send several requests to elasticsearch // in order to page through the results of their respective composite aggregations. // Both chains of requests are supposed to run in parallel, and their results be merged @@ -184,11 +186,12 @@ const mergeNodeBuckets = ( nodeGroupByBuckets: InfraSnapshotNodeGroupByBucket[], nodeMetricsBuckets: InfraSnapshotNodeMetricsBucket[], options: InfraSnapshotRequestOptions -): SnapshotNode[] => { +): NamedSnapshotNode[] => { const nodeMetricsForLookup = getNodeMetricsForLookup(nodeMetricsBuckets); return nodeGroupByBuckets.map((node) => { return { + name: node.key.name || node.key.id, // For type safety; name can be derived from getNodePath but not in a TS-friendly way path: getNodePath(node, options), metrics: getNodeMetrics(nodeMetricsForLookup[node.key.id], options), }; diff --git a/x-pack/plugins/infra/server/routes/ip_to_hostname.ts b/x-pack/plugins/infra/server/routes/ip_to_hostname.ts index 08ad266a22f9b3..e699de58193310 100644 --- a/x-pack/plugins/infra/server/routes/ip_to_hostname.ts +++ b/x-pack/plugins/infra/server/routes/ip_to_hostname.ts @@ -48,7 +48,7 @@ export const initIpToHostName = ({ framework }: InfraBackendLibs) => { body: { message: 'Host with matching IP address not found.' }, }); } - const hostDoc = first(hits.hits) as any; + const hostDoc = first(hits.hits)!; return response.ok({ body: { host: hostDoc._source.host.name } }); } catch ({ statusCode = 500, message = 'Unknown error occurred' }) { return response.customError({ diff --git a/x-pack/plugins/infra/server/utils/create_afterkey_handler.ts b/x-pack/plugins/infra/server/utils/create_afterkey_handler.ts index cdfb9d7cc99f30..d6378c2dea2725 100644 --- a/x-pack/plugins/infra/server/utils/create_afterkey_handler.ts +++ b/x-pack/plugins/infra/server/utils/create_afterkey_handler.ts @@ -10,11 +10,14 @@ import { InfraDatabaseSearchResponse } from '../lib/adapters/framework'; export const createAfterKeyHandler = ( optionsAfterKeyPath: string | string[], afterKeySelector: (input: InfraDatabaseSearchResponse) => any -) => (options: Options, response: InfraDatabaseSearchResponse): Options => { +) => ( + options: Options, + response: InfraDatabaseSearchResponse +): Options => { if (!response.aggregations) { return options; } - const newOptions = { ...options } as any; + const newOptions = { ...options }; const afterKey = afterKeySelector(response); set(newOptions, optionsAfterKeyPath, afterKey); return newOptions; From bf25e16a8bb19351c93f5c47a4c927ee15f99f2b Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Thu, 27 Aug 2020 08:23:07 -0700 Subject: [PATCH 069/216] Skip creating SpacesClient when not needed in auth interceptor (#75706) Co-authored-by: Elastic Machine --- .../lib/request_interceptors/on_post_auth_interceptor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts index 772914bb532112..3d6084d37a3846 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts @@ -38,13 +38,12 @@ export function initSpacesOnPostAuthRequestInterceptor({ const isRequestingSpaceRoot = path === '/' && spaceId !== DEFAULT_SPACE_ID; const isRequestingApplication = path.startsWith('/app'); - const spacesClient = await spacesService.scopedClient(request); - // if requesting the application root, then show the Space Selector UI to allow the user to choose which space // they wish to visit. This is done "onPostAuth" to allow the Saved Objects Client to use the request's auth credentials, // which is not available at the time of "onRequest". if (isRequestingKibanaRoot) { try { + const spacesClient = await spacesService.scopedClient(request); const spaces = await spacesClient.getAll(); if (spaces.length === 1) { @@ -77,6 +76,7 @@ export function initSpacesOnPostAuthRequestInterceptor({ try { log.debug(`Verifying access to space "${spaceId}"`); + const spacesClient = await spacesService.scopedClient(request); space = await spacesClient.get(spaceId); } catch (error) { const wrappedError = wrapError(error); From 69a8d061299c10075d34d26bb0359a77778a8f70 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Thu, 27 Aug 2020 08:40:56 -0700 Subject: [PATCH 070/216] [Reporting/Download CSV] Get the file name from savedSearch data (#76031) * [Reporting/Download CSV] provide title even if panel \titles are hidden in the dashboard * add functional test * Update embeddable_panel.tsx * Update download_csv.ts --- .../public/lib/panel/embeddable_panel.tsx | 1 + .../panel_actions/get_csv_panel_action.tsx | 2 +- .../apps/dashboard/reporting/download_csv.ts | 46 +++++++++++++----- .../reporting/ecommerce_kibana/data.json.gz | Bin 4138 -> 4219 bytes 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index d8659680dceb95..ca5cb5ca4f0d51 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -211,6 +211,7 @@ export class EmbeddablePanel extends React.Component { const kibanaTimezone = this.core.uiSettings.get('dateFormat:tz'); const id = `search:${embeddable.getSavedSearch().id}`; - const filename = embeddable.getTitle(); + const filename = embeddable.getSavedSearch().title; const timezone = kibanaTimezone === 'Browser' ? moment.tz.guess() : kibanaTimezone; const fromTime = dateMath.parse(from); const toTime = dateMath.parse(to); diff --git a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts index b39613b3dbd1bb..5c41945cb88d85 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts +++ b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts @@ -14,15 +14,27 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; const csvPath = path.resolve(REPO_ROOT, 'target/functional-tests/downloads/Ecommerce Data.csv'); +// checks every 100ms for the file to exist in the download dir +// just wait up to 5 seconds +const getDownload$ = (filePath: string) => { + return Rx.interval(100).pipe( + map(() => fs.existsSync(filePath)), + filter((value) => value === true), + first(), + timeout(5000) + ); +}; + export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); const dashboardPanelActions = getService('dashboardPanelActions'); const log = getService('log'); const testSubjects = getService('testSubjects'); + const find = getService('find'); const PageObjects = getPageObjects(['reporting', 'common', 'dashboard']); - describe('Reporting Download CSV', () => { + describe('Download CSV', () => { before('initialize tests', async () => { log.debug('ReportingPage:initTests'); await esArchiver.loadIfNeeded('reporting/ecommerce'); @@ -33,10 +45,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after('clean up archives and previous file download', async () => { await esArchiver.unload('reporting/ecommerce'); await esArchiver.unload('reporting/ecommerce_kibana'); + }); + + afterEach('remove download', () => { try { fs.unlinkSync(csvPath); } catch (e) { - // nothing to worry + // it might not have been there to begin with } }); @@ -50,19 +65,26 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('embeddablePanelAction-downloadCsvReport'); await testSubjects.existOrFail('csvDownloadStarted'); // validate toast panel - // check every 100ms for the file to exist in the download dir - // just wait up to 5 seconds - const success$ = Rx.interval(100).pipe( - map(() => fs.existsSync(csvPath)), - filter((value) => value === true), - first(), - timeout(5000) - ); - - const fileExists = await success$.toPromise(); + const fileExists = await getDownload$(csvPath).toPromise(); expect(fileExists).to.be(true); // no need to validate download contents, API Integration tests do that some different variations }); + + it('Gets the correct filename if panel titles are hidden', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard Hidden Panel Titles'); + const savedSearchPanel = await find.byCssSelector( + '[data-test-embeddable-id="94eab06f-60ac-4a85-b771-3a8ed475c9bb"]' + ); // panel title is hidden + await dashboardPanelActions.toggleContextMenu(savedSearchPanel); + + await testSubjects.existOrFail('embeddablePanelAction-downloadCsvReport'); + await testSubjects.click('embeddablePanelAction-downloadCsvReport'); + await testSubjects.existOrFail('csvDownloadStarted'); + + const fileExists = await getDownload$(csvPath).toPromise(); // file exists with proper name + expect(fileExists).to.be(true); + }); }); } diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json.gz b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json.gz index 454d260a518cd39001004d026afd8dc00c5e01d6..d4dd21528a882aa45d262280bb0323f2b9b46bf3 100644 GIT binary patch delta 4208 zcmV-$5RdPwAp0PHABzYGp4~=Y0s~}WbYU)Pb8l_{?OjWcB7NSQ45BSr)9&B6%c3tTUPAP9_jVi<$mWDqCusv8$s zwBhymC-gSRkI1Pik|HH)TI#nZ0;apAQ?Kv5>YPIsIUl}%rYH?|I3|s*((og%(d2|Z z9HgA`A$)$0Enwf<}SUZdPj`Zx{PJ2@0nbK3omja0v5 zX{x3XN5yS_O;b@Hn?}bo`;OUeWW$g0c^Wb4lbD1aNnno08TCogoQo)9qoAi_4$tpl z##?oyIVy5g&Dusz7g=3XYXR+jx!lof{O9Pk*FIcY=$*CDUoLb_hefybc30QCn%=UU z&LxD-NI*t}#fRPE)AF)EkGEpxigwb3#Bbwdi;!@CGh$Rs$>NmEh%V=38sYik(NW83 z>z5FZkbyeJjFC9(E~NRTrB_*_Dxy9mfzO57c>Li`Bg7-J)9CIrdeq-(G|EAV1DJJ^`v1r6B_vg3pN;(=iAQzV>mD5KbaE_J-IE_;g$cK*NJUOa_=$ zsWHKS{{1)_y%~9rxWJNpmZN4BA!lk)kh1Ts;7q(ICRLnaI&w)YXv@IKiBouT^1^_G zeN+*ZiB-|%4Nql5ivzT82-S-0@ANHbtZ=aCel>*y; zqjL+A3S1gOmc<)x!EMP1)8Ot)bwDCIZh4d)F0r-T0l-nnXb8T!VyI!!_^?NlsOLrg z%HR$ns8?tdR`EhxZyZq$XBu*;>DEBQaDcob_A$a?+Q%MC<3;nQTMX}Iu&W=~tD>I{ z>3|mE`NeD78;Uj(<|?GK<2dqDk0q^th0)%krI*33zNJ@1KW*tntBVuT8^_cmi)C3u z9Q1qrAi}q^+gEX?A!Vxw{<7C#Z{8(dor|$GN|@^$9xTQ0r%fkAcPt(hczv3PrF(cJ z5v-C^6yV1s_CQ;Buwq`lQfXZ+>Q~6SU!Leqy3ueI1IS8379VC3!FjlYh}+zMT|m4N zS7B>Wze4_8RmNO@{vgF6qwMe!mke*(%#z*o?q9Mf&@z0mlGz2|?x(>j?)88?AW5={ z&_#W=ib?6^eDa3U{>-FV#PBK^c5crrhg;xXod6^8f(w3-@+&;jC079U!iVT?GamBO zx_1)glk!<@Mn~j6KQy}uxcRw%*~ta&YBv^$X5vMo5wE^y4{VR-AYPl}vQ%X}5Tr6=cI~x~f)*)Hg!h6L3q5Bi!u?@n1a7#kl5)A!d z>xY{y$#Z3fdc_qzj%GAn@LQ6B7X|4ktn$an*>$?=iuuXOX=-uSr`nL;GO1E4rwr-_ zOnC~>YpxAeJ3#6%Vf;zu!3pW{QumFr0}d(UY@-sXRu>g{6;hn2hR+MT)Q}E_0sOGn zgJ7~{>`A+u5Fjq_tDn4o;>thnG{goL;Bb(N>u*5Wm2F2r@6&+seXskNZ)QT-?~?vz zz9}waKQLj*K1z%yQOu@Hp!|8hYBCWwN~9;XiYT_bxAN^yc@y^6vO@L>AJyhQf03E+ zqPvscX_*GP#O~zdWavgX_Pds+TWx}_c!MhDuX^RpnQh4)>kP1eSvAusdz@oUkpP}l zoWmy%aY%w~ZU#vy`a(?K!0~{FMKCQhJLY4GiTDF36NlPqh{v=9#z9PdZcdVsgDlt| zW`FnLuWrDHA+Hlm?nPp}=4ra8_jR>{JY6+>(^Fkr*HzPWbwe`|vWO;m7C7FKF0}AK zo|HhU=leX1xF;Tes^a*H`O01_2tCACTY^c!)cIT*uHz6*we60n8ivsa!}UzHZ#sI% zX}cPBtT|jRGH`h=wI#XCRVjGlTpFF6%TB?iV-V~jtFKxJd#Zr}v1{9!+QuE?8@B0z zlh$&%*udqv)Gx?oyFJb2gh^BPG{?1E)z&myHFN-sZG)(Po{4NwO}pRsP%V>-4P2hd z_63B9mfZs>L8*TPTN+oXrdYaP?6SLI>@m*>)0l1sr< zg-vsoOMr)5)HYS$HZ9fgz_A>xbyN-a9b(zO)o<5tK8p=po=X!}s}wSj{l+92z5{h> z48d_apo}qp?A_V(l9gmE52UCzIEeBHim1s0yq2n4+sNwbh{tPWS?^DmQ|}VUFyq<9 zMB{ceJ(b7Zc@34)sL)a`gnlhGpn&q=M<9EZZP< zU)9>4>089m5JvU&L>5WvONBj}q&T2I6>QW7?;0$BI>a;Nco)W^T_Khw%EVeMdOemb zty~a-+cw17mSO2vQYUzj`NA&c!D?!PN8^CNAIQwr zNTlX}YEcfatAz8bgsf;@7YXYkVO=Dwi-dKNur3mAsYp=XroK-?MZ_mX2B>x6ur3_d zg~PgVxJ=IaZidzy$vJvQo$SUEGFy(VO#;V^6c;PguBaxv zLw6D<#~Re)Mjws|R=gpOS(*2gZZQyX!O_uwoN(H#n5r0)NuW3e!WR&nXq zaJ|v={0SM1NVe=`fgSm-wc3Xwd42<0&5rH%AvKaP=Hy0lP&Cu0ob*VWfb?vWzjzRT zOqcj?C3zlKIxp!aj$5hiKHwhOSjs!VE(tQWs?!wSncGsFhMq70s0h%J zumYAXBqSu_$t-t|B)MR%I6n1&^jRItVxC!k1L%GkZn?(A6Zrhhn=rBSBoIeT@{7vi z$j#>^^2i?o;JTH@B_))Qn3CjSZb^@4R-84g0&>Ezw@c(He!r;P1o-ko&GJrvb0n-E z?+aUegRp%P$XVSe#W3O_-OE}gz3M9J~neOH4omIBr8T-#2i)Th+&q7k*LTb8ai@%zI>p zB@5d9jAyN6L!jj}2(s5422m`3NZ)(Qwk+e`w6PqOHNI0HUP@8v=!!D3C2E*;Ms}MS znV~}_j@B93LK)eZl50)KVE%PJ_HU4n6?(pcphJ^4qcCNq-m{59C9;%*Dt}{GK=w#5 zH-DNNJ_=vBhUl@xMV58+xP|DEUB_v64Ws2CXT_q&CF8|HcWI00@sGcMe)7-HzW(R4 zpIuYP7~php6G3C{URDCI*lq?J7EQ(hK70?yp?D0X*q^ew`21i*gK(5TTFW=(5!jT( zC3o9AkYv+N-o&sWYmv&N+d16VV>ySQZWRgj{XC&|p?s+!M^F*Ln3t z+%hxbIK{%{2sXczoRkx80D_SIQmR&~y^DGBi6_J3fs6k`LuOOc^rohpP1Mr=Q%+Y{ z4p>_DU{X;W!@7gK%0n%uRR73tBb@A()^i6NRRV8+0}Ez_l-$98eHsKWLnJ!g%W#*T z_09`p>F-ggw}y@ejt4D#f#(be)i(4 z?|%K_|G)d_yH8#mzxe8!Vqz(T-E2@iF9H_VfNGHDAH_>0=EX87b3~k&*>qnPedNs5 z@Uxk}2KfBo1HmMJ3nz2p#?kypQ`y*yt@wK|;ndDs2~E?)<@Vz`L%W8pd5N3ScVlb5 znyt?2YOti)UfnB3=WSHm^z7v4sltZo;o0%uuj^9J2*#S>spc=pBto z_<)AH$vfdD+26))CH2WCgxRNfP=eFXmC@PBZy1oEMG2yRv*V{!+56__p>mL(eoYnc z^l!@W+piSw?D)&H3EfV=4hPByDOSSa>F+~j@9g+FYbwL&^z)E+`XU@QmE`Q?FUlUQ z^|7aX^C_QrJmgKv>CM*qK7s`uPM%@KOQEmFSNRm$y(m<|!M9(XoqR@{iYqxB@{_R*6l&7E%W-;KGJK?a zcKjFU8V-P8_zh6XQei_96#!xU$+HaL!Rb@U@-M?-YiB2ff7@{courgKXdVi1fN^&S z-f8w*_ zXJIZm=z08n%nycf{hWRt30(*!KMylN1;d%4$Qd~h_;~s!;B+W{gGGG~f4IiSLyi)H z5^$SO6Q2I9rEJweI6L_r$M^d~_{|mjg$KN1S`3%#?MizcP%Mp#a|&V--@^fX`M8)6 zBzXMykmLS?RN=GCc7aW>iI0See~tm5_w7^Z4mm`@pHS^%?MbF}_y=EptZbZ}d?_UR zM`haiBtmY?pN5KNq5?_D+yLSKfQ>v5Zn-%~4^RIOyy(BdJ}Li{aDNS($|eXAOjD@W z6`uCZ|B4}Eyz(QyhR3;9-iD5w+~JxE4Z+)_NP%ZX7rxHTN?OyeDEHJ~e<=wlxuv|_ zQgnpcEoB@0cA&I%tCf{)MSz{+?@D6p2#*tW@;};Dzg{4WR>xOj5|jclbyG%?ha}#> z978!YawTo^7r*3<<;N}(!Waz)$xKE7 zc_%3CQ90)j(twU46wW;udk>HK8y5Zg3z?mk(YD^da4J=6unSy2Eiaq<&A$K(o8ofr GbN~RTtxO#N delta 4126 zcmV+(5aI9pAgUmLABzYGwZV^G0s~}WbYU)Pb8l_{?Oj`sNdrzb*@EK#*hkuq8AON<8M%|j3*3#=c4AP9_jVi<#LGFWHhr*16r z(1xGKKcQb2`4Kr)MN*_hO-p^(WB^aMbn5netnW_|V(zSh5O-)mLd$q;8Dd#`|EYEF09vytle zEKSuk;;6WPt7$44V$LGXftM1Ov#dr%!#hxWESE1;?YsZ z>FSpdkC1^n!Hkh4>My1Fm8Dl(qb{N$C85uS+Isx)PAkG=veWACv<+Wn-5lg0>2rf;fYxATJF_ z*hd{vl~@;D(eUJUz)FNmDOvWo4IO*1Lj-YhxExSM4p<$hc43?=in9RP*(Hbjak8Y3 zmz}R&ws}*Ux!V#HfFDC-r1IkgLkwUuB0oBRn&B}^c%(SI1aI1P@ivPzW$~CK1D7Uj zG~jWj4!We&AWQ0v=wbohAx%;?ko1dLJi0N9pU3D&e@xY7y zwZR?6P_NK9s^f*W!6c>}&Mf3g)2)Gq;Q)C}>|>0hY=}LUCClbdw;0~5VAnse*F`@Y z(g7{R^NZKEHxz9o%ymd-CrRvQ9!ooaOQXF-ORs`me@m~6e%8{K3(`J*QI~EU0ygp6E(gQq} z2-e9dO7Ihsc%UsDteID@Ra)1G`W5o-mnVADZZulQ0J4^l#fP~>a1rh><~DbK7Z9(- zDQqL^SID31%2?>nA7waVlpS8;$nd7kEZxoS{*gtAmf?f7%q{@;FbmgluLtBINz-+N zF6y&&OiC~3lQ)$1=O)c!hS$lk3wvHW+yd|V1Q>}IT=1iepYTYRTm#rkU!uFsc*t+- z-bs{Cs&};+9h3X~((ESS=J#fQrw82iZY&hd#EZvcUVYDB*dEg$5s!r)ktBs*^_#h~ zmv&or$LgR9U;D04x7_t+qpx*4-QH!c+)MHIE#zn*UZiTC<@0;ZSj8GbYR~LBuIu== zXSu82Yks)^<voK=h_U-iYt2@&1t&iw;}^C4zqDo=Z}-~bGrJ9#m&iCYI)bE-cUR;sZ(pW44MYa zcnZ)P&IaopAa$5B{-pNeg!Fi+`&QKfN0f24QH9i~i<-PjDNa?x=Y?HrM1xTX|Ja*h zINdV#q}@#k5EuCAC$G4FijO-jv4Mp+3Nms229#adb_Dbx4H@6}`j7c$CY1d?8EzJv z;wtt76ISe_!gv}dY{mqtpBJkp7jdgXdeW$fa=Uw{*zT0KVSlYEWUus5ZQ=VDxd|`3 zJL$cSX`oB&PCiaYZj2MZZ+W`aCFqJbsB-?gSKgf2R_w9K0NZtcGo7)=1=bV`;Azb{ zeDV-SB;4j^kXE8E#PkiE1T-pxX_eUtA5%`mUqG2U)J{vhrX?^23H7--Nk$IxV1JnZ z-iP110Ut)ZPB6I_iSe4J>7G8+)gJP6)$mPEb!}Z&P1Ds4%|yr|n&esFcu%^}(gS%? z0;!(w^DN??c&Uni<16Q@da)o35MONtCIwUHb7{DaLp0U4d!}j_#t;nGGu5H#=sl5_lR%UrUy>i z$mMbaSLaf{AeY_lESFOzP2JNR*K$=`(`?ny0Wh`=qIxEOvOzW7;m|{kOfENYbtbzP zWYW-bCN-^4B`UeZ8tL^MAAG8Zh-x@pTg9ersgCUoyN0jzOs83u%MDzeOJhYY1yeOP z&3P^X9&%CFRDIjDRKo+uaO<1i~$Uyd+kaYAO)TIdo z$Ju}~!LWCK=kH6_lCdI?qQ>AL$|ESErZ4bXs%~u~tFI#-uaRYaI9pD=Pawlg<`)x< z+wtsFc3-#qCWL;w`(bNQ4g%>%5qg)*L0(==R3kam6Wzw2bd5ko^5HP;h)Bxx=c_Gp^nkp4ul(HOiNu;>tf&yeF?7>jm=SXL+#8?orkSn{-T zNe~9^TG#^+Huky_gljpT>J5=^J6+TBE&F94x>qO=0#&DJODy^Yuozc}MeFL=6^3## zxtObAS$LA+&w2J-ouDHX8LEc15$bnsh_xNVy39-3HH`fgy!XluGwMFK0e*6TPSSn); zkOoN@3-Ta2D4y$6O>m@5Kq|J$|9%LVF7e+rZAu3KqbQbH+7C`lg{mh^aT#d*Uj zA*T#`yF^~b@yjYrfUhppuI@BP!us*Pu*J6s+b1M?$ltZ8T%+OQ`GES2S9N+W-Fr0g z!s=Zc;aHP6lu-oUfF_Y?EBks95OnoJeBhO2ZX-?T!A6JKddS|;fK(F<#v-6 zae_|>-#2iwTi3^k7kP2$b8c(uEPCXIC5zYnoM)|KL!jj>4D-KM62^%jeeWIHvW$DP z#&T5E_)c?psYIotE6T`Ls9|1bWVe}-89HR*=ygW6R7N(T0f3soDk7O73TUBG>RGm&!$>Q(pD@pj)RD|D8pny?Wth}EWLSnIdyw^riCq2k_>0)?V zu=98t7*P@ZOeBJKPBu%rZC2^FSLK@GTWM_w>8~=Y@?3;uX4K5a=_B^`-Nq34C@9(m4`Y`rT&rM#yH)rtmg(B zbpmgH3kzn2l-%Gx4a1iq5}oelxGS%06os+$_c&r(kXZo5LPs+(vGnk32!tz~@V^d( zw-*Qv9tdrOUI)U(f$+r_FOI+a>5H$v`^}60`|gwPe)Zz`#aGvV6cZ~U>}G@Fc@eNY z18P88d=&47EsAAO=7=~kx9Pqt`pB8<;b*h>YhLFE9|$ISI9U)kjuuCn+QwdP#ovPo zXLjCNXqqLiwjVbc+BIy=D?F6G8(Z_$Y;{&&gC))G`kok_w^41=vy-1?3L9mIXUBiL zuA`nYPImoxKdSA2QnUL>#m+T;=p9c;^ngaY>3h*8+26))E%hlTgxP0!P{PyCmGRlh zZyAuFMG51xF`{rkna*&;VO%?C-ugd7#uN3d>_{*#f-A=!b0_CF&E79om50SEW zcKn>Rl~H{9dBi(?5slhPdUo;`We?W+xu<;dDW7;U;!Vm->C)qTlK~DCYSz5Vae7=be58AJ{1@mN1;8)-1SnOhun~z%fH3~#Sq|{v^r>X| zm(i%RvlGF8+er+aq?A2q9tm)Oad!mY+I*Vh=TR{zXYnl((kR`fePw5dZ({Rm1MZ&# zh2{hJ@Xsjckq;1R)MSTvxxW*?VjuL_ra9d0ho&L3>Y&AeQJNZ4w_lHCH$rbyB2fSih z4wvigN_!0`mPW-n1u=>5Q2<|lUQP%SJpNn6asN@O@L6uVz$VzlCqhMk#{kg#_NjD- z0;1qgsP?h;B-c9p2VZ`yY@D5ZDJ1(RW!Cv5LT=2TMv7*l5=q6}0O5avjXV%;xfx`K zr~eIJ^dDfKlz&ROzea6k6NCt+Db(u#ly;(c0R*iP=6yJU-O`eN~7A< z%v#Wab}~*MLGX){2lx;INYje-0U_@vUcFng?=^AJ7(|7J9?D`_EB%17(f3I#-pwm8 zR6`?I(l&p8vuLcoW04ZZXcVM#83E*-ptMKToI^+hI*L%Z@Dl7jJmG&R#cw{y>~)N; c_2Gq6sYZic;{3F_Y~eTm06Ls^>9uG80ER^n&j0`b From 3541e779c62a9f4f0d398a7e41316cc1f33018bb Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Thu, 27 Aug 2020 10:57:37 -0500 Subject: [PATCH 071/216] Index pattern class cleanup - remove _.apply and `any` instance (#76004) Index pattern class cleanup - remove _.apply and `any` instance --- ...ins-data-public.fieldlist._constructor_.md | 4 +- ...s-data-public.indexpattern.intervalname.md | 11 ++++ ...plugin-plugins-data-public.indexpattern.md | 3 + ...ugins-data-public.indexpattern.prepbody.md | 18 +++++- ...-data-public.indexpattern.sourcefilters.md | 11 ++++ ...n-plugins-data-public.indexpattern.type.md | 11 ++++ .../index_patterns/fields/field_list.ts | 2 +- .../index_patterns/index_pattern.test.ts | 13 ++++ .../index_patterns/index_pattern.ts | 63 +++++++++++-------- src/plugins/data/public/public.api.md | 23 +++++-- 10 files changed, 122 insertions(+), 37 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.intervalname.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.sourcefilters.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.type.md diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist._constructor_.md index 3b60ac0f48edd2..9f9613a5a68f7c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `FieldList` class Signature: ```typescript -constructor(indexPattern: IndexPattern, specs?: FieldSpec[], shortDotsEnable?: boolean, onNotification?: () => void); +constructor(indexPattern: IndexPattern, specs?: FieldSpec[], shortDotsEnable?: boolean, onNotification?: OnNotification); ``` ## Parameters @@ -19,5 +19,5 @@ constructor(indexPattern: IndexPattern, specs?: FieldSpec[], shortDotsEnable?: b | indexPattern | IndexPattern | | | specs | FieldSpec[] | | | shortDotsEnable | boolean | | -| onNotification | () => void | | +| onNotification | OnNotification | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.intervalname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.intervalname.md new file mode 100644 index 00000000000000..762b4a37bfd289 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.intervalname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [intervalName](./kibana-plugin-plugins-data-public.indexpattern.intervalname.md) + +## IndexPattern.intervalName property + +Signature: + +```typescript +intervalName: string | undefined; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index 649f8ef077e3f6..c15cb3358f689e 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -27,9 +27,12 @@ export declare class IndexPattern implements IIndexPattern | [formatField](./kibana-plugin-plugins-data-public.indexpattern.formatfield.md) | | any | | | [formatHit](./kibana-plugin-plugins-data-public.indexpattern.formathit.md) | | any | | | [id](./kibana-plugin-plugins-data-public.indexpattern.id.md) | | string | | +| [intervalName](./kibana-plugin-plugins-data-public.indexpattern.intervalname.md) | | string | undefined | | | [metaFields](./kibana-plugin-plugins-data-public.indexpattern.metafields.md) | | string[] | | +| [sourceFilters](./kibana-plugin-plugins-data-public.indexpattern.sourcefilters.md) | | SourceFilter[] | | | [timeFieldName](./kibana-plugin-plugins-data-public.indexpattern.timefieldname.md) | | string | undefined | | | [title](./kibana-plugin-plugins-data-public.indexpattern.title.md) | | string | | +| [type](./kibana-plugin-plugins-data-public.indexpattern.type.md) | | string | undefined | | | [typeMeta](./kibana-plugin-plugins-data-public.indexpattern.typemeta.md) | | TypeMeta | | ## Methods diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.prepbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.prepbody.md index 5c9f017b571da7..1d77b2a55860e2 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.prepbody.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.prepbody.md @@ -8,12 +8,26 @@ ```typescript prepBody(): { - [key: string]: any; + title: string; + timeFieldName: string | undefined; + intervalName: string | undefined; + sourceFilters: string | undefined; + fields: string | undefined; + fieldFormatMap: string | undefined; + type: string | undefined; + typeMeta: string | undefined; }; ``` Returns: `{ - [key: string]: any; + title: string; + timeFieldName: string | undefined; + intervalName: string | undefined; + sourceFilters: string | undefined; + fields: string | undefined; + fieldFormatMap: string | undefined; + type: string | undefined; + typeMeta: string | undefined; }` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.sourcefilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.sourcefilters.md new file mode 100644 index 00000000000000..10ccf8e137627a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.sourcefilters.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [sourceFilters](./kibana-plugin-plugins-data-public.indexpattern.sourcefilters.md) + +## IndexPattern.sourceFilters property + +Signature: + +```typescript +sourceFilters?: SourceFilter[]; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.type.md new file mode 100644 index 00000000000000..7a10d058b9c656 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [type](./kibana-plugin-plugins-data-public.indexpattern.type.md) + +## IndexPattern.type property + +Signature: + +```typescript +type: string | undefined; +``` diff --git a/src/plugins/data/common/index_patterns/fields/field_list.ts b/src/plugins/data/common/index_patterns/fields/field_list.ts index 34bd69230a2e46..d2489a5d1f7e33 100644 --- a/src/plugins/data/common/index_patterns/fields/field_list.ts +++ b/src/plugins/data/common/index_patterns/fields/field_list.ts @@ -64,7 +64,7 @@ export class FieldList extends Array implements IIndexPattern indexPattern: IndexPattern, specs: FieldSpec[] = [], shortDotsEnable = false, - onNotification = () => {} + onNotification: OnNotification = () => {} ) { super(); this.indexPattern = indexPattern; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index 09b79cae4aac2a..f7e1156170f032 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -43,8 +43,21 @@ jest.mock('../../field_mapping', () => { id: true, title: true, fieldFormatMap: { + _serialize: jest.fn().mockImplementation(() => {}), _deserialize: jest.fn().mockImplementation(() => []), }, + fields: { + _serialize: jest.fn().mockImplementation(() => {}), + _deserialize: jest.fn().mockImplementation((fields) => fields), + }, + sourceFilters: { + _serialize: jest.fn().mockImplementation(() => {}), + _deserialize: jest.fn().mockImplementation(() => undefined), + }, + typeMeta: { + _serialize: jest.fn().mockImplementation(() => {}), + _deserialize: jest.fn().mockImplementation(() => undefined), + }, })), }; }); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index e81ef1d6b2482e..5d6ae61a77e00d 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -56,14 +56,14 @@ interface IndexPatternDeps { } export class IndexPattern implements IIndexPattern { - [key: string]: any; - public id?: string; public title: string = ''; public fieldFormatMap: any; public typeMeta?: TypeMeta; public fields: IIndexPatternFieldList & { toSpec: () => FieldSpec[] }; public timeFieldName: string | undefined; + public intervalName: string | undefined; + public type: string | undefined; public formatHit: any; public formatField: any; public flattenHit: any; @@ -72,7 +72,7 @@ export class IndexPattern implements IIndexPattern { private version: string | undefined; private savedObjectsClient: SavedObjectsClientCommon; private patternCache: PatternCache; - private sourceFilters?: SourceFilter[]; + public sourceFilters?: SourceFilter[]; private originalBody: { [key: string]: any } = {}; public fieldsFetcher: any; // probably want to factor out any direct usage and change to private private shortDotsEnable: boolean = false; @@ -126,7 +126,7 @@ export class IndexPattern implements IIndexPattern { this.shortDotsEnable = shortDotsEnable; this.metaFields = metaFields; - this.fields = new FieldList(this, [], this.shortDotsEnable, this.onUnknownType); + this.fields = new FieldList(this, [], this.shortDotsEnable, this.onNotification); this.apiClient = apiClient; this.fieldsFetcher = createFieldsFetcher(this, apiClient, metaFields); @@ -226,10 +226,13 @@ export class IndexPattern implements IIndexPattern { response[name] = fieldMapping._deserialize(response[name]); }); - // give index pattern all of the values - const fieldList = this.fields; - _.assign(this, response); - this.fields = fieldList; + this.title = response.title; + this.timeFieldName = response.timeFieldName; + this.intervalName = response.intervalName; + this.sourceFilters = response.sourceFilters; + this.fieldFormatMap = response.fieldFormatMap; + this.type = response.type; + this.typeMeta = response.typeMeta; if (!this.title && this.id) { this.title = this.id; @@ -430,18 +433,16 @@ export class IndexPattern implements IIndexPattern { } prepBody() { - const body: { [key: string]: any } = {}; - - // serialize json fields - _.forOwn(this.mapping, (fieldMapping, fieldName) => { - if (!fieldName || this[fieldName] == null) return; - - body[fieldName] = fieldMapping._serialize - ? fieldMapping._serialize(this[fieldName]) - : this[fieldName]; - }); - - return body; + return { + title: this.title, + timeFieldName: this.timeFieldName, + intervalName: this.intervalName, + sourceFilters: this.mapping.sourceFilters._serialize!(this.sourceFilters), + fields: this.mapping.fields._serialize!(this.fields), + fieldFormatMap: this.mapping.fieldFormatMap._serialize!(this.fieldFormatMap), + type: this.type, + typeMeta: this.mapping.typeMeta._serialize!(this.mapping), + }; } getFormatterForField(field: IndexPatternField | IndexPatternField['spec']): FieldFormat { @@ -485,10 +486,14 @@ export class IndexPattern implements IIndexPattern { async save(saveAttempts: number = 0): Promise { if (!this.id) return; const body = this.prepBody(); - // What keys changed since they last pulled the index pattern - const originalChangedKeys = Object.keys(body).filter( - (key) => body[key] !== this.originalBody[key] - ); + + const originalChangedKeys: string[] = []; + Object.entries(body).forEach(([key, value]) => { + if (value !== this.originalBody[key]) { + originalChangedKeys.push(key); + } + }); + return this.savedObjectsClient .update(savedObjectType, this.id, body, { version: this.version }) .then((resp) => { @@ -519,8 +524,12 @@ export class IndexPattern implements IIndexPattern { // and ensure we ignore the key if the server response // is the same as the original response (since that is expected // if we made a change in that key) - const serverChangedKeys = Object.keys(updatedBody).filter((key) => { - return updatedBody[key] !== body[key] && this.originalBody[key] !== updatedBody[key]; + + const serverChangedKeys: string[] = []; + Object.entries(updatedBody).forEach(([key, value]) => { + if (value !== (body as any)[key] && value !== this.originalBody[key]) { + serverChangedKeys.push(key); + } }); let unresolvedCollision = false; @@ -545,7 +554,7 @@ export class IndexPattern implements IIndexPattern { // Set the updated response on this object serverChangedKeys.forEach((key) => { - this[key] = samePattern[key]; + (this as any)[key] = (samePattern as any)[key]; }); this.version = samePattern.version; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 261f16229460ac..9a2a82e8ed2069 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -604,7 +604,8 @@ export type FieldFormatsGetConfigFn = GetConfigFn; // @public (undocumented) export class FieldList extends Array implements IIndexPatternFieldList { // Warning: (ae-forgotten-export) The symbol "FieldSpec" needs to be exported by the entry point index.d.ts - constructor(indexPattern: IndexPattern, specs?: FieldSpec[], shortDotsEnable?: boolean, onNotification?: () => void); + // Warning: (ae-forgotten-export) The symbol "OnNotification" needs to be exported by the entry point index.d.ts + constructor(indexPattern: IndexPattern, specs?: FieldSpec[], shortDotsEnable?: boolean, onNotification?: OnNotification); // (undocumented) readonly add: (field: FieldSpec) => void; // (undocumented) @@ -946,8 +947,6 @@ export class IndexPattern implements IIndexPattern { // Warning: (ae-forgotten-export) The symbol "IndexPatternDeps" needs to be exported by the entry point index.d.ts constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, shortDotsEnable, metaFields, }: IndexPatternDeps); // (undocumented) - [key: string]: any; - // (undocumented) addScriptedField(name: string, script: string, fieldType: string | undefined, lang: string): Promise; // (undocumented) create(allowOverride?: boolean): Promise; @@ -1008,6 +1007,8 @@ export class IndexPattern implements IIndexPattern { // (undocumented) initFromSpec(spec: IndexPatternSpec): this; // (undocumented) + intervalName: string | undefined; + // (undocumented) isTimeBased(): boolean; // (undocumented) isTimeBasedWildcard(): boolean; @@ -1021,7 +1022,14 @@ export class IndexPattern implements IIndexPattern { popularizeField(fieldName: string, unit?: number): Promise; // (undocumented) prepBody(): { - [key: string]: any; + title: string; + timeFieldName: string | undefined; + intervalName: string | undefined; + sourceFilters: string | undefined; + fields: string | undefined; + fieldFormatMap: string | undefined; + type: string | undefined; + typeMeta: string | undefined; }; // (undocumented) refreshFields(): Promise; @@ -1029,6 +1037,10 @@ export class IndexPattern implements IIndexPattern { removeScriptedField(fieldName: string): Promise; // (undocumented) save(saveAttempts?: number): Promise; + // Warning: (ae-forgotten-export) The symbol "SourceFilter" needs to be exported by the entry point index.d.ts + // + // (undocumented) + sourceFilters?: SourceFilter[]; // (undocumented) timeFieldName: string | undefined; // (undocumented) @@ -1040,6 +1052,8 @@ export class IndexPattern implements IIndexPattern { // (undocumented) toString(): string; // (undocumented) + type: string | undefined; + // (undocumented) typeMeta?: IndexPatternTypeMeta; } @@ -1081,7 +1095,6 @@ export interface IndexPatternAttributes { // // @public (undocumented) export class IndexPatternField implements IFieldType { - // Warning: (ae-forgotten-export) The symbol "OnNotification" needs to be exported by the entry point index.d.ts constructor(indexPattern: IndexPattern, spec: FieldSpec, displayName: string, onNotification: OnNotification); // (undocumented) get aggregatable(): boolean; From d556c79481e7fa19b3644f8e925ea0236e237a44 Mon Sep 17 00:00:00 2001 From: Bhavya RM Date: Thu, 27 Aug 2020 12:03:53 -0400 Subject: [PATCH 072/216] Test user assignment to maps tests - 2 (#75890) and removing unused data from fullscreen maps.js --- x-pack/test/functional/apps/maps/add_layer_panel.js | 6 ++++++ .../test/functional/apps/maps/blended_vector_layer.js | 10 ++++++++++ x-pack/test/functional/apps/maps/full_screen_mode.js | 7 +++++-- x-pack/test/functional/apps/maps/layer_visibility.js | 3 +++ .../functional/apps/maps/saved_object_management.js | 9 +++++++++ x-pack/test/functional/apps/maps/vector_styling.js | 7 ++++++- 6 files changed, 39 insertions(+), 3 deletions(-) diff --git a/x-pack/test/functional/apps/maps/add_layer_panel.js b/x-pack/test/functional/apps/maps/add_layer_panel.js index 3902b616cf1ee1..9eb560ed42c312 100644 --- a/x-pack/test/functional/apps/maps/add_layer_panel.js +++ b/x-pack/test/functional/apps/maps/add_layer_panel.js @@ -9,17 +9,23 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['maps']); + const security = getService('security'); describe('Add layer panel', () => { const LAYER_NAME = 'World Countries'; before(async () => { + await security.testUser.setRoles(['global_maps_all']); await PageObjects.maps.openNewMap(); await PageObjects.maps.clickAddLayer(); await PageObjects.maps.selectEMSBoundariesSource(); await PageObjects.maps.selectVectorLayer(LAYER_NAME); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show unsaved layer in layer TOC', async () => { const vectorLayerExists = await PageObjects.maps.doesLayerExist(LAYER_NAME); expect(vectorLayerExists).to.be(true); diff --git a/x-pack/test/functional/apps/maps/blended_vector_layer.js b/x-pack/test/functional/apps/maps/blended_vector_layer.js index 9658cb37291343..9793d4b8f03d3c 100644 --- a/x-pack/test/functional/apps/maps/blended_vector_layer.js +++ b/x-pack/test/functional/apps/maps/blended_vector_layer.js @@ -9,12 +9,22 @@ import expect from '@kbn/expect'; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); const inspector = getService('inspector'); + const security = getService('security'); describe('blended vector layer', () => { before(async () => { + await security.testUser.setRoles(['test_logstash_reader', 'global_maps_all']); await PageObjects.maps.loadSavedMap('blended document example'); }); + afterEach(async () => { + await inspector.close(); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should request documents when zoomed to smaller regions showing less data', async () => { const hits = await PageObjects.maps.getHits(); expect(hits).to.equal('33'); diff --git a/x-pack/test/functional/apps/maps/full_screen_mode.js b/x-pack/test/functional/apps/maps/full_screen_mode.js index b4ea2b0baf255c..a114826f564bb6 100644 --- a/x-pack/test/functional/apps/maps/full_screen_mode.js +++ b/x-pack/test/functional/apps/maps/full_screen_mode.js @@ -9,13 +9,16 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['maps', 'common']); const retry = getService('retry'); - const esArchiver = getService('esArchiver'); + const security = getService('security'); describe('maps full screen mode', () => { before(async () => { - await esArchiver.loadIfNeeded('maps/data'); + await security.testUser.setRoles(['global_maps_all']); await PageObjects.maps.openNewMap(); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); it('full screen button should exist', async () => { const exists = await PageObjects.maps.fullScreenModeMenuItemExists(); diff --git a/x-pack/test/functional/apps/maps/layer_visibility.js b/x-pack/test/functional/apps/maps/layer_visibility.js index 22cff6de416c1f..dd9b93c995695b 100644 --- a/x-pack/test/functional/apps/maps/layer_visibility.js +++ b/x-pack/test/functional/apps/maps/layer_visibility.js @@ -9,14 +9,17 @@ import expect from '@kbn/expect'; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); const inspector = getService('inspector'); + const security = getService('security'); describe('layer visibility', () => { before(async () => { + await security.testUser.setRoles(['test_logstash_reader', 'global_maps_all']); await PageObjects.maps.loadSavedMap('document example hidden'); }); afterEach(async () => { await inspector.close(); + await security.testUser.restoreDefaults(); }); it('should not make any requests when layer is hidden', async () => { diff --git a/x-pack/test/functional/apps/maps/saved_object_management.js b/x-pack/test/functional/apps/maps/saved_object_management.js index 810df8e9950643..277a8a5651453d 100644 --- a/x-pack/test/functional/apps/maps/saved_object_management.js +++ b/x-pack/test/functional/apps/maps/saved_object_management.js @@ -12,6 +12,7 @@ export default function ({ getPageObjects, getService }) { const filterBar = getService('filterBar'); const browser = getService('browser'); const inspector = getService('inspector'); + const security = getService('security'); describe('map saved object management', () => { const MAP_NAME_PREFIX = 'saved_object_management_test_'; @@ -20,8 +21,16 @@ export default function ({ getPageObjects, getService }) { describe('read', () => { before(async () => { + await security.testUser.setRoles([ + 'global_maps_all', + 'geoshape_data_reader', + 'test_logstash_reader', + ]); await PageObjects.maps.loadSavedMap('join example'); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); it('should update global Kibana time to value stored with map', async () => { const timeConfig = await PageObjects.timePicker.getTimeConfig(); diff --git a/x-pack/test/functional/apps/maps/vector_styling.js b/x-pack/test/functional/apps/maps/vector_styling.js index 29c01918165cf1..1def542982dd8e 100644 --- a/x-pack/test/functional/apps/maps/vector_styling.js +++ b/x-pack/test/functional/apps/maps/vector_styling.js @@ -6,13 +6,18 @@ import expect from '@kbn/expect'; -export default function ({ getPageObjects }) { +export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['maps']); + const security = getService('security'); describe('vector styling', () => { before(async () => { + await security.testUser.setRoles(['test_logstash_reader', 'global_maps_all']); await PageObjects.maps.loadSavedMap('document example'); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); describe('categorical styling', () => { before(async () => { From 51fd423689faa64fc08de43c379e4afdd1c4b711 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 27 Aug 2020 12:42:24 -0400 Subject: [PATCH 073/216] [Docs] Fix typo in docs for `server.xsrf.disableProtection` (#76102) --- docs/setup/settings.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index e1fb1802b2a210..4a931aabd3646d 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -592,7 +592,7 @@ The `server.xsrf.whitelist` setting requires the following format: [cols="2*<"] |=== -| [[settings-xsrf-disableProtection]] `status.xsrf.disableProtection:` +| [[settings-xsrf-disableProtection]] `server.xsrf.disableProtection:` | Setting this to `true` will completely disable Cross-site request forgery protection in Kibana. This is not recommended. *Default: `false`* | `status.allowAnonymous:` From 12f4f6d74ac8986bf59ab6721fa06ab8c4c671e8 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 27 Aug 2020 10:53:16 -0600 Subject: [PATCH 074/216] [Maps] fix read only badge is no longer shown in nav for users with read-only permission (#76091) --- .../maps/public/routing/maps_router.js | 21 ++++++++++++++++++- .../maps/feature_controls/maps_security.ts | 3 +-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/maps/public/routing/maps_router.js b/x-pack/plugins/maps/public/routing/maps_router.js index 9b7900d032f5a6..f0f5234e3f9898 100644 --- a/x-pack/plugins/maps/public/routing/maps_router.js +++ b/x-pack/plugins/maps/public/routing/maps_router.js @@ -7,7 +7,14 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Router, Switch, Route, Redirect } from 'react-router-dom'; -import { getCoreI18n, getToasts, getEmbeddableService } from '../kibana_services'; +import { i18n } from '@kbn/i18n'; +import { + getCoreChrome, + getCoreI18n, + getMapsCapabilities, + getToasts, + getEmbeddableService, +} from '../kibana_services'; import { createKbnUrlStateStorage, withNotifyOnErrors, @@ -44,6 +51,18 @@ const App = ({ history, appBasePath, onAppLeave }) => { const { originatingApp } = stateTransfer?.getIncomingEditorState({ keysToRemoveAfterFetch: ['originatingApp'] }) || {}; + if (!getMapsCapabilities().save) { + getCoreChrome().setBadge({ + text: i18n.translate('xpack.maps.badge.readOnly.text', { + defaultMessage: 'Read only', + }), + tooltip: i18n.translate('xpack.maps.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save maps', + }), + iconType: 'glasses', + }); + } + return ( diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts index f480f1f0ae24af..ae9b0f095fc44c 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts @@ -181,8 +181,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await testSubjects.missingOrFail('checkboxSelectAll'); }); - // This behavior was removed when the Maps app was migrated to NP - it.skip(`shows read-only badge`, async () => { + it(`shows read-only badge`, async () => { await globalNav.badgeExistsOrFail('Read only'); }); From 2010ec6ac4ba518fd032afb044f6ea0b47e3f587 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 27 Aug 2020 18:22:53 +0100 Subject: [PATCH 075/216] fix(NA): keystore read path on serve (#75659) Co-authored-by: Elastic Machine --- src/cli/serve/serve.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 345156b2491a12..c08f3aec64335b 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -149,7 +149,7 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { ); merge(extraCliOptions); - merge(readKeystore(get('path.data'))); + merge(readKeystore()); return rawConfig; } From ebfba81ba54b4b25f07a98fc3eefe7a7927a3e02 Mon Sep 17 00:00:00 2001 From: Brent Kimmel Date: Thu, 27 Aug 2020 13:25:01 -0400 Subject: [PATCH 076/216] [Security Solution][Resolver] break/wrap for process detail (#76095) * [Security Solution][Resolver]break/wrap for process detail * add an enzyme test to check for the breakers --- .../public/resolver/mocks/resolver_tree.ts | 2 +- .../public/resolver/view/panel.test.tsx | 17 ++++++++-- .../view/panels/panel_content_utilities.tsx | 32 +++++++++++++++++++ .../resolver/view/panels/process_details.tsx | 16 +++++++--- .../view/panels/related_event_detail.tsx | 30 +---------------- 5 files changed, 60 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts index 7edf4f8071ed8c..8bd5953e9cb411 100644 --- a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts +++ b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts @@ -177,7 +177,7 @@ export function mockTreeWithNoAncestorsAnd2Children({ const origin: ResolverEvent = mockEndpointEvent({ pid: 0, entityID: originID, - name: 'c', + name: 'c.ext', parentEntityId: 'none', timestamp: 0, }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx index 037337fb2f868e..1add907ae933d4 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx @@ -81,7 +81,7 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an }; }) ).toYieldEqualTo({ - title: 'c', + title: 'c.ext', titleIcon: 'Running Process', detailEntries: [ ['process.executable', 'executable'], @@ -94,6 +94,19 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an ], }); }); + it('should have breaking opportunities (s) in node titles to allow wrapping', async () => { + await expect( + simulator().map(() => { + const titleWrapper = simulator().testSubject('resolver:node-detail:title'); + return { + wordBreaks: titleWrapper.find('wbr').length, + }; + }) + ).toYieldEqualTo({ + // The GeneratedText component adds 1 after the period and one at the end + wordBreaks: 2, + }); + }); }); const queryStringWithFirstChildSelected = urlSearch(resolverComponentInstanceID, { @@ -174,7 +187,7 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an .testSubject('resolver:node-list:node-link:title') .map((node) => node.text()); }) - ).toYieldEqualTo(['c', 'd', 'e']); + ).toYieldEqualTo(['c.ext', 'd', 'e']); }); }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx index 5c7a4a476efbab..19f0aa3fe1d678 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx @@ -43,6 +43,38 @@ const betaBadgeLabel = i18n.translate( } ); +/** + * A component that renders an element with breaking opportunities (``s) + * spliced into text children at word boundaries. + */ +export const GeneratedText = React.memo(function ({ children }) { + return <>{processedValue()}; + + function processedValue() { + return React.Children.map(children, (child) => { + if (typeof child === 'string') { + const valueSplitByWordBoundaries = child.split(/\b/); + + if (valueSplitByWordBoundaries.length < 2) { + return valueSplitByWordBoundaries[0]; + } + + return [ + valueSplitByWordBoundaries[0], + ...valueSplitByWordBoundaries + .splice(1) + .reduce(function (generatedTextMemo: Array, value, index) { + return [...generatedTextMemo, value, ]; + }, []), + ]; + } else { + return child; + } + }); + } +}); +GeneratedText.displayName = 'GeneratedText'; + /** * A component to keep time representations in blocks so they don't wrap * and look bad. diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx index 15711909c4c9be..e86e3e6baf4a4a 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx @@ -19,7 +19,7 @@ import { FormattedMessage } from 'react-intl'; import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list'; import * as selectors from '../../store/selectors'; import * as event from '../../../../common/endpoint/models/event'; -import { formatDate, StyledBreadcrumbs } from './panel_content_utilities'; +import { formatDate, StyledBreadcrumbs, GeneratedText } from './panel_content_utilities'; import { processPath, processPid, @@ -39,6 +39,10 @@ const StyledDescriptionList = styled(EuiDescriptionList)` } `; +const StyledTitle = styled('h4')` + overflow-wrap: break-word; +`; + /** * A description list view of all the Metadata that goes with a particular process event, like: * Created, PID, User/Domain, etc. @@ -114,7 +118,7 @@ export const ProcessDetails = memo(function ProcessDetails({ .map((entry) => { return { ...entry, - description: String(entry.description), + description: {String(entry.description)}, }; }); @@ -163,13 +167,15 @@ export const ProcessDetails = memo(function ProcessDetails({ -

+ - {processName} -

+ + {processName} + +
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx index da4cd3c9dacade..6aacf91c561782 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx @@ -10,7 +10,7 @@ import { EuiSpacer, EuiText, EuiDescriptionList, EuiTextColor, EuiTitle } from ' import styled from 'styled-components'; import { useSelector } from 'react-redux'; import { FormattedMessage } from 'react-intl'; -import { StyledBreadcrumbs, BoldCode, StyledTime } from './panel_content_utilities'; +import { StyledBreadcrumbs, BoldCode, StyledTime, GeneratedText } from './panel_content_utilities'; import * as event from '../../../../common/endpoint/models/event'; import { ResolverEvent } from '../../../../common/endpoint/types'; import * as selectors from '../../store/selectors'; @@ -57,34 +57,6 @@ const TitleHr = memo(() => { }); TitleHr.displayName = 'TitleHR'; -const GeneratedText = React.memo(function ({ children }) { - return <>{processedValue()}; - - function processedValue() { - return React.Children.map(children, (child) => { - if (typeof child === 'string') { - const valueSplitByWordBoundaries = child.split(/\b/); - - if (valueSplitByWordBoundaries.length < 2) { - return valueSplitByWordBoundaries[0]; - } - - return [ - valueSplitByWordBoundaries[0], - ...valueSplitByWordBoundaries - .splice(1) - .reduce(function (generatedTextMemo: Array, value, index) { - return [...generatedTextMemo, value, ]; - }, []), - ]; - } else { - return child; - } - }); - } -}); -GeneratedText.displayName = 'GeneratedText'; - /** * Take description list entries and prepare them for display by * seeding with `` tags. From 165752b05f2e2e847c3204a762a44c430f58ebac Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 27 Aug 2020 10:39:44 -0700 Subject: [PATCH 077/216] [DOCS] Add monitoring.ui.logs.index (#75830) --- docs/settings/monitoring-settings.asciidoc | 43 ++++++++++++---------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 5b8fa0725d96b3..d538519eefcc43 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -5,12 +5,12 @@ Monitoring settings ++++ -By default, the Monitoring application is enabled, but data collection -is disabled. When you first start {kib} monitoring, you are prompted to -enable data collection. If you are using {stack-security-features}, you must be -signed in as a user with the `cluster:manage` privilege to enable -data collection. The built-in `superuser` role has this privilege and the -built-in `elastic` user has this role. +By default, *{stack-monitor-app}* is enabled, but data collection is disabled. +When you first start {kib} monitoring, you are prompted to enable data +collection. If you are using {stack-security-features}, you must be signed in as +a user with the `cluster:manage` privilege to enable data collection. The +built-in `superuser` role has this privilege and the built-in `elastic` user has +this role. You can adjust how monitoring data is collected from {kib} and displayed in {kib} by configuring settings in the @@ -49,7 +49,7 @@ For more information, see in {kib} to the {es} monitoring cluster and to verify licensing status on the {es} monitoring cluster. + + - Every other request performed by the Stack Monitoring UI to the monitoring {es} + Every other request performed by *{stack-monitor-app}* to the monitoring {es} cluster uses the authenticated user's credentials, which must be the same on both the {es} monitoring cluster and the {es} production cluster. + + @@ -60,7 +60,7 @@ For more information, see in {kib} to the {es} monitoring cluster and to verify licensing status on the {es} monitoring cluster. + + - Every other request performed by the Stack Monitoring UI to the monitoring {es} + Every other request performed by *{stack-monitor-app}* to the monitoring {es} cluster uses the authenticated user's credentials, which must be the same on both the {es} monitoring cluster and the {es} production cluster. + + @@ -83,7 +83,7 @@ These settings control how data is collected from {kib}. |=== | `monitoring.kibana.collection.enabled` | Set to `true` (default) to enable data collection from the {kib} NodeJS server - for {kib} Dashboards to be featured in the Monitoring. + for {kib} dashboards to be featured in *{stack-monitor-app}*. | `monitoring.kibana.collection.interval` | Specifies the number of milliseconds to wait in between data sampling on the @@ -96,16 +96,26 @@ These settings control how data is collected from {kib}. [[monitoring-ui-settings]] ==== Monitoring UI settings -These settings adjust how the {kib} Monitoring page displays monitoring data. +These settings adjust how *{stack-monitor-app}* displays monitoring data. However, the defaults work best in most circumstances. For more information about configuring {kib}, see -{kibana-ref}/settings.html[Setting Kibana Server Properties]. +{kibana-ref}/settings.html[Setting {kib} server properties]. [cols="2*<"] |=== | `monitoring.ui.elasticsearch.logFetchCount` - | Specifies the number of log entries to display in the Monitoring UI. Defaults to - `10`. The maximum value is `50`. + | Specifies the number of log entries to display in *{stack-monitor-app}*. + Defaults to `10`. The maximum value is `50`. + +| `monitoring.ui.enabled` + | Set to `false` to hide *{stack-monitor-app}*. The monitoring back-end + continues to run as an agent for sending {kib} stats to the monitoring + cluster. Defaults to `true`. + +| `monitoring.ui.logs.index` + | Specifies the name of the indices that are shown on the + <> page in *{stack-monitor-app}*. The default value + is `filebeat-*`. | `monitoring.ui.max_bucket_size` | Specifies the number of term buckets to return out of the overall terms list when @@ -120,18 +130,13 @@ about configuring {kib}, see `monitoring.ui.collection.interval` in `elasticsearch.yml`, use the same value in this setting. -| `monitoring.ui.enabled` - | Set to `false` to hide the Monitoring UI in {kib}. The monitoring back-end - continues to run as an agent for sending {kib} stats to the monitoring - cluster. Defaults to `true`. - |=== [float] [[monitoring-ui-cgroup-settings]] ===== Monitoring UI container settings -The Monitoring UI exposes the Cgroup statistics that we collect for you to make +*{stack-monitor-app}* exposes the Cgroup statistics that we collect for you to make better decisions about your container performance, rather than guessing based on the overall machine performance. If you are not running your applications in a container, then Cgroup statistics are not useful. From c31acce6499932fd39babcb4a984f58260df4239 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 27 Aug 2020 13:54:09 -0400 Subject: [PATCH 078/216] Fix more broken usages of `bulkCreate` (#76005) --- .../tutorial/saved_objects_installer.js | 9 +++++--- .../tutorial/saved_objects_installer.test.js | 21 +++++++++++++++++++ .../models/data_recognizer/data_recognizer.ts | 7 ++++--- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/plugins/home/public/application/components/tutorial/saved_objects_installer.js b/src/plugins/home/public/application/components/tutorial/saved_objects_installer.js index 790c6d9c2574e3..dd63827c38c5d2 100644 --- a/src/plugins/home/public/application/components/tutorial/saved_objects_installer.js +++ b/src/plugins/home/public/application/components/tutorial/saved_objects_installer.js @@ -62,9 +62,12 @@ class SavedObjectsInstallerUi extends React.Component { let resp; try { - resp = await this.props.bulkCreate(this.props.savedObjects, { - overwrite: this.state.overwrite, - }); + // Filter out the saved object version field, if present, to avoid inadvertently triggering optimistic concurrency control. + const objectsToCreate = this.props.savedObjects.map( + // eslint-disable-next-line no-unused-vars + ({ version, ...savedObject }) => savedObject + ); + resp = await this.props.bulkCreate(objectsToCreate, { overwrite: this.state.overwrite }); } catch (error) { if (!this._isMounted) { return; diff --git a/src/plugins/home/public/application/components/tutorial/saved_objects_installer.test.js b/src/plugins/home/public/application/components/tutorial/saved_objects_installer.test.js index 6cc02184fbc163..e7b7d8ed1d7fd5 100644 --- a/src/plugins/home/public/application/components/tutorial/saved_objects_installer.test.js +++ b/src/plugins/home/public/application/components/tutorial/saved_objects_installer.test.js @@ -79,4 +79,25 @@ describe('bulkCreate', () => { expect(component).toMatchSnapshot(); }); + + test('should filter out saved object version before calling bulkCreate', async () => { + const bulkCreateMock = jest.fn().mockResolvedValue({ + savedObjects: [savedObject], + }); + const component = mountWithIntl( + + ); + + findTestSubject(component, 'loadSavedObjects').simulate('click'); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(bulkCreateMock).toHaveBeenCalledWith([savedObject], expect.any(Object)); + }); }); diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index cc42a545c11e2a..206baacd983223 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -20,6 +20,7 @@ import { getAuthorizationHeader } from '../../lib/request_authorization'; import { MlInfoResponse } from '../../../common/types/ml_server_info'; import { KibanaObjects, + KibanaObjectConfig, ModuleDataFeed, ModuleJob, Module, @@ -100,7 +101,7 @@ interface ObjectExistResponse { id: string; type: string; exists: boolean; - savedObject?: any; + savedObject?: { id: string; type: string; attributes: KibanaObjectConfig }; } interface SaveResults { @@ -678,14 +679,14 @@ export class DataRecognizer { let results = { saved_objects: [] as any[] }; const filteredSavedObjects = objectExistResults .filter((o) => o.exists === false) - .map((o) => o.savedObject); + .map((o) => o.savedObject!); if (filteredSavedObjects.length) { results = await this.savedObjectsClient.bulkCreate( // Add an empty migrationVersion attribute to each saved object to ensure // it is automatically migrated to the 7.0+ format with a references attribute. filteredSavedObjects.map((doc) => ({ ...doc, - migrationVersion: doc.migrationVersion || {}, + migrationVersion: {}, })) ); } From 1bd8f4127531a5b0426416b4ee264f1af6166a1f Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Thu, 27 Aug 2020 13:50:15 -0500 Subject: [PATCH 079/216] [ML] Add indicator if there are stopped partitions in categorization job wizard (#75709) --- .../common/results_loader/results_loader.ts | 4 + .../category_stopped_partitions.tsx | 137 ++++++++++++++++++ .../metric_selection_summary.tsx | 2 + 3 files changed, 143 insertions(+) create mode 100644 x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts index 110b031cd1dc07..2b250b96222862 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts @@ -126,6 +126,10 @@ export class ResultsLoader { this._results$.next(this._results); } + public get results$() { + return this._results$; + } + public subscribeToResults(func: ResultsSubscriber) { return this._results$.subscribe(func); } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx new file mode 100644 index 00000000000000..5e28a2f7c69757 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx @@ -0,0 +1,137 @@ +/* + * 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, useContext, useEffect, useState, useMemo, useCallback } from 'react'; +import { EuiBasicTable, EuiCallOut, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { from } from 'rxjs'; +import { switchMap, takeWhile, tap } from 'rxjs/operators'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { CategorizationJobCreator } from '../../../../../common/job_creator'; +import { ml } from '../../../../../../../services/ml_api_service'; +import { extractErrorProperties } from '../../../../../../../../../common/util/errors'; + +const NUMBER_OF_PREVIEW = 5; +export const CategoryStoppedPartitions: FC = () => { + const { jobCreator: jc, resultsLoader } = useContext(JobCreatorContext); + const jobCreator = jc as CategorizationJobCreator; + const [tableRow, setTableRow] = useState>([]); + const [stoppedPartitionsError, setStoppedPartitionsError] = useState(); + + const columns = useMemo( + () => [ + { + field: 'partitionName', + name: i18n.translate( + 'xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsPreviewColumnName', + { + defaultMessage: 'Stopped partition names', + } + ), + render: (partition: any) => ( + + {partition} + + ), + }, + ], + [] + ); + + const loadCategoryStoppedPartitions = useCallback(async () => { + try { + const { jobs } = await ml.results.getCategoryStoppedPartitions([jobCreator.jobId]); + + if ( + !Array.isArray(jobs) && // if jobs is object of jobId: [partitions] + Array.isArray(jobs[jobCreator.jobId]) && + jobs[jobCreator.jobId].length > 0 + ) { + return jobs[jobCreator.jobId]; + } + } catch (e) { + const error = extractErrorProperties(e); + // might get 404 because job has not been created yet and that's ok + if (error.statusCode !== 404) { + setStoppedPartitionsError(error.message); + } + } + }, [jobCreator.jobId]); + + useEffect(() => { + // only need to run this check if jobCreator.perPartitionStopOnWarn is turned on + if (jobCreator.perPartitionCategorization && jobCreator.perPartitionStopOnWarn) { + // subscribe to result updates + const resultsSubscription = resultsLoader.results$ + .pipe( + switchMap(() => { + return from(loadCategoryStoppedPartitions()); + }), + tap((results) => { + if (Array.isArray(results)) { + setTableRow( + results.slice(0, NUMBER_OF_PREVIEW).map((partitionName) => ({ + partitionName, + })) + ); + } + }), + takeWhile((results) => { + return !results || (Array.isArray(results) && results.length <= NUMBER_OF_PREVIEW); + }) + ) + .subscribe(); + return () => resultsSubscription.unsubscribe(); + } + }, []); + + return ( + <> + {stoppedPartitionsError && ( + <> + + + } + /> + + )} + {Array.isArray(tableRow) && tableRow.length > 0 && ( + <> + +
+ +
+ + + } + /> + + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx index 768d8c394fb8f4..9f66fb95b53a83 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx @@ -11,6 +11,7 @@ import { Results, Anomaly } from '../../../../../common/results_loader'; import { LineChartPoint } from '../../../../../common/chart_loader'; import { EventRateChart } from '../../../charts/event_rate_chart'; import { TopCategories } from './top_categories'; +import { CategoryStoppedPartitions } from './category_stopped_partitions'; const DTR_IDX = 0; @@ -73,6 +74,7 @@ export const CategorizationDetectorsSummary: FC = () => { fadeChart={jobIsRunning} /> + ); }; From a7b0f7a102f4785d6c9cefbbdc07c1049087dfe0 Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 27 Aug 2020 12:03:20 -0700 Subject: [PATCH 080/216] [Enterprise Search] Add reusable FlashMessages helper (#75901) * Set up basic shared FlashMessages & FlashMessagesLogic * Add top-level FlashMessagesProvider and history listener - This ensures that: - Our FlashMessagesLogic is a global state that persists throughout the entire app and only unmounts when the app itself does (allowing for persistent messages if needed) - history.listen enables the same behavior as previously, where flash messages would be cleared between page views * Set up queued messages that appear on page nav/load * [AS] Add FlashMessages component to Engines Overview + add Kea/Redux context/state to mountWithContext (in order for tests to pass) * Fix missing type exports, replace previous IFlashMessagesProps * [WS] Remove flashMessages state in OverviewLogic - in favor of either connecting it or using FlashMessagesLogic directly in the future * PR feedback: DRY out EUI callout color type def * PR Feedback: make flashMessages method names more explicit * PR Feedback: Shorter FlashMessagesLogic type names * PR feedback: Typing Co-authored-by: Byron Hulcher Co-authored-by: Byron Hulcher --- .../__mocks__/mount_with_context.mock.tsx | 9 +- .../engine_overview/engine_overview.tsx | 2 + .../public/applications/index.tsx | 2 + .../flash_messages/flash_messages.test.tsx | 64 +++++++++ .../shared/flash_messages/flash_messages.tsx | 43 ++++++ .../flash_messages_logic.test.ts | 136 ++++++++++++++++++ .../flash_messages/flash_messages_logic.ts | 87 +++++++++++ .../flash_messages_provider.test.tsx | 46 ++++++ .../flash_messages_provider.tsx | 30 ++++ .../shared/flash_messages/index.ts | 14 ++ .../public/applications/shared/types.ts | 9 +- .../overview/__mocks__/overview_logic.mock.ts | 1 - .../views/overview/overview_logic.test.ts | 9 -- .../views/overview/overview_logic.ts | 11 +- 14 files changed, 434 insertions(+), 29 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx index 9f8fda856eed63..826e0482acef73 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx @@ -8,6 +8,10 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { mount, ReactWrapper } from 'enzyme'; +import { Provider } from 'react-redux'; +import { Store } from 'redux'; +import { getContext, resetContext } from 'kea'; + import { I18nProvider } from '@kbn/i18n/react'; import { KibanaContext } from '../'; import { mockKibanaContext } from './kibana_context.mock'; @@ -24,11 +28,14 @@ import { mockLicenseContext } from './license_context.mock'; * const wrapper = mountWithContext(, { config: { host: 'someOverride' } }); */ export const mountWithContext = (children: React.ReactNode, context?: object) => { + resetContext({ createStore: true }); + const store = getContext().store as Store; + return mount( - {children} + {children} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx index 74bcd9aeafb289..c3b47b2b585bd4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx @@ -16,6 +16,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; +import { FlashMessages } from '../../../shared/flash_messages'; import { LicenseContext, ILicenseContext, hasPlatinumLicense } from '../../../shared/licensing'; import { KibanaContext, IKibanaContext } from '../../../index'; @@ -88,6 +89,7 @@ export const EngineOverview: React.FC = () => { +

diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 60e4cedf413f2e..a54295548004a8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -22,6 +22,7 @@ import { } from 'src/core/public'; import { ClientConfigType, ClientData, PluginsSetup } from '../plugin'; import { LicenseProvider } from './shared/licensing'; +import { FlashMessagesProvider } from './shared/flash_messages'; import { HttpProvider } from './shared/http'; import { IExternalUrl } from './shared/enterprise_search_url'; import { IInitialAppData } from '../../common/types'; @@ -69,6 +70,7 @@ export const renderApp = ( + diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx new file mode 100644 index 00000000000000..59bb7ee5b96252 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx @@ -0,0 +1,64 @@ +/* + * 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 '../../__mocks__/kea.mock'; + +import { useValues } from 'kea'; +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiCallOut } from '@elastic/eui'; + +import { FlashMessages } from './'; + +describe('FlashMessages', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('does not render if no messages exist', () => { + (useValues as jest.Mock).mockImplementationOnce(() => ({ messages: [] })); + + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('renders an array of flash messages & types', () => { + const mockMessages = [ + { type: 'success', message: 'Hello world!!' }, + { + type: 'error', + message: 'Whoa nelly!', + description:
Something went wrong
, + }, + { type: 'info', message: 'Everything is fine, nothing is ruined' }, + { type: 'warning', message: 'Uh oh' }, + { type: 'info', message: 'Testing multiples of same type' }, + ]; + (useValues as jest.Mock).mockImplementationOnce(() => ({ messages: mockMessages })); + + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(5); + expect(wrapper.find(EuiCallOut).first().prop('color')).toEqual('success'); + expect(wrapper.find('[data-test-subj="error"]')).toHaveLength(1); + expect(wrapper.find(EuiCallOut).last().prop('iconType')).toEqual('iInCircle'); + }); + + it('renders any children', () => { + (useValues as jest.Mock).mockImplementationOnce(() => ({ messages: [{ type: 'success' }] })); + + const wrapper = shallow( + + + + ); + + expect(wrapper.find('[data-test-subj="testing"]').text()).toContain('Some action'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx new file mode 100644 index 00000000000000..5a909a287795cf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { useValues } from 'kea'; +import { EuiCallOut, EuiCallOutProps, EuiSpacer } from '@elastic/eui'; + +import { FlashMessagesLogic, IFlashMessagesValues } from './flash_messages_logic'; + +const FLASH_MESSAGE_TYPES = { + success: { color: 'success' as EuiCallOutProps['color'], icon: 'check' }, + info: { color: 'primary' as EuiCallOutProps['color'], icon: 'iInCircle' }, + warning: { color: 'warning' as EuiCallOutProps['color'], icon: 'alert' }, + error: { color: 'danger' as EuiCallOutProps['color'], icon: 'cross' }, +}; + +export const FlashMessages: React.FC = ({ children }) => { + const { messages } = useValues(FlashMessagesLogic) as IFlashMessagesValues; + + // If we have no messages to display, do not render the element at all + if (!messages.length) return null; + + return ( +
+ {messages.map(({ type, message, description }, index) => ( + + + {description} + + + + ))} + {children} +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts new file mode 100644 index 00000000000000..136912847baa98 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts @@ -0,0 +1,136 @@ +/* + * 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 { resetContext } from 'kea'; + +import { FlashMessagesLogic, IFlashMessage } from './flash_messages_logic'; + +describe('FlashMessagesLogic', () => { + const DEFAULT_VALUES = { + messages: [], + queuedMessages: [], + historyListener: null, + }; + + beforeEach(() => { + jest.clearAllMocks(); + resetContext({}); + }); + + it('has expected default values', () => { + FlashMessagesLogic.mount(); + expect(FlashMessagesLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('setFlashMessages()', () => { + it('sets an array of messages', () => { + const messages: IFlashMessage[] = [ + { type: 'success', message: 'Hello world!!' }, + { type: 'error', message: 'Whoa nelly!', description: 'Uh oh' }, + { type: 'info', message: 'Everything is fine, nothing is ruined' }, + ]; + + FlashMessagesLogic.mount(); + FlashMessagesLogic.actions.setFlashMessages(messages); + + expect(FlashMessagesLogic.values.messages).toEqual(messages); + }); + + it('automatically converts to an array if a single message obj is passed in', () => { + const message = { type: 'success', message: 'I turn into an array!' } as IFlashMessage; + + FlashMessagesLogic.mount(); + FlashMessagesLogic.actions.setFlashMessages(message); + + expect(FlashMessagesLogic.values.messages).toEqual([message]); + }); + }); + + describe('clearFlashMessages()', () => { + it('sets messages back to an empty array', () => { + FlashMessagesLogic.mount(); + FlashMessagesLogic.actions.setFlashMessages('test' as any); + FlashMessagesLogic.actions.clearFlashMessages(); + + expect(FlashMessagesLogic.values.messages).toEqual([]); + }); + }); + + describe('setQueuedMessages()', () => { + it('sets an array of messages', () => { + const queuedMessage: IFlashMessage = { type: 'error', message: 'You deleted a thing' }; + + FlashMessagesLogic.mount(); + FlashMessagesLogic.actions.setQueuedMessages(queuedMessage); + + expect(FlashMessagesLogic.values.queuedMessages).toEqual([queuedMessage]); + }); + }); + + describe('clearQueuedMessages()', () => { + it('sets queued messages back to an empty array', () => { + FlashMessagesLogic.mount(); + FlashMessagesLogic.actions.setQueuedMessages('test' as any); + FlashMessagesLogic.actions.clearQueuedMessages(); + + expect(FlashMessagesLogic.values.queuedMessages).toEqual([]); + }); + }); + + describe('history listener logic', () => { + describe('setHistoryListener()', () => { + it('sets the historyListener value', () => { + FlashMessagesLogic.mount(); + FlashMessagesLogic.actions.setHistoryListener('test' as any); + + expect(FlashMessagesLogic.values.historyListener).toEqual('test'); + }); + }); + + describe('listenToHistory()', () => { + it('listens for history changes and clears messages on change', () => { + FlashMessagesLogic.mount(); + FlashMessagesLogic.actions.setQueuedMessages(['queuedMessages'] as any); + jest.spyOn(FlashMessagesLogic.actions, 'clearFlashMessages'); + jest.spyOn(FlashMessagesLogic.actions, 'setFlashMessages'); + jest.spyOn(FlashMessagesLogic.actions, 'clearQueuedMessages'); + jest.spyOn(FlashMessagesLogic.actions, 'setHistoryListener'); + + const mockListener = jest.fn(() => jest.fn()); + const history = { listen: mockListener } as any; + FlashMessagesLogic.actions.listenToHistory(history); + + expect(mockListener).toHaveBeenCalled(); + expect(FlashMessagesLogic.actions.setHistoryListener).toHaveBeenCalled(); + + const mockHistoryChange = (mockListener.mock.calls[0] as any)[0]; + mockHistoryChange(); + expect(FlashMessagesLogic.actions.clearFlashMessages).toHaveBeenCalled(); + expect(FlashMessagesLogic.actions.setFlashMessages).toHaveBeenCalledWith([ + 'queuedMessages', + ]); + expect(FlashMessagesLogic.actions.clearQueuedMessages).toHaveBeenCalled(); + }); + }); + + describe('beforeUnmount', () => { + it('removes history listener on unmount', () => { + const mockUnlistener = jest.fn(); + const unmount = FlashMessagesLogic.mount(); + + FlashMessagesLogic.actions.setHistoryListener(mockUnlistener); + unmount(); + + expect(mockUnlistener).toHaveBeenCalled(); + }); + + it('does not crash if no listener exists', () => { + const unmount = FlashMessagesLogic.mount(); + unmount(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts new file mode 100644 index 00000000000000..96c7817832c527 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts @@ -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 { kea } from 'kea'; +import { ReactNode } from 'react'; +import { History } from 'history'; + +import { IKeaLogic, TKeaReducers, IKeaParams } from '../types'; + +export interface IFlashMessage { + type: 'success' | 'info' | 'warning' | 'error'; + message: ReactNode; + description?: ReactNode; +} + +export interface IFlashMessagesValues { + messages: IFlashMessage[]; + queuedMessages: IFlashMessage[]; + historyListener: Function | null; +} +export interface IFlashMessagesActions { + setFlashMessages(messages: IFlashMessage | IFlashMessage[]): void; + clearFlashMessages(): void; + setQueuedMessages(messages: IFlashMessage | IFlashMessage[]): void; + clearQueuedMessages(): void; + listenToHistory(history: History): void; + setHistoryListener(historyListener: Function): void; +} + +const convertToArray = (messages: IFlashMessage | IFlashMessage[]) => + !Array.isArray(messages) ? [messages] : messages; + +export const FlashMessagesLogic = kea({ + actions: (): IFlashMessagesActions => ({ + setFlashMessages: (messages) => ({ messages: convertToArray(messages) }), + clearFlashMessages: () => null, + setQueuedMessages: (messages) => ({ messages: convertToArray(messages) }), + clearQueuedMessages: () => null, + listenToHistory: (history) => history, + setHistoryListener: (historyListener) => ({ historyListener }), + }), + reducers: (): TKeaReducers => ({ + messages: [ + [], + { + setFlashMessages: (_, { messages }) => messages, + clearFlashMessages: () => [], + }, + ], + queuedMessages: [ + [], + { + setQueuedMessages: (_, { messages }) => messages, + clearQueuedMessages: () => [], + }, + ], + historyListener: [ + null, + { + setHistoryListener: (_, { historyListener }) => historyListener, + }, + ], + }), + listeners: ({ values, actions }): Partial => ({ + listenToHistory: (history) => { + // On React Router navigation, clear previous flash messages and load any queued messages + const unlisten = history.listen(() => { + actions.clearFlashMessages(); + actions.setFlashMessages(values.queuedMessages); + actions.clearQueuedMessages(); + }); + actions.setHistoryListener(unlisten); + }, + }), + events: ({ values }) => ({ + beforeUnmount: () => { + const { historyListener: removeHistoryListener } = values; + if (removeHistoryListener) removeHistoryListener(); + }, + }), +} as IKeaParams) as IKeaLogic< + IFlashMessagesValues, + IFlashMessagesActions +>; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.test.tsx new file mode 100644 index 00000000000000..bcd7abd6d7ce23 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.test.tsx @@ -0,0 +1,46 @@ +/* + * 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 '../../__mocks__/shallow_usecontext.mock'; +import '../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { useValues, useActions } from 'kea'; + +import { mockHistory } from '../../__mocks__'; + +import { FlashMessagesProvider } from './'; + +describe('FlashMessagesProvider', () => { + const props = { history: mockHistory as any }; + const listenToHistory = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + (useActions as jest.Mock).mockImplementationOnce(() => ({ listenToHistory })); + }); + + it('does not render', () => { + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('listens to history on mount', () => { + shallow(); + + expect(listenToHistory).toHaveBeenCalledWith(mockHistory); + }); + + it('does not add another history listener if one already exists', () => { + (useValues as jest.Mock).mockImplementationOnce(() => ({ historyListener: 'exists' as any })); + + shallow(); + + expect(listenToHistory).not.toHaveBeenCalledWith(props); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx new file mode 100644 index 00000000000000..584124468a91fc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx @@ -0,0 +1,30 @@ +/* + * 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, { useEffect } from 'react'; +import { useValues, useActions } from 'kea'; +import { History } from 'history'; + +import { + FlashMessagesLogic, + IFlashMessagesValues, + IFlashMessagesActions, +} from './flash_messages_logic'; + +interface IFlashMessagesProviderProps { + history: History; +} + +export const FlashMessagesProvider: React.FC = ({ history }) => { + const { historyListener } = useValues(FlashMessagesLogic) as IFlashMessagesValues; + const { listenToHistory } = useActions(FlashMessagesLogic) as IFlashMessagesActions; + + useEffect(() => { + if (!historyListener) listenToHistory(history); + }, []); + + return null; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts new file mode 100644 index 00000000000000..74e233ad6b320b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { FlashMessages } from './flash_messages'; +export { + FlashMessagesLogic, + IFlashMessage, + IFlashMessagesValues, + IFlashMessagesActions, +} from './flash_messages_logic'; +export { FlashMessagesProvider } from './flash_messages_provider'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index a8e08323c5e3b1..561016d36921d4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -4,14 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface IFlashMessagesProps { - info?: string[]; - warning?: string[]; - error?: string[]; - success?: string[]; - isWrapped?: boolean; - children?: React.ReactNode; -} +export { IFlashMessage } from './flash_messages'; export interface IKeaLogic { mount(): Function; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts index 5588c4fc53b673..05715c648e5dcf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts @@ -22,7 +22,6 @@ export const mockLogicValues = { personalSourcesCount: 0, sourcesCount: 0, dataLoading: true, - flashMessages: {}, } as IOverviewValues; export const mockLogicActions = { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts index 3fbf0e60b5b49e..61108d7cb1f2f8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts @@ -76,15 +76,6 @@ describe('OverviewLogic', () => { }); }); - describe('setFlashMessages', () => { - it('will set `flashMessages`', () => { - const flashMessages = { error: ['error'] }; - OverviewLogic.actions.setFlashMessages(flashMessages); - - expect(OverviewLogic.values.flashMessages).toEqual(flashMessages); - }); - }); - describe('initializeOverview', () => { it('calls API and sets values', async () => { const setServerDataSpy = jest.spyOn(OverviewLogic.actions, 'setServerData'); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts index 057bce1b4056c3..6606e5b55cb33b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts @@ -8,7 +8,7 @@ import { kea } from 'kea'; import { HttpLogic } from '../../../shared/http'; import { IAccount, IOrganization } from '../../types'; -import { IFlashMessagesProps, IKeaLogic, TKeaReducers, IKeaParams } from '../../../shared/types'; +import { IKeaLogic, TKeaReducers, IKeaParams } from '../../../shared/types'; import { IFeedActivity } from './recent_activity'; @@ -30,19 +30,16 @@ export interface IOverviewServerData { export interface IOverviewActions { setServerData(serverData: IOverviewServerData): void; - setFlashMessages(flashMessages: IFlashMessagesProps): void; initializeOverview(): void; } export interface IOverviewValues extends IOverviewServerData { dataLoading: boolean; - flashMessages: IFlashMessagesProps; } export const OverviewLogic = kea({ actions: (): IOverviewActions => ({ setServerData: (serverData) => serverData, - setFlashMessages: (flashMessages) => ({ flashMessages }), initializeOverview: () => null, }), reducers: (): TKeaReducers => ({ @@ -70,12 +67,6 @@ export const OverviewLogic = kea({ setServerData: (_, { canCreateInvitations }) => canCreateInvitations, }, ], - flashMessages: [ - {}, - { - setFlashMessages: (_, { flashMessages }) => flashMessages, - }, - ], hasUsers: [ false, { From e2e9d96df674e7cdbcaa94f33f9ca4098c0c9cc8 Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Thu, 27 Aug 2020 12:15:47 -0700 Subject: [PATCH 081/216] accessibility test for Painless lab (#75688) * accessibility test for painless lab * skipped a test due to aria-violation * skipped tests due to aria-violation and added datatestsubj * removed the unwanted import * incorporate review comments * feedback incorporated * review comments incorporated * removed unwanted expect --- .../components/output_pane/output_pane.tsx | 1 + .../public/application/constants.tsx | 3 + .../test/accessibility/apps/painless_lab.ts | 65 +++++++++++++++++++ x-pack/test/accessibility/config.ts | 1 + x-pack/test/functional/config.js | 4 ++ 5 files changed, 74 insertions(+) create mode 100644 x-pack/test/accessibility/apps/painless_lab.ts diff --git a/x-pack/plugins/painless_lab/public/application/components/output_pane/output_pane.tsx b/x-pack/plugins/painless_lab/public/application/components/output_pane/output_pane.tsx index e6a97bb02f7386..ce597b27cc2a39 100644 --- a/x-pack/plugins/painless_lab/public/application/components/output_pane/output_pane.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/output_pane/output_pane.tsx @@ -50,6 +50,7 @@ export const OutputPane: FunctionComponent = ({ isLoading, response }) => {defaultLabel} @@ -41,6 +42,7 @@ export const painlessContextOptions = [ { value: 'filter', inputDisplay: filterLabel, + 'data-test-subj': 'filterButtonDropdown', dropdownDisplay: ( <> {filterLabel} @@ -57,6 +59,7 @@ export const painlessContextOptions = [ { value: 'score', inputDisplay: scoreLabel, + 'data-test-subj': 'scoreButtonDropdown', dropdownDisplay: ( <> {scoreLabel} diff --git a/x-pack/test/accessibility/apps/painless_lab.ts b/x-pack/test/accessibility/apps/painless_lab.ts new file mode 100644 index 00000000000000..0ec8285f50ec86 --- /dev/null +++ b/x-pack/test/accessibility/apps/painless_lab.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'security']); + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const a11y = getService('a11y'); + + describe('Accessibility Painless Lab Editor', () => { + before(async () => { + await PageObjects.common.navigateToApp('painlessLab'); + }); + + it('renders the page without a11y errors', async () => { + await PageObjects.common.navigateToApp('painlessLab'); + await a11y.testAppSnapshot(); + }); + + it('click on the output button', async () => { + const painlessTabsOutput = await find.byCssSelector( + '[data-test-subj="painlessTabs"] #output' + ); + await painlessTabsOutput.click(); + await a11y.testAppSnapshot(); + }); + + it('click on the parameters button', async () => { + const painlessTabsParameters = await find.byCssSelector( + '[data-test-subj="painlessTabs"] #parameters' + ); + await painlessTabsParameters.click(); + await a11y.testAppSnapshot(); + }); + + // github.com/elastic/kibana/issues/75876 + it.skip('click on the context button', async () => { + const painlessTabsContext = await find.byCssSelector( + '[data-test-subj="painlessTabs"] #context' + ); + await painlessTabsContext.click(); + await a11y.testAppSnapshot(); + }); + + it.skip('click on the Basic button', async () => { + await testSubjects.click('basicButtonDropdown'); + await a11y.testAppSnapshot(); + }); + + it.skip('click on the Filter button', async () => { + await testSubjects.click('filterButtonDropdown'); + await a11y.testAppSnapshot(); + }); + + it.skip('click on the Score button', async () => { + await testSubjects.click('scoreButtonDropdown'); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index 7f4543d014def5..0a958057543145 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -20,6 +20,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/grok_debugger'), require.resolve('./apps/search_profiler'), require.resolve('./apps/uptime'), + require.resolve('./apps/painless_lab'), ], pageObjects, services, diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index cdc6292ba808a6..16e2cd1559fce1 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -128,6 +128,10 @@ export default async function ({ readConfigFile }) { pathname: '/app/dev_tools', hash: '/searchprofiler', }, + painlessLab: { + pathname: '/app/dev_tools', + hash: '/painless_lab', + }, spaceSelector: { pathname: '/', }, From ca94923900f9ce424f08f4d32bea53b40bc0e552 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 27 Aug 2020 14:34:28 -0500 Subject: [PATCH 082/216] [Enterprise Search] Migrate util and components from ent-search (#76051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Migrate useDidUpdateEffect hook Migrates https://github.com/elastic/ent-search/blob/master/app/javascript/shared/utils/useDidUpdateEffect.ts with test added. * Migrate TruncateContent component * Migrate TableHeader component * Remove unused deps * Fix test name * Remove custom type in favor of DependencyList * Add stylesheet for truncated content * Actually import stylesheet 🤦🏼‍♂️ * Replace legacy mixin Co-authored-by: Elastic Machine --- .../applications/shared/table_header/index.ts | 7 +++ .../shared/table_header/table_header.test.tsx | 29 ++++++++++++ .../shared/table_header/table_header.tsx | 23 +++++++++ .../applications/shared/truncate/index.ts | 8 ++++ .../shared/truncate/truncate.test.tsx | 47 +++++++++++++++++++ .../applications/shared/truncate/truncate.ts | 13 +++++ .../shared/truncate/truncated_content.scss | 35 ++++++++++++++ .../shared/truncate/truncated_content.tsx | 35 ++++++++++++++ .../shared/use_did_update_effect/index.ts | 7 +++ .../use_did_update_effect.test.tsx | 33 +++++++++++++ .../use_did_update_effect.tsx | 23 +++++++++ 11 files changed, 260 insertions(+) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/table_header/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/truncate/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/table_header/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/index.ts new file mode 100644 index 00000000000000..34ce070fcde469 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/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 { TableHeader } from './table_header'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.test.tsx new file mode 100644 index 00000000000000..70e2ac7ac6f0d0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.test.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiTableHeader, EuiTableHeaderCell } from '@elastic/eui'; + +import { TableHeader } from './table_header'; + +const headerItems = ['foo', 'bar', 'baz']; + +describe('TableHeader', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTableHeader)).toHaveLength(1); + expect(wrapper.find(EuiTableHeaderCell)).toHaveLength(3); + }); + + it('renders extra cell', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTableHeader)).toHaveLength(1); + expect(wrapper.find(EuiTableHeaderCell)).toHaveLength(4); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.tsx new file mode 100644 index 00000000000000..e7f9617fdcd917 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.tsx @@ -0,0 +1,23 @@ +/* + * 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 { EuiTableHeader, EuiTableHeaderCell } from '@elastic/eui'; + +interface ITableHeaderProps { + headerItems: string[]; + extraCell?: boolean; +} + +export const TableHeader: React.FC = ({ headerItems, extraCell }) => ( + + {headerItems.map((item, i) => ( + {item} + ))} + {extraCell && } + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/truncate/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/index.ts new file mode 100644 index 00000000000000..d3ee618e92b5b1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { truncate, truncateBeginning } from './truncate'; +export { TruncatedContent } from './truncated_content'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.test.tsx new file mode 100644 index 00000000000000..aa8427cd822be8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.test.tsx @@ -0,0 +1,47 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { TruncatedContent } from './'; + +const content = 'foobarbaz'; + +describe('TruncatedContent', () => { + it('renders with no truncation', () => { + const wrapper = shallow(); + + expect(wrapper.find('span.truncated-content')).toHaveLength(0); + expect(wrapper.text()).toEqual('foo'); + }); + + it('renders with truncation at the end', () => { + const wrapper = shallow(); + const element = wrapper.find('span.truncated-content'); + + expect(element).toHaveLength(1); + expect(element.prop('title')).toEqual(content); + expect(wrapper.text()).toEqual('foob…'); + expect(wrapper.find('span.truncated-content__tooltip')).toHaveLength(0); + }); + + it('renders with truncation at the beginning', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('span.truncated-content')).toHaveLength(1); + expect(wrapper.text()).toEqual('…rbaz'); + }); + + it('renders with inline tooltip', () => { + const wrapper = shallow(); + + expect(wrapper.find('span.truncated-content').prop('title')).toEqual(''); + expect(wrapper.find('span.truncated-content__tooltip')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.ts b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.ts new file mode 100644 index 00000000000000..36094e3abe258b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.ts @@ -0,0 +1,13 @@ +/* + * 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 function truncate(text: string, length: number) { + return `${text.substring(0, length)}…`; +} + +export function truncateBeginning(text: string, length: number) { + return `…${text.substring(text.length - length)}`; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.scss b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.scss new file mode 100644 index 00000000000000..701834acfed9dd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.scss @@ -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. + */ + +.truncated-content { + position: relative; + z-index: 2; + display: inline-block; + white-space: nowrap; + + &__tooltip { + position: absolute; + top: 50%; + transform: translateY(-50%); + left: -3px; + margin-top: -1px; + background: $euiColorEmptyShade; + border-radius: 2px; + width: calc(100% + 4px); + height: calc(100% + 4px); + padding: 0 2px; + display: none; + align-items: center; + box-shadow: 0 1px 3px rgba(black, 0.1); + border: 1px solid $euiBorderColor; + width: auto; + white-space: nowrap; + + .truncated-content:hover & { + display: flex; + } + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.tsx new file mode 100644 index 00000000000000..7785f75b71d341 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncated_content.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 from 'react'; + +import { truncate, truncateBeginning } from './'; + +import './truncated_content.scss'; + +interface ITruncatedContentProps { + content: string; + length: number; + beginning?: boolean; + tooltipType?: 'inline' | 'title'; +} + +export const TruncatedContent: React.FC = ({ + content, + length, + beginning = false, + tooltipType = 'inline', +}) => { + if (content.length <= length) return <>{content}; + + const inline = tooltipType === 'inline'; + return ( + + {beginning ? truncateBeginning(content, length) : truncate(content, length)} + {inline && {content}} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/index.ts new file mode 100644 index 00000000000000..05c60ebced0885 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/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 { useDidUpdateEffect } from './use_did_update_effect'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.tsx new file mode 100644 index 00000000000000..e3d2ffb44f01e2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { mount } from 'enzyme'; + +import { EuiLink } from '@elastic/eui'; + +import { useDidUpdateEffect } from './use_did_update_effect'; + +const fn = jest.fn(); + +const TestHook = ({ value }: { value: number }) => { + const [inputValue, setValue] = useState(value); + useDidUpdateEffect(fn, [inputValue]); + return setValue(2)} />; +}; + +const wrapper = mount(); + +describe('useDidUpdateEffect', () => { + it('should not fire function when value unchanged', () => { + expect(fn).not.toHaveBeenCalled(); + }); + + it('should fire function when value changed', () => { + wrapper.find(EuiLink).simulate('click'); + expect(fn).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx new file mode 100644 index 00000000000000..4c3e10fc84b841 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/* + * Sometimes we don't want to fire the initial useEffect call. + * This custom Hook only fires after the intial render has completed. + */ +import { useEffect, useRef, DependencyList } from 'react'; + +export const useDidUpdateEffect = (fn: Function, inputs: DependencyList) => { + const didMountRef = useRef(false); + + useEffect(() => { + if (didMountRef.current) { + fn(); + } else { + didMountRef.current = true; + } + }, inputs); +}; From 31507f82b6b4e6751fc694ec61b3b0cab3cbd83c Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Thu, 27 Aug 2020 15:43:52 -0400 Subject: [PATCH 083/216] [Resolver] Fix useSelector usage (#76129) In some cases we have selectors returning thunks. The thunks need to be called inside `useSelector` in order for a rerender to be reliably triggered. `useSelector` triggers a re-render if its return value changes. By calling the thunk inside of the selector passed to `useSelector`, we will trigger re-renders when needed. --- .../resolver/view/panels/process_details.tsx | 6 ++++-- .../view/panels/related_event_detail.tsx | 7 +++---- .../public/resolver/view/process_event_dot.tsx | 18 +++++++++++------- .../view/resolver_without_providers.tsx | 11 +++++++---- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx index e86e3e6baf4a4a..1ec56b8aa169a7 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx @@ -31,7 +31,7 @@ import { import { CubeForProcess } from './cube_for_process'; import { ResolverEvent } from '../../../../common/endpoint/types'; import { useResolverTheme } from '../assets'; -import { CrumbInfo } from '../../types'; +import { CrumbInfo, ResolverState } from '../../types'; const StyledDescriptionList = styled(EuiDescriptionList)` &.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title { @@ -56,7 +56,9 @@ export const ProcessDetails = memo(function ProcessDetails({ }) { const processName = event.eventName(processEvent); const entityId = event.entityId(processEvent); - const isProcessTerminated = useSelector(selectors.isProcessTerminated)(entityId); + const isProcessTerminated = useSelector((state: ResolverState) => + selectors.isProcessTerminated(state)(entityId) + ); const processInfoEntry: EuiDescriptionListProps['listItems'] = useMemo(() => { const eventTime = event.eventTimestamp(processEvent); const dateTime = eventTime === undefined ? null : formatDate(eventTime); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx index 6aacf91c561782..dfafbae9c9a16b 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/related_event_detail.tsx @@ -16,7 +16,7 @@ import { ResolverEvent } from '../../../../common/endpoint/types'; import * as selectors from '../../store/selectors'; import { useResolverDispatch } from '../use_resolver_dispatch'; import { PanelContentError } from './panel_content_error'; -import { CrumbInfo } from '../../types'; +import { CrumbInfo, ResolverState } from '../../types'; // Adding some styles to prevent horizontal scrollbars, per request from UX review const StyledDescriptionList = memo(styled(EuiDescriptionList)` @@ -126,9 +126,8 @@ export const RelatedEventDetail = memo(function RelatedEventDetail({ relatedEventCategory = naString, sections, formattedDate, - ] = useSelector(selectors.relatedEventDisplayInfoByEntityAndSelfId)( - processEntityId, - relatedEventId + ] = useSelector((state: ResolverState) => + selectors.relatedEventDisplayInfoByEntityAndSelfId(state)(processEntityId, relatedEventId) ); const waitCrumbs = useMemo(() => { diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 2bb104801866f1..baa8ce1fcdd860 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -12,7 +12,7 @@ import { htmlIdGenerator, EuiButton, EuiI18nNumber, EuiFlexGroup, EuiFlexItem } import { useSelector } from 'react-redux'; import { NodeSubMenu, subMenuAssets } from './submenu'; import { applyMatrix3 } from '../models/vector2'; -import { Vector2, Matrix3 } from '../types'; +import { Vector2, Matrix3, ResolverState } from '../types'; import { SymbolIds, useResolverTheme, calculateResolverFontSize } from './assets'; import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types'; import { useResolverDispatch } from './use_resolver_dispatch'; @@ -118,7 +118,9 @@ const UnstyledProcessEventDot = React.memo( // NB: this component should be taking nodeID as a `string` instead of handling this logic here throw new Error('Tried to render a node with no ID'); } - const relatedEventStats = useSelector(selectors.relatedEventsStats)(nodeID); + const relatedEventStats = useSelector((state: ResolverState) => + selectors.relatedEventsStats(state)(nodeID) + ); // define a standard way of giving HTML IDs to nodes based on their entity_id/nodeID. // this is used to link nodes via aria attributes @@ -126,11 +128,13 @@ const UnstyledProcessEventDot = React.memo( htmlIDPrefix, ]); - const ariaLevel: number | null = useSelector(selectors.ariaLevel)(nodeID); + const ariaLevel: number | null = useSelector((state: ResolverState) => + selectors.ariaLevel(state)(nodeID) + ); // the node ID to 'flowto' - const ariaFlowtoNodeID: string | null = useSelector(selectors.ariaFlowtoNodeID)(timeAtRender)( - nodeID + const ariaFlowtoNodeID: string | null = useSelector((state: ResolverState) => + selectors.ariaFlowtoNodeID(state)(timeAtRender)(nodeID) ); const isShowingEventActions = xScale > 0.8; @@ -290,8 +294,8 @@ const UnstyledProcessEventDot = React.memo( ? subMenuAssets.initialMenuStatus : relatedEventOptions; - const grandTotal: number | null = useSelector(selectors.relatedEventTotalForProcess)( - event as ResolverEvent + const grandTotal: number | null = useSelector((state: ResolverState) => + selectors.relatedEventTotalForProcess(state)(event as ResolverEvent) ); /* eslint-disable jsx-a11y/click-events-have-key-events */ diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx index 32faeec043f2da..aa845e7283ebe6 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx @@ -21,7 +21,7 @@ import { useStateSyncingActions } from './use_state_syncing_actions'; import { StyledMapContainer, StyledPanel, GraphContainer } from './styles'; import { entityIDSafeVersion } from '../../../common/endpoint/models/event'; import { SideEffectContext } from './side_effect_context'; -import { ResolverProps } from '../types'; +import { ResolverProps, ResolverState } from '../types'; /** * The highest level connected Resolver component. Needs a `Provider` in its ancestry to work. @@ -46,9 +46,12 @@ export const ResolverWithoutProviders = React.memo( // use this for the entire render in order to keep things in sync const timeAtRender = timestamp(); - const { processNodePositions, connectingEdgeLineSegments } = useSelector( - selectors.visibleNodesAndEdgeLines - )(timeAtRender); + const { + processNodePositions, + connectingEdgeLineSegments, + } = useSelector((state: ResolverState) => + selectors.visibleNodesAndEdgeLines(state)(timeAtRender) + ); const terminatedProcesses = useSelector(selectors.terminatedProcesses); const { projectionMatrix, ref: cameraRef, onMouseDown } = useCamera(); From 50193eaabb3993fa662efc83caa5712ef11ae64d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 27 Aug 2020 16:00:03 -0400 Subject: [PATCH 084/216] Fix alerts unable to create / update when the name has trailing whitepace(s) (#76079) * Trim alert name in API key name * Add API integration tests --- .../alerts/server/alerts_client.test.ts | 128 +++++++++++++++++- x-pack/plugins/alerts/server/alerts_client.ts | 4 +- .../tests/alerting/create.ts | 39 ++++++ .../tests/alerting/update.ts | 51 +++++++ 4 files changed, 219 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index d994269366ae6c..f4aef62657abc0 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -652,6 +652,70 @@ describe('create()', () => { expect(taskManager.schedule).toHaveBeenCalledTimes(0); }); + test('should trim alert name when creating API key', async () => { + const data = getMockData({ name: ' my alert name ' }); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: false, + name: ' my alert name ', + alertTypeId: '123', + schedule: { interval: 10000 }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + taskManager.schedule.mockResolvedValueOnce({ + id: 'task-123', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + + await alertsClient.create({ data }); + expect(alertsClientParams.createAPIKey).toHaveBeenCalledWith('Alerting: 123/my alert name'); + }); + test('should validate params', async () => { const data = getMockData(); alertTypeRegistry.get.mockReturnValue({ @@ -2896,9 +2960,13 @@ describe('update()', () => { type: 'alert', attributes: { enabled: true, + tags: ['foo'], alertTypeId: 'myType', + schedule: { interval: '10s' }, consumer: 'myApp', scheduledTaskId: 'task-123', + params: {}, + throttle: null, actions: [ { group: 'default', @@ -2927,7 +2995,7 @@ describe('update()', () => { unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); alertTypeRegistry.get.mockReturnValue({ - id: '123', + id: 'myType', name: 'Test', actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', @@ -3489,6 +3557,64 @@ describe('update()', () => { ); }); + it('should trim alert name in the API key name', async () => { + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: false, + name: ' my alert name ', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + scheduledTaskId: 'task-123', + apiKey: null, + }, + updated_at: new Date().toISOString(), + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + await alertsClient.update({ + id: '1', + data: { + ...existingAlert.attributes, + name: ' my alert name ', + }, + }); + + expect(alertsClientParams.createAPIKey).toHaveBeenCalledWith('Alerting: myType/my alert name'); + }); + it('swallows error when invalidate API key throws', async () => { alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 80e021fc5cb6e1..74aef644d58caa 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { omit, isEqual, map, uniq, pick, truncate } from 'lodash'; +import { omit, isEqual, map, uniq, pick, truncate, trim } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Logger, @@ -940,7 +940,7 @@ export class AlertsClient { } private generateAPIKeyName(alertTypeId: string, alertName: string) { - return truncate(`Alerting: ${alertTypeId}/${alertName}`, { length: 256 }); + return truncate(`Alerting: ${alertTypeId}/${trim(alertName)}`, { length: 256 }); } } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts index 8d7b9dec58cf1c..983f87405a1a64 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts @@ -347,6 +347,45 @@ export default function createAlertTests({ getService }: FtrProviderContext) { } }); + it('should handle create alert request appropriately when alert name has leading and trailing whitespaces', async () => { + const response = await supertestWithoutAuth + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send( + getTestAlertData({ + name: ' leading and trailing whitespace ', + }) + ); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'global_read at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'create', + 'test.noop', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + expect(response.body.name).to.eql(' leading and trailing whitespace '); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + it('should handle create alert request appropriately when alert type is unregistered', async () => { const response = await supertestWithoutAuth .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts index ab3a92d0b3f706..48269cc1c4498f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts @@ -505,6 +505,57 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { } }); + it('should handle update alert request appropriately when alert name has leading and trailing whitespaces', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + const updatedData = { + name: ' leading and trailing whitespace ', + tags: ['bar'], + params: { + foo: true, + }, + schedule: { interval: '12s' }, + actions: [], + throttle: '1m', + }; + const response = await supertestWithoutAuth + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send(updatedData); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'update', + 'test.noop', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + expect(response.body.name).to.eql(' leading and trailing whitespace '); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + it(`shouldn't update alert from another space`, async () => { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) From 905c76242d650dce2f9288c43ba91c8cbd5184e0 Mon Sep 17 00:00:00 2001 From: Chris Cressman Date: Thu, 27 Aug 2020 16:11:18 -0400 Subject: [PATCH 085/216] Fixes App Search documentation links (#76133) Two links to App Search docs are pointing to outdated versions. Update the URLs. --- .../app_search/components/setup_guide/setup_guide.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx index fa55289e73e0b7..204f355c7a31a9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx @@ -19,8 +19,8 @@ export const SetupGuide: React.FC = () => ( Date: Thu, 27 Aug 2020 14:42:21 -0600 Subject: [PATCH 086/216] [Security_Solution][Resolver] Resolver loading and error state (#75600) --- .../data_access_layer/mocks/emptify_mock.ts | 88 +++++++++ .../data_access_layer/mocks/pausify_mock.ts | 124 +++++++++++++ .../resolver/test_utilities/extend_jest.ts | 12 +- .../resolver/view/clickthrough.test.tsx | 1 + .../view/resolver_loading_state.test.tsx | 167 ++++++++++++++++++ 5 files changed, 387 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts new file mode 100644 index 00000000000000..43282848dcf9a1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts @@ -0,0 +1,88 @@ +/* + * 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 { + ResolverRelatedEvents, + ResolverTree, + ResolverEntityIndex, +} from '../../../../common/endpoint/types'; +import { mockTreeWithNoProcessEvents } from '../../mocks/resolver_tree'; +import { DataAccessLayer } from '../../types'; + +type EmptiableRequests = 'relatedEvents' | 'resolverTree' | 'entities' | 'indexPatterns'; + +interface Metadata { + /** + * The `_id` of the document being analyzed. + */ + databaseDocumentID: string; + /** + * A record of entityIDs to be used in tests assertions. + */ + entityIDs: T; +} + +/** + * A simple mock dataAccessLayer that allows you to control whether a request comes back with data or empty. + */ +export function emptifyMock( + { + metadata, + dataAccessLayer, + }: { + dataAccessLayer: DataAccessLayer; + metadata: Metadata; + }, + dataShouldBeEmpty: EmptiableRequests[] +): { + dataAccessLayer: DataAccessLayer; + metadata: Metadata; +} { + return { + metadata, + dataAccessLayer: { + /** + * Fetch related events for an entity ID + */ + async relatedEvents(...args): Promise { + return dataShouldBeEmpty.includes('relatedEvents') + ? Promise.resolve({ + entityID: args[0], + events: [], + nextEvent: null, + }) + : dataAccessLayer.relatedEvents(...args); + }, + + /** + * Fetch a ResolverTree for a entityID + */ + async resolverTree(...args): Promise { + return dataShouldBeEmpty.includes('resolverTree') + ? Promise.resolve(mockTreeWithNoProcessEvents()) + : dataAccessLayer.resolverTree(...args); + }, + + /** + * Get an array of index patterns that contain events. + */ + indexPatterns(...args): string[] { + return dataShouldBeEmpty.includes('indexPatterns') + ? [] + : dataAccessLayer.indexPatterns(...args); + }, + + /** + * Get entities matching a document. + */ + async entities(...args): Promise { + return dataShouldBeEmpty.includes('entities') + ? Promise.resolve([]) + : dataAccessLayer.entities(...args); + }, + }, + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts new file mode 100644 index 00000000000000..baddcdfd0cd841 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts @@ -0,0 +1,124 @@ +/* + * 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 { + ResolverRelatedEvents, + ResolverTree, + ResolverEntityIndex, +} from '../../../../common/endpoint/types'; +import { DataAccessLayer } from '../../types'; + +type PausableRequests = 'relatedEvents' | 'resolverTree' | 'entities'; + +interface Metadata { + /** + * The `_id` of the document being analyzed. + */ + databaseDocumentID: string; + /** + * A record of entityIDs to be used in tests assertions. + */ + entityIDs: T; +} + +/** + * A simple mock dataAccessLayer that allows you to manually pause and resume a request. + */ +export function pausifyMock({ + metadata, + dataAccessLayer, +}: { + dataAccessLayer: DataAccessLayer; + metadata: Metadata; +}): { + dataAccessLayer: DataAccessLayer; + metadata: Metadata; + pause: (pausableRequests: PausableRequests[]) => void; + resume: (pausableRequests: PausableRequests[]) => void; +} { + let relatedEventsPromise = Promise.resolve(); + let resolverTreePromise = Promise.resolve(); + let entitiesPromise = Promise.resolve(); + + let relatedEventsResolver: (() => void) | null; + let resolverTreeResolver: (() => void) | null; + let entitiesResolver: (() => void) | null; + + return { + metadata, + pause: (pausableRequests: PausableRequests[]) => { + const pauseRelatedEventsRequest = pausableRequests.includes('relatedEvents'); + const pauseResolverTreeRequest = pausableRequests.includes('resolverTree'); + const pauseEntitiesRequest = pausableRequests.includes('entities'); + + if (pauseRelatedEventsRequest && !relatedEventsResolver) { + relatedEventsPromise = new Promise((resolve) => { + relatedEventsResolver = resolve; + }); + } + if (pauseResolverTreeRequest && !resolverTreeResolver) { + resolverTreePromise = new Promise((resolve) => { + resolverTreeResolver = resolve; + }); + } + if (pauseEntitiesRequest && !entitiesResolver) { + entitiesPromise = new Promise((resolve) => { + entitiesResolver = resolve; + }); + } + }, + resume: (pausableRequests: PausableRequests[]) => { + const resumeEntitiesRequest = pausableRequests.includes('entities'); + const resumeResolverTreeRequest = pausableRequests.includes('resolverTree'); + const resumeRelatedEventsRequest = pausableRequests.includes('relatedEvents'); + + if (resumeEntitiesRequest && entitiesResolver) { + entitiesResolver(); + entitiesResolver = null; + } + if (resumeResolverTreeRequest && resolverTreeResolver) { + resolverTreeResolver(); + resolverTreeResolver = null; + } + if (resumeRelatedEventsRequest && relatedEventsResolver) { + relatedEventsResolver(); + relatedEventsResolver = null; + } + }, + dataAccessLayer: { + /** + * Fetch related events for an entity ID + */ + async relatedEvents(...args): Promise { + await relatedEventsPromise; + return dataAccessLayer.relatedEvents(...args); + }, + + /** + * Fetch a ResolverTree for a entityID + */ + async resolverTree(...args): Promise { + await resolverTreePromise; + return dataAccessLayer.resolverTree(...args); + }, + + /** + * Get an array of index patterns that contain events. + */ + indexPatterns(...args): string[] { + return dataAccessLayer.indexPatterns(...args); + }, + + /** + * Get entities matching a document. + */ + async entities(...args): Promise { + await entitiesPromise; + return dataAccessLayer.entities(...args); + }, + }, + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts index df8f32d15a7ab6..aa04221361de05 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts @@ -44,7 +44,7 @@ expect.extend({ const received: T[] = []; // Set to true if the test passes. - let pass: boolean = false; + let lastCheckPassed: boolean = false; // Async iterate over the iterable for await (const next of receivedIterable) { @@ -52,15 +52,17 @@ expect.extend({ received.push(next); // Use deep equals to compare the value to the expected value if (this.equals(next, expected)) { - // If the value is equal, break - pass = true; + lastCheckPassed = true; + } else if (lastCheckPassed) { + // the previous check passed but this one didn't + lastCheckPassed = false; break; } } // Use `pass` as set in the above loop (or initialized to `false`) // See https://jestjs.io/docs/en/expect#custom-matchers-api and https://jestjs.io/docs/en/expect#thisutils - const message = pass + const message = lastCheckPassed ? () => `${this.utils.matcherHint(matcherName, undefined, undefined, options)}\n\n` + `Expected: not ${this.utils.printExpected(expected)}\n${ @@ -84,7 +86,7 @@ expect.extend({ ) .join(`\n\n`)}`; - return { message, pass }; + return { message, pass: lastCheckPassed }; }, /** * A custom matcher that takes an async generator and compares each value it yields to an expected value. diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index 358fcd17b998a5..1e5ac093cac776 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -242,6 +242,7 @@ describe('Resolver, when analyzing a tree that has two related events for the or ); if (button) { button.simulate('click'); + button.simulate('click'); // The first click opened the menu, this second click closes it } }); it('should close the submenu', async () => { diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx new file mode 100644 index 00000000000000..c357ee18acfebd --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx @@ -0,0 +1,167 @@ +/* + * 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 { Simulator } from '../test_utilities/simulator'; +import { pausifyMock } from '../data_access_layer/mocks/pausify_mock'; +import { emptifyMock } from '../data_access_layer/mocks/emptify_mock'; +import { noAncestorsTwoChildren } from '../data_access_layer/mocks/no_ancestors_two_children'; +import '../test_utilities/extend_jest'; + +describe('Resolver: data loading and resolution states', () => { + let simulator: Simulator; + const resolverComponentInstanceID = 'resolver-loading-resolution-states'; + + describe('When entities data is being requested', () => { + beforeEach(() => { + const { + metadata: { databaseDocumentID }, + dataAccessLayer, + pause, + } = pausifyMock(noAncestorsTwoChildren()); + pause(['entities']); + simulator = new Simulator({ + dataAccessLayer, + databaseDocumentID, + resolverComponentInstanceID, + }); + }); + + it('should display a loading state', async () => { + await expect( + simulator.map(() => ({ + resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, + resolverGraphError: simulator.testSubject('resolver:graph:error').length, + resolverGraph: simulator.testSubject('resolver:graph').length, + })) + ).toYieldEqualTo({ + resolverGraphLoading: 1, + resolverGraphError: 0, + resolverGraph: 0, + }); + }); + }); + + describe('When resolver tree data is being requested', () => { + beforeEach(() => { + const { + metadata: { databaseDocumentID }, + dataAccessLayer, + pause, + } = pausifyMock(noAncestorsTwoChildren()); + pause(['resolverTree']); + simulator = new Simulator({ + dataAccessLayer, + databaseDocumentID, + resolverComponentInstanceID, + }); + }); + + it('should display a loading state', async () => { + await expect( + simulator.map(() => ({ + resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, + resolverGraphError: simulator.testSubject('resolver:graph:error').length, + resolverGraph: simulator.testSubject('resolver:graph').length, + })) + ).toYieldEqualTo({ + resolverGraphLoading: 1, + resolverGraphError: 0, + resolverGraph: 0, + }); + }); + }); + + describe("When the entities request doesn't return any data", () => { + beforeEach(() => { + const { + metadata: { databaseDocumentID }, + dataAccessLayer, + } = emptifyMock(noAncestorsTwoChildren(), ['entities']); + + simulator = new Simulator({ + dataAccessLayer, + databaseDocumentID, + resolverComponentInstanceID, + }); + }); + + it('should display an error', async () => { + await expect( + simulator.map(() => ({ + resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, + resolverGraphError: simulator.testSubject('resolver:graph:error').length, + resolverGraph: simulator.testSubject('resolver:graph').length, + })) + ).toYieldEqualTo({ + resolverGraphLoading: 0, + resolverGraphError: 1, + resolverGraph: 0, + }); + }); + }); + + describe("When the resolver tree request doesn't return any data", () => { + beforeEach(() => { + const { + metadata: { databaseDocumentID }, + dataAccessLayer, + } = emptifyMock(noAncestorsTwoChildren(), ['resolverTree']); + + simulator = new Simulator({ + dataAccessLayer, + databaseDocumentID, + resolverComponentInstanceID, + }); + }); + + it('should display a resolver graph with 0 nodes', async () => { + await expect( + simulator.map(() => ({ + resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, + resolverGraphError: simulator.testSubject('resolver:graph:error').length, + resolverGraph: simulator.testSubject('resolver:graph').length, + resolverGraphNodes: simulator.testSubject('resolver:node').length, + })) + ).toYieldEqualTo({ + resolverGraphLoading: 0, + resolverGraphError: 0, + resolverGraph: 1, + resolverGraphNodes: 0, + }); + }); + }); + + describe('When all resolver data requests successfully resolve', () => { + beforeEach(async () => { + const { + metadata: { databaseDocumentID }, + dataAccessLayer, + } = noAncestorsTwoChildren(); + + simulator = new Simulator({ + dataAccessLayer, + databaseDocumentID, + resolverComponentInstanceID, + }); + }); + + it('should display the resolver graph with 3 nodes', async () => { + await expect( + simulator.map(() => ({ + resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, + resolverGraphError: simulator.testSubject('resolver:graph:error').length, + resolverGraph: simulator.testSubject('resolver:graph').length, + resolverGraphNodes: simulator.testSubject('resolver:node').length, + })) + ).toYieldEqualTo({ + resolverGraphLoading: 0, + resolverGraphError: 0, + resolverGraph: 1, + resolverGraphNodes: 3, + }); + }); + }); +}); From 89ae03221b99c2cf06aa007b3f055e6f0cd43537 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 27 Aug 2020 14:19:41 -0700 Subject: [PATCH 087/216] [docs/getting-started] link to yarn v1 specifically (#76169) Co-authored-by: spalger --- docs/developer/getting-started/index.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer/getting-started/index.asciidoc b/docs/developer/getting-started/index.asciidoc index 10e603a8da8bbc..9b334a55c4203c 100644 --- a/docs/developer/getting-started/index.asciidoc +++ b/docs/developer/getting-started/index.asciidoc @@ -30,7 +30,7 @@ you can switch to the correct version when using nvm by running: nvm use ---- -Install the latest version of https://yarnpkg.com[yarn]. +Install the latest version of https://classic.yarnpkg.com/en/docs/install[yarn v1]. Bootstrap {kib} and install all the dependencies: From 64311d306f88f649677a26b1f9a50c2f39b1a2aa Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 27 Aug 2020 14:56:48 -0700 Subject: [PATCH 088/216] [plugin-helpers] improve 3rd party KP plugin support (#75019) Co-authored-by: Tyler Smalley Co-authored-by: spalger --- .../external-plugin-functional-tests.asciidoc | 4 +- packages/kbn-dev-utils/package.json | 18 +- packages/kbn-dev-utils/src/babel.ts | 59 +++++ packages/kbn-dev-utils/src/index.ts | 3 + .../src/parse_kibana_platform_plugin.ts | 59 +++++ packages/kbn-dev-utils/src/run/flags.ts | 4 +- .../kbn-dev-utils/src/serializers/index.ts | 1 + .../src/serializers/replace_serializer.ts | 36 +++ ...simple_kibana_platform_plugin_discovery.ts | 54 +---- .../src/streams.ts | 53 ++-- .../src/optimizer/kibana_platform_plugins.ts | 2 +- .../kbn-plugin-generator/src/plugin_types.ts | 60 +++++ .../src/render_template.ts | 25 +- .../template/README.md.ejs | 11 + .../template/package.json.ejs | 2 + packages/kbn-plugin-helpers/package.json | 35 +-- .../build_context.ts} | 20 +- packages/kbn-plugin-helpers/src/cli.ts | 123 ++++++---- packages/kbn-plugin-helpers/src/config.ts | 83 +++++++ .../src/{lib/run.ts => find_kibana_json.ts} | 26 +- .../src/{tasks/start => }/index.ts | 2 +- .../src/integration_tests/build.test.ts | 123 ++++++++++ .../commander_action.test.js.snap | 43 ---- .../src/lib/commander_action.test.js | 87 ------- .../src/lib/commander_action.ts | 36 --- .../kbn-plugin-helpers/src/lib/config_file.ts | 69 ------ .../lib/enable_collecting_unknown_options.ts | 30 --- .../kbn-plugin-helpers/src/lib/pipeline.ts | 23 -- .../src/lib/plugin_config.ts | 74 ------ packages/kbn-plugin-helpers/src/lib/utils.ts | 42 ---- .../kbn-plugin-helpers/src/lib/win_cmd.ts | 24 -- ...task.ts => load_kibana_platform_plugin.ts} | 41 ++-- .../tasks.ts => resolve_kibana_version.ts} | 33 +-- .../src/tasks/build/README.md | 19 -- .../src/tasks/build/build_task.ts | 63 ----- .../src/tasks/build/create_build.ts | 179 -------------- .../src/tasks/build/git_info.ts | 46 ---- .../src/tasks/build/index.ts | 20 -- .../build_action_test_plugin/package.json | 16 -- .../translations/es.json | 4 - .../create_build_test_plugin/index.js | 20 -- .../create_build_test_plugin/package.json | 16 -- .../translations/es.json | 4 - .../create_package_test_plugin/index.js | 20 -- .../create_package_test_plugin/package.json | 16 -- .../translations/es.json | 4 - .../__snapshots__/build_action.test.js.snap | 3 - .../integration_tests/build_action.test.js | 117 --------- .../integration_tests/create_build.test.js | 87 ------- .../integration_tests/create_package.test.js | 48 ---- .../src/tasks/build/rewrite_package_json.ts | 54 ----- .../src/{lib/docs.ts => tasks/clean.ts} | 26 +- .../create_package.ts => create_archive.ts} | 48 ++-- .../src/{lib => tasks}/index.ts | 10 +- .../kbn-plugin-helpers/src/tasks/optimize.ts | 53 ++++ .../src/tasks/start/README.md | 6 - .../src/tasks/start/start_task.ts | 51 ---- .../src/tasks/test/mocha/README.md | 45 ---- .../src/tasks/test/mocha/index.ts | 20 -- .../src/tasks/write_server_files.ts | 101 ++++++++ .../src/tasks/yarn_install.ts | 40 ++++ packages/kbn-plugin-helpers/tsconfig.json | 5 +- .../index.js => scripts/plugin_helpers.js | 3 +- src/setup_node_env/prebuilt_dev_only_entry.js | 1 + .../plugins/kbn_tp_run_pipeline/package.json | 2 +- .../kbn_tp_custom_visualizations/package.json | 2 +- x-pack/.kibana-plugin-helpers.json | 35 --- x-pack/gulpfile.js | 2 - x-pack/package.json | 4 +- x-pack/scripts/api_debug.js | 2 +- x-pack/scripts/functional_test_runner.js | 2 +- x-pack/scripts/functional_tests.js | 2 +- x-pack/scripts/functional_tests_server.js | 2 +- x-pack/scripts/jest.js | 2 +- x-pack/tasks/build.ts | 68 +++++- x-pack/tasks/dev.ts | 14 -- .../common/config.ts | 8 +- .../spaces_api_integration/common/config.ts | 6 +- yarn.lock | 226 +++++++++++++----- 79 files changed, 1146 insertions(+), 1681 deletions(-) create mode 100644 packages/kbn-dev-utils/src/babel.ts create mode 100644 packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts create mode 100644 packages/kbn-dev-utils/src/serializers/replace_serializer.ts rename packages/{kbn-plugin-generator => kbn-dev-utils}/src/streams.ts (62%) create mode 100644 packages/kbn-plugin-generator/src/plugin_types.ts rename packages/kbn-plugin-helpers/{bin/plugin-helpers.js => src/build_context.ts} (73%) mode change 100755 => 100644 create mode 100644 packages/kbn-plugin-helpers/src/config.ts rename packages/kbn-plugin-helpers/src/{lib/run.ts => find_kibana_json.ts} (65%) rename packages/kbn-plugin-helpers/src/{tasks/start => }/index.ts (96%) create mode 100644 packages/kbn-plugin-helpers/src/integration_tests/build.test.ts delete mode 100644 packages/kbn-plugin-helpers/src/lib/__snapshots__/commander_action.test.js.snap delete mode 100644 packages/kbn-plugin-helpers/src/lib/commander_action.test.js delete mode 100644 packages/kbn-plugin-helpers/src/lib/commander_action.ts delete mode 100644 packages/kbn-plugin-helpers/src/lib/config_file.ts delete mode 100644 packages/kbn-plugin-helpers/src/lib/enable_collecting_unknown_options.ts delete mode 100644 packages/kbn-plugin-helpers/src/lib/pipeline.ts delete mode 100644 packages/kbn-plugin-helpers/src/lib/plugin_config.ts delete mode 100644 packages/kbn-plugin-helpers/src/lib/utils.ts delete mode 100644 packages/kbn-plugin-helpers/src/lib/win_cmd.ts rename packages/kbn-plugin-helpers/src/{tasks/test/mocha/test_mocha_task.ts => load_kibana_platform_plugin.ts} (52%) rename packages/kbn-plugin-helpers/src/{lib/tasks.ts => resolve_kibana_version.ts} (58%) delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/README.md delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/build_task.ts delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/create_build.ts delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/git_info.ts delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/index.ts delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__fixtures__/build_action_test_plugin/package.json delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__fixtures__/build_action_test_plugin/translations/es.json delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__fixtures__/create_build_test_plugin/index.js delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__fixtures__/create_build_test_plugin/package.json delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__fixtures__/create_build_test_plugin/translations/es.json delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__fixtures__/create_package_test_plugin/index.js delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__fixtures__/create_package_test_plugin/package.json delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__fixtures__/create_package_test_plugin/translations/es.json delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__snapshots__/build_action.test.js.snap delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/build_action.test.js delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/create_build.test.js delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/integration_tests/create_package.test.js delete mode 100644 packages/kbn-plugin-helpers/src/tasks/build/rewrite_package_json.ts rename packages/kbn-plugin-helpers/src/{lib/docs.ts => tasks/clean.ts} (62%) rename packages/kbn-plugin-helpers/src/tasks/{build/create_package.ts => create_archive.ts} (53%) rename packages/kbn-plugin-helpers/src/{lib => tasks}/index.ts (83%) create mode 100644 packages/kbn-plugin-helpers/src/tasks/optimize.ts delete mode 100644 packages/kbn-plugin-helpers/src/tasks/start/README.md delete mode 100644 packages/kbn-plugin-helpers/src/tasks/start/start_task.ts delete mode 100644 packages/kbn-plugin-helpers/src/tasks/test/mocha/README.md delete mode 100644 packages/kbn-plugin-helpers/src/tasks/test/mocha/index.ts create mode 100644 packages/kbn-plugin-helpers/src/tasks/write_server_files.ts create mode 100644 packages/kbn-plugin-helpers/src/tasks/yarn_install.ts rename packages/kbn-plugin-helpers/src/tasks/build/integration_tests/__fixtures__/build_action_test_plugin/index.js => scripts/plugin_helpers.js (88%) delete mode 100644 x-pack/.kibana-plugin-helpers.json delete mode 100644 x-pack/tasks/dev.ts diff --git a/docs/developer/plugin/external-plugin-functional-tests.asciidoc b/docs/developer/plugin/external-plugin-functional-tests.asciidoc index 706bf6af8ed9b4..7e5b5b79d06e9c 100644 --- a/docs/developer/plugin/external-plugin-functional-tests.asciidoc +++ b/docs/developer/plugin/external-plugin-functional-tests.asciidoc @@ -13,7 +13,7 @@ To get started copy and paste this example to `test/functional/config.js`: ["source","js"] ----------- import { resolve } from 'path'; -import { resolveKibanaPath } from '@kbn/plugin-helpers'; +import { REPO_ROOT } from '@kbn/dev-utils'; import { MyServiceProvider } from './services/my_service'; import { MyAppPageProvider } from './services/my_app_page'; @@ -24,7 +24,7 @@ export default async function ({ readConfigFile }) { // read the {kib} config file so that we can utilize some of // its services and PageObjects - const kibanaConfig = await readConfigFile(resolveKibanaPath('test/functional/config.js')); + const kibanaConfig = await readConfigFile(resolve(REPO_ROOT, 'test/functional/config.js')); return { // list paths to the files that contain your plugins tests diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index 768a67794517f0..4f6f995f38f31b 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -1,32 +1,38 @@ { "name": "@kbn/dev-utils", - "main": "./target/index.js", "version": "1.0.0", - "license": "Apache-2.0", "private": true, + "license": "Apache-2.0", + "main": "./target/index.js", "scripts": { "build": "tsc", "kbn:bootstrap": "yarn build", "kbn:watch": "yarn build --watch" }, "dependencies": { + "@babel/core": "^7.11.1", "axios": "^0.19.0", "chalk": "^4.1.0", + "cheerio": "0.22.0", "dedent": "^0.7.0", "execa": "^4.0.2", "exit-hook": "^2.2.0", "getopts": "^2.2.5", + "globby": "^8.0.1", "load-json-file": "^6.2.0", - "normalize-path": "^3.0.0", + "markdown-it": "^10.0.0", "moment": "^2.24.0", + "normalize-path": "^3.0.0", "rxjs": "^6.5.5", "strip-ansi": "^6.0.0", "tree-kill": "^1.2.2", - "tslib": "^2.0.0" + "vinyl": "^2.2.0" }, "devDependencies": { - "typescript": "4.0.2", + "@kbn/babel-preset": "1.0.0", "@kbn/expect": "1.0.0", - "chance": "1.0.18" + "@types/vinyl": "^2.0.4", + "chance": "1.0.18", + "typescript": "4.0.2" } } diff --git a/packages/kbn-dev-utils/src/babel.ts b/packages/kbn-dev-utils/src/babel.ts new file mode 100644 index 00000000000000..e48fe81d0232c6 --- /dev/null +++ b/packages/kbn-dev-utils/src/babel.ts @@ -0,0 +1,59 @@ +/* + * 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 File from 'vinyl'; +import * as Babel from '@babel/core'; + +const transformedFiles = new WeakSet(); + +/** + * Returns a promise that resolves when the file has been + * mutated so the contents of the file are tranformed with + * babel, include inline sourcemaps, and the filename has + * been updated to use .js. + * + * If the file was previously transformed with this function + * the promise will just resolve immediately. + */ +export async function transformFileWithBabel(file: File) { + if (!(file.contents instanceof Buffer)) { + throw new Error('file must be buffered'); + } + + if (transformedFiles.has(file)) { + return; + } + + const source = file.contents.toString('utf8'); + const result = await Babel.transformAsync(source, { + babelrc: false, + configFile: false, + sourceMaps: 'inline', + filename: file.path, + presets: [require.resolve('@kbn/babel-preset/node_preset')], + }); + + if (!result || typeof result.code !== 'string') { + throw new Error('babel transformation failed without an error...'); + } + + file.contents = Buffer.from(result.code); + file.extname = '.js'; + transformedFiles.add(file); +} diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 798746d159f60d..2871fe2ffcf4a0 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -41,3 +41,6 @@ export * from './stdio'; export * from './ci_stats_reporter'; export * from './plugin_list'; export * from './simple_kibana_platform_plugin_discovery'; +export * from './streams'; +export * from './babel'; +export * from './parse_kibana_platform_plugin'; diff --git a/packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts b/packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts new file mode 100644 index 00000000000000..83d8c2684d7caa --- /dev/null +++ b/packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts @@ -0,0 +1,59 @@ +/* + * 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 Path from 'path'; +import loadJsonFile from 'load-json-file'; + +export interface KibanaPlatformPlugin { + readonly directory: string; + readonly manifestPath: string; + readonly manifest: { + id: string; + ui: boolean; + server: boolean; + [key: string]: unknown; + }; +} + +export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformPlugin { + if (!Path.isAbsolute(manifestPath)) { + throw new TypeError('expected new platform manifest path to be absolute'); + } + + const manifest = loadJsonFile.sync(manifestPath); + if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) { + throw new TypeError('expected new platform plugin manifest to be a JSON encoded object'); + } + + if (typeof manifest.id !== 'string') { + throw new TypeError('expected new platform plugin manifest to have a string id'); + } + + return { + directory: Path.dirname(manifestPath), + manifestPath, + manifest: { + ...manifest, + + ui: !!manifest.ui, + server: !!manifest.server, + id: manifest.id, + }, + }; +} diff --git a/packages/kbn-dev-utils/src/run/flags.ts b/packages/kbn-dev-utils/src/run/flags.ts index 12642bceca15a4..54758b4a7dbf8c 100644 --- a/packages/kbn-dev-utils/src/run/flags.ts +++ b/packages/kbn-dev-utils/src/run/flags.ts @@ -52,8 +52,8 @@ export function mergeFlagOptions(global: FlagOptions = {}, local: FlagOptions = boolean: [...(global.boolean || []), ...(local.boolean || [])], string: [...(global.string || []), ...(local.string || [])], default: { - ...global.alias, - ...local.alias, + ...global.default, + ...local.default, }, help: local.help, diff --git a/packages/kbn-dev-utils/src/serializers/index.ts b/packages/kbn-dev-utils/src/serializers/index.ts index e645a3be3fe5d4..6e0ac0b8be0290 100644 --- a/packages/kbn-dev-utils/src/serializers/index.ts +++ b/packages/kbn-dev-utils/src/serializers/index.ts @@ -21,3 +21,4 @@ export * from './absolute_path_serializer'; export * from './strip_ansi_serializer'; export * from './recursive_serializer'; export * from './any_instance_serizlizer'; +export * from './replace_serializer'; diff --git a/packages/kbn-dev-utils/src/serializers/replace_serializer.ts b/packages/kbn-dev-utils/src/serializers/replace_serializer.ts new file mode 100644 index 00000000000000..06096c4bee3a20 --- /dev/null +++ b/packages/kbn-dev-utils/src/serializers/replace_serializer.ts @@ -0,0 +1,36 @@ +/* + * 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 { createRecursiveSerializer } from './recursive_serializer'; + +type Replacer = (substring: string, ...args: any[]) => string; + +export function createReplaceSerializer( + toReplace: string | RegExp, + replaceWith: string | Replacer +) { + return createRecursiveSerializer( + typeof toReplace === 'string' + ? (v: any) => typeof v === 'string' && v.includes(toReplace) + : (v: any) => typeof v === 'string' && toReplace.test(v), + typeof replaceWith === 'string' + ? (v: string) => v.replace(toReplace, replaceWith) + : (v: string) => v.replace(toReplace, replaceWith) + ); +} diff --git a/packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts b/packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts index c7155b2b3c51b2..c56d63edb9ac40 100644 --- a/packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts +++ b/packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts @@ -20,67 +20,37 @@ import Path from 'path'; import globby from 'globby'; -import loadJsonFile from 'load-json-file'; -export interface KibanaPlatformPlugin { - readonly directory: string; - readonly manifestPath: string; - readonly manifest: { - id: string; - [key: string]: unknown; - }; -} +import { parseKibanaPlatformPlugin } from './parse_kibana_platform_plugin'; /** * Helper to find the new platform plugins. */ -export function simpleKibanaPlatformPluginDiscovery(scanDirs: string[], paths: string[]) { +export function simpleKibanaPlatformPluginDiscovery(scanDirs: string[], pluginPaths: string[]) { const patterns = Array.from( new Set([ // find kibana.json files up to 5 levels within the scan dir ...scanDirs.reduce( (acc: string[], dir) => [ ...acc, - `${dir}/*/kibana.json`, - `${dir}/*/*/kibana.json`, - `${dir}/*/*/*/kibana.json`, - `${dir}/*/*/*/*/kibana.json`, - `${dir}/*/*/*/*/*/kibana.json`, + Path.resolve(dir, '*/kibana.json'), + Path.resolve(dir, '*/*/kibana.json'), + Path.resolve(dir, '*/*/*/kibana.json'), + Path.resolve(dir, '*/*/*/*/kibana.json'), + Path.resolve(dir, '*/*/*/*/*/kibana.json'), ], [] ), - ...paths.map((path) => `${path}/kibana.json`), + ...pluginPaths.map((path) => Path.resolve(path, `kibana.json`)), ]) ); const manifestPaths = globby.sync(patterns, { absolute: true }).map((path) => - // absolute paths returned from globby are using normalize or something so the path separators are `/` even on windows, Path.resolve solves this + // absolute paths returned from globby are using normalize or + // something so the path separators are `/` even on windows, + // Path.resolve solves this Path.resolve(path) ); - return manifestPaths.map( - (manifestPath): KibanaPlatformPlugin => { - if (!Path.isAbsolute(manifestPath)) { - throw new TypeError('expected new platform manifest path to be absolute'); - } - - const manifest = loadJsonFile.sync(manifestPath); - if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) { - throw new TypeError('expected new platform plugin manifest to be a JSON encoded object'); - } - - if (typeof manifest.id !== 'string') { - throw new TypeError('expected new platform plugin manifest to have a string id'); - } - - return { - directory: Path.dirname(manifestPath), - manifestPath, - manifest: { - ...manifest, - id: manifest.id, - }, - }; - } - ); + return manifestPaths.map(parseKibanaPlatformPlugin); } diff --git a/packages/kbn-plugin-generator/src/streams.ts b/packages/kbn-dev-utils/src/streams.ts similarity index 62% rename from packages/kbn-plugin-generator/src/streams.ts rename to packages/kbn-dev-utils/src/streams.ts index 976008e879dd3f..6a868f648e78d2 100644 --- a/packages/kbn-plugin-generator/src/streams.ts +++ b/packages/kbn-dev-utils/src/streams.ts @@ -20,7 +20,6 @@ import { Transform } from 'stream'; import File from 'vinyl'; -import { Minimatch } from 'minimatch'; interface BufferedFile extends File { contents: Buffer; @@ -33,41 +32,31 @@ interface BufferedFile extends File { * mutate the file, replace it with another file (return a new File * object), or drop it from the stream (return null) */ -export const tapFileStream = ( +export const transformFileStream = ( fn: (file: BufferedFile) => File | void | null | Promise ) => new Transform({ objectMode: true, - transform(file: BufferedFile, _, cb) { - Promise.resolve(file) - .then(fn) - .then( - (result) => { - // drop the file when null is returned - if (result === null) { - cb(); - } else { - cb(undefined, result || file); - } - }, - (error) => cb(error) - ); - }, - }); + transform(file: File, _, cb) { + Promise.resolve() + .then(async () => { + if (file.isDirectory()) { + return cb(undefined, file); + } -export const excludeFiles = (globs: string[]) => { - const patterns = globs.map( - (g) => - new Minimatch(g, { - matchBase: true, - }) - ); + if (!(file.contents instanceof Buffer)) { + throw new Error('files must be buffered to use transformFileStream()'); + } - return tapFileStream((file) => { - const path = file.relative.replace(/\.ejs$/, ''); - const exclude = patterns.some((p) => p.match(path)); - if (exclude) { - return null; - } + const result = await fn(file as BufferedFile); + + if (result === null) { + // explicitly drop file if null is returned + cb(); + } else { + cb(undefined, result || file); + } + }) + .catch(cb); + }, }); -}; diff --git a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts index a848d779dc9a26..8a3379211927b1 100644 --- a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts +++ b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts @@ -50,7 +50,7 @@ export function findKibanaPlatformPlugins(scanDirs: string[], paths: string[]) { directory, manifestPath, id: manifest.id, - isUiPlugin: !!manifest.ui, + isUiPlugin: manifest.ui, extraPublicDirs: extraPublicDirs || [], }; } diff --git a/packages/kbn-plugin-generator/src/plugin_types.ts b/packages/kbn-plugin-generator/src/plugin_types.ts new file mode 100644 index 00000000000000..ae5201f4e8dbb2 --- /dev/null +++ b/packages/kbn-plugin-generator/src/plugin_types.ts @@ -0,0 +1,60 @@ +/* + * 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 Path from 'path'; + +import { REPO_ROOT } from '@kbn/dev-utils'; + +export interface PluginType { + thirdParty: boolean; + installDir: string; +} + +export const PLUGIN_TYPE_OPTIONS: Array<{ name: string; value: PluginType }> = [ + { + name: 'Installable plugin', + value: { thirdParty: true, installDir: Path.resolve(REPO_ROOT, 'plugins') }, + }, + { + name: 'Kibana Example', + value: { thirdParty: false, installDir: Path.resolve(REPO_ROOT, 'examples') }, + }, + { + name: 'Kibana OSS', + value: { thirdParty: false, installDir: Path.resolve(REPO_ROOT, 'src/plugins') }, + }, + { + name: 'Kibana OSS Functional Testing', + value: { + thirdParty: false, + installDir: Path.resolve(REPO_ROOT, 'test/plugin_functional/plugins'), + }, + }, + { + name: 'X-Pack', + value: { thirdParty: false, installDir: Path.resolve(REPO_ROOT, 'x-pack/plugins') }, + }, + { + name: 'X-Pack Functional Testing', + value: { + thirdParty: false, + installDir: Path.resolve(REPO_ROOT, 'x-pack/test/plugin_functional/plugins'), + }, + }, +]; diff --git a/packages/kbn-plugin-generator/src/render_template.ts b/packages/kbn-plugin-generator/src/render_template.ts index 18bdcf1be1a6b2..894088c1196516 100644 --- a/packages/kbn-plugin-generator/src/render_template.ts +++ b/packages/kbn-plugin-generator/src/render_template.ts @@ -23,15 +23,32 @@ import { promisify } from 'util'; import vfs from 'vinyl-fs'; import prettier from 'prettier'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT, transformFileStream } from '@kbn/dev-utils'; import ejs from 'ejs'; +import { Minimatch } from 'minimatch'; import { snakeCase, camelCase, upperCamelCase } from './casing'; -import { excludeFiles, tapFileStream } from './streams'; import { Answers } from './ask_questions'; const asyncPipeline = promisify(pipeline); +const excludeFiles = (globs: string[]) => { + const patterns = globs.map( + (g) => + new Minimatch(g, { + matchBase: true, + }) + ); + + return transformFileStream((file) => { + const path = file.relative.replace(/\.ejs$/, ''); + const exclude = patterns.some((p) => p.match(path)); + if (exclude) { + return null; + } + }); +}; + /** * Stream all the files from the template directory, ignoring * certain files based on the answers, process the .ejs templates @@ -82,7 +99,7 @@ export async function renderTemplates({ ), // render .ejs templates and rename to not use .ejs extension - tapFileStream((file) => { + transformFileStream((file) => { if (file.extname !== '.ejs') { return; } @@ -108,7 +125,7 @@ export async function renderTemplates({ }), // format each file with prettier - tapFileStream((file) => { + transformFileStream((file) => { if (!file.extname) { return; } diff --git a/packages/kbn-plugin-generator/template/README.md.ejs b/packages/kbn-plugin-generator/template/README.md.ejs index 5f30bf04633059..2cd19c904263e1 100755 --- a/packages/kbn-plugin-generator/template/README.md.ejs +++ b/packages/kbn-plugin-generator/template/README.md.ejs @@ -7,3 +7,14 @@ A Kibana plugin ## Development See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. + +<% if (thirdPartyPlugin) { %> +## Scripts +
+
yarn kbn bootstrap
+
Execute this to install node_modules and setup the dependencies in your plugin and in Kibana
+ +
yarn plugin-helpers build
+
Execute this to create a distributable version of this plugin that can be installed in Kibana
+
+<% } %> diff --git a/packages/kbn-plugin-generator/template/package.json.ejs b/packages/kbn-plugin-generator/template/package.json.ejs index cbd59894ca47c9..ab234b1df2bc50 100644 --- a/packages/kbn-plugin-generator/template/package.json.ejs +++ b/packages/kbn-plugin-generator/template/package.json.ejs @@ -3,6 +3,8 @@ "version": "0.0.0", "private": true, "scripts": { + "build": "yarn plugin-helpers build", + "plugin-helpers": "node ../../scripts/plugin_helpers", "kbn": "node ../../scripts/kbn" } } diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json index ba39528a1f8097..129c58a4b41744 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -1,43 +1,32 @@ { "name": "@kbn/plugin-helpers", - "version": "9.0.2", + "version": "1.0.0", "private": true, "description": "Just some helpers for kibana plugin devs.", "license": "Apache-2.0", - "main": "target/lib/index.js", - "scripts": { - "kbn:bootstrap": "tsc" - }, + "main": "target/index.js", "bin": { "plugin-helpers": "bin/plugin-helpers.js" }, + "scripts": { + "kbn:bootstrap": "rm -rf target && tsc", + "kbn:watch": "tsc --watch" + }, "dependencies": { - "@babel/core": "^7.11.1", - "argv-split": "^2.0.1", - "commander": "^3.0.0", + "@kbn/dev-utils": "1.0.0", + "@kbn/optimizer": "1.0.0", "del": "^5.1.0", "execa": "^4.0.2", - "globby": "^8.0.1", - "gulp-babel": "^8.0.0", - "gulp-rename": "1.4.0", - "gulp-zip": "5.0.1", + "gulp-zip": "^5.0.2", "inquirer": "^1.2.2", - "minimatch": "^3.0.4", - "through2": "^2.0.3", - "through2-map": "^3.0.0", - "vinyl": "^2.2.0", + "load-json-file": "^6.2.0", "vinyl-fs": "^3.0.3" }, "devDependencies": { - "@types/gulp-rename": "^0.0.33", + "@types/decompress": "^4.2.3", "@types/gulp-zip": "^4.0.1", "@types/inquirer": "^6.5.0", - "@types/through2": "^2.0.35", - "@types/through2-map": "^3.0.0", - "@types/vinyl": "^2.0.4", + "decompress": "^4.2.1", "typescript": "4.0.2" - }, - "peerDependencies": { - "@kbn/babel-preset": "1.0.0" } } diff --git a/packages/kbn-plugin-helpers/bin/plugin-helpers.js b/packages/kbn-plugin-helpers/src/build_context.ts old mode 100755 new mode 100644 similarity index 73% rename from packages/kbn-plugin-helpers/bin/plugin-helpers.js rename to packages/kbn-plugin-helpers/src/build_context.ts index 175ff1019fa2dc..62300d5a34e490 --- a/packages/kbn-plugin-helpers/bin/plugin-helpers.js +++ b/packages/kbn-plugin-helpers/src/build_context.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env node - /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -19,10 +17,16 @@ * under the License. */ -const nodeMajorVersion = parseFloat(process.version.replace(/^v(\d+)\..+/, '$1')); -if (nodeMajorVersion < 6) { - console.error('FATAL: kibana-plugin-helpers requires node 6+'); - process.exit(1); -} +import { ToolingLog } from '@kbn/dev-utils'; -require('../target/cli'); +import { Plugin } from './load_kibana_platform_plugin'; +import { Config } from './config'; + +export interface BuildContext { + log: ToolingLog; + plugin: Plugin; + config: Config; + sourceDir: string; + buildDir: string; + kibanaVersion: string; +} diff --git a/packages/kbn-plugin-helpers/src/cli.ts b/packages/kbn-plugin-helpers/src/cli.ts index 18ddc62cba8a63..21b6559f636504 100644 --- a/packages/kbn-plugin-helpers/src/cli.ts +++ b/packages/kbn-plugin-helpers/src/cli.ts @@ -17,59 +17,86 @@ * under the License. */ -import Fs from 'fs'; import Path from 'path'; -import program from 'commander'; +import { RunWithCommands, createFlagError, createFailError } from '@kbn/dev-utils'; -import { createCommanderAction } from './lib/commander_action'; -import { docs } from './lib/docs'; -import { enableCollectingUnknownOptions } from './lib/enable_collecting_unknown_options'; +import { findKibanaJson } from './find_kibana_json'; +import { loadKibanaPlatformPlugin } from './load_kibana_platform_plugin'; +import * as Tasks from './tasks'; +import { BuildContext } from './build_context'; +import { resolveKibanaVersion } from './resolve_kibana_version'; +import { loadConfig } from './config'; -const pkg = JSON.parse(Fs.readFileSync(Path.resolve(__dirname, '../package.json'), 'utf8')); -program.version(pkg.version); +export function runCli() { + new RunWithCommands({ + description: 'Some helper tasks for plugin-authors', + }) + .command({ + name: 'build', + description: ` + Copies files from the source into a zip archive that can be distributed for + installation into production Kibana installs. The archive includes the non- + development npm dependencies and builds itself using raw files in the source + directory so make sure they are clean/up to date. The resulting archive can + be found at: -enableCollectingUnknownOptions( - program - .command('start') - .description('Start kibana and have it include this plugin') - .on('--help', docs('start')) - .action( - createCommanderAction('start', (command) => ({ - flags: command.unknownOptions, - })) - ) -); + build/{plugin.id}-{kibanaVersion}.zip -program - .command('build [files...]') - .description('Build a distributable archive') - .on('--help', docs('build')) - .option('--skip-archive', "Don't create the zip file, leave the build path alone") - .option( - '-d, --build-destination ', - 'Target path for the build output, absolute or relative to the plugin root' - ) - .option('-b, --build-version ', 'Version for the build output') - .option('-k, --kibana-version ', 'Kibana version for the build output') - .action( - createCommanderAction('build', (command, files) => ({ - buildDestination: command.buildDestination, - buildVersion: command.buildVersion, - kibanaVersion: command.kibanaVersion, - skipArchive: Boolean(command.skipArchive), - files, - })) - ); + `, + flags: { + boolean: ['skip-archive'], + string: ['kibana-version'], + alias: { + k: 'kibana-version', + }, + help: ` + --skip-archive Don't create the zip file, just create the build/kibana directory + --kibana-version, -v Kibana version that the + `, + }, + async run({ log, flags }) { + const versionFlag = flags['kibana-version']; + if (versionFlag !== undefined && typeof versionFlag !== 'string') { + throw createFlagError('expected a single --kibana-version flag'); + } -program - .command('test:mocha [files...]') - .description('Run the server tests using mocha') - .on('--help', docs('test/mocha')) - .action( - createCommanderAction('testMocha', (command, files) => ({ - files, - })) - ); + const skipArchive = flags['skip-archive']; + if (skipArchive !== undefined && typeof skipArchive !== 'boolean') { + throw createFlagError('expected a single --skip-archive flag'); + } -program.parse(process.argv); + const pluginDir = await findKibanaJson(process.cwd()); + if (!pluginDir) { + throw createFailError( + `Unable to find Kibana Platform plugin in [${process.cwd()}] or any of its parent directories. Has it been migrated properly? Does it have a kibana.json file?` + ); + } + + const plugin = loadKibanaPlatformPlugin(pluginDir); + const config = await loadConfig(log, plugin); + const kibanaVersion = await resolveKibanaVersion(versionFlag, plugin); + const sourceDir = plugin.directory; + const buildDir = Path.resolve(plugin.directory, 'build/kibana', plugin.manifest.id); + + const context: BuildContext = { + log, + plugin, + config, + sourceDir, + buildDir, + kibanaVersion, + }; + + await Tasks.initTargets(context); + await Tasks.optimize(context); + await Tasks.writeServerFiles(context); + await Tasks.yarnInstall(context); + + if (skipArchive !== true) { + await Tasks.createArchive(context); + } + }, + }) + .execute(); +} diff --git a/packages/kbn-plugin-helpers/src/config.ts b/packages/kbn-plugin-helpers/src/config.ts new file mode 100644 index 00000000000000..bd5ad8ab6acc7e --- /dev/null +++ b/packages/kbn-plugin-helpers/src/config.ts @@ -0,0 +1,83 @@ +/* + * 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 Path from 'path'; + +import loadJsonFile from 'load-json-file'; + +import { ToolingLog } from '@kbn/dev-utils'; +import { Plugin } from './load_kibana_platform_plugin'; + +export interface Config { + skipInstallDependencies: boolean; + serverSourcePatterns?: string[]; +} + +const isArrayOfStrings = (v: any): v is string[] => + Array.isArray(v) && v.every((p) => typeof p === 'string'); + +export async function loadConfig(log: ToolingLog, plugin: Plugin): Promise { + try { + const path = Path.resolve(plugin.directory, '.kibana-plugin-helpers.json'); + const file = await loadJsonFile(path); + + if (!(typeof file === 'object' && file && !Array.isArray(file))) { + throw new TypeError(`expected config at [${path}] to be an object`); + } + + const { + skipInstallDependencies = false, + buildSourcePatterns, + serverSourcePatterns, + ...rest + } = file; + + if (typeof skipInstallDependencies !== 'boolean') { + throw new TypeError(`expected [skipInstallDependencies] at [${path}] to be a boolean`); + } + + if (buildSourcePatterns) { + log.warning( + `DEPRECATED: rename [buildSourcePatterns] to [serverSourcePatterns] in [${path}]` + ); + } + const ssp = buildSourcePatterns || serverSourcePatterns; + if (ssp !== undefined && !isArrayOfStrings(ssp)) { + throw new TypeError(`expected [serverSourcePatterns] at [${path}] to be an array of strings`); + } + + if (Object.keys(rest).length) { + throw new TypeError(`unexpected key in [${path}]: ${Object.keys(rest).join(', ')}`); + } + + log.info(`Loaded config file from [${path}]`); + return { + skipInstallDependencies, + serverSourcePatterns: ssp, + }; + } catch (error) { + if (error.code === 'ENOENT') { + return { + skipInstallDependencies: false, + }; + } + + throw error; + } +} diff --git a/packages/kbn-plugin-helpers/src/lib/run.ts b/packages/kbn-plugin-helpers/src/find_kibana_json.ts similarity index 65% rename from packages/kbn-plugin-helpers/src/lib/run.ts rename to packages/kbn-plugin-helpers/src/find_kibana_json.ts index 2b1a2a63c1074e..93403090568307 100644 --- a/packages/kbn-plugin-helpers/src/lib/run.ts +++ b/packages/kbn-plugin-helpers/src/find_kibana_json.ts @@ -17,21 +17,21 @@ * under the License. */ -import { pluginConfig, PluginConfig } from './plugin_config'; -import { tasks, Tasks } from './tasks'; +import Path from 'path'; +import Fs from 'fs'; +import { promisify } from 'util'; -export interface TaskContext { - plugin: PluginConfig; - run: typeof run; - options?: any; -} +const existsAsync = promisify(Fs.exists); + +export async function findKibanaJson(directory: string): Promise { + if (await existsAsync(Path.resolve(directory, 'kibana.json'))) { + return directory; + } -export function run(name: keyof Tasks, options?: any) { - const action = tasks[name]; - if (!action) { - throw new Error('Invalid task: "' + name + '"'); + const parent = Path.dirname(directory); + if (parent === directory) { + return undefined; } - const plugin = pluginConfig(); - return action({ plugin, run, options }); + return findKibanaJson(parent); } diff --git a/packages/kbn-plugin-helpers/src/tasks/start/index.ts b/packages/kbn-plugin-helpers/src/index.ts similarity index 96% rename from packages/kbn-plugin-helpers/src/tasks/start/index.ts rename to packages/kbn-plugin-helpers/src/index.ts index cf34bdbadf4166..a05bc698bde174 100644 --- a/packages/kbn-plugin-helpers/src/tasks/start/index.ts +++ b/packages/kbn-plugin-helpers/src/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export * from './start_task'; +export * from './cli'; diff --git a/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts new file mode 100644 index 00000000000000..62f83cd672f3d0 --- /dev/null +++ b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts @@ -0,0 +1,123 @@ +/* + * 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 Path from 'path'; +import Fs from 'fs'; + +import execa from 'execa'; +import { createStripAnsiSerializer, REPO_ROOT, createReplaceSerializer } from '@kbn/dev-utils'; +import decompress from 'decompress'; +import del from 'del'; +import globby from 'globby'; +import loadJsonFile from 'load-json-file'; + +const PLUGIN_DIR = Path.resolve(REPO_ROOT, 'plugins/foo_test_plugin'); +const PLUGIN_BUILD_DIR = Path.resolve(PLUGIN_DIR, 'build'); +const PLUGIN_ARCHIVE = Path.resolve(PLUGIN_BUILD_DIR, `fooTestPlugin-7.5.0.zip`); +const TMP_DIR = Path.resolve(__dirname, '__tmp__'); + +expect.addSnapshotSerializer(createReplaceSerializer(/[\d\.]+ sec/g, '

; } - return ( - - { - this.props.onCloseTooltip(); - const filters = await tooltipProperty.getESFilters(); - this.props.addFilters(filters); - }} - aria-label={i18n.translate('xpack.maps.tooltip.filterOnPropertyAriaLabel', { - defaultMessage: 'Filter on property', - })} - data-test-subj="mapTooltipCreateFilterButton" - /> + const applyFilterButton = ( + { + this.props.onCloseTooltip(); + const filters = await tooltipProperty.getESFilters(); + this.props.addFilters(filters); + }} + aria-label={i18n.translate('xpack.maps.tooltip.filterOnPropertyAriaLabel', { + defaultMessage: 'Filter on property', + })} + data-test-subj="mapTooltipCreateFilterButton" + > + + + ); + + return this.state.actions.length === 0 || + (this.state.actions.length === 1 && + this.state.actions[0].id === ACTION_GLOBAL_APPLY_FILTER) ? ( + {applyFilterButton} + + {applyFilterButton} + { + this._showFilterActions(tooltipProperty); + }} + aria-label={i18n.translate('xpack.maps.tooltip.viewActionsTitle', { + defaultMessage: 'View filter actions', + })} + data-test-subj="mapTooltipMoreActionsButton" + > + + +
{label} {}, showFilterButtons: false, + getFilterActions: () => { + return [{ id: ACTION_GLOBAL_APPLY_FILTER }]; + }, }; const mockTooltipProperties = [ @@ -44,10 +48,29 @@ const mockTooltipProperties = [ ]; describe('FeatureProperties', () => { - test('should not show filter button', async () => { + test('should render', async () => { + const component = shallow( + { + return mockTooltipProperties; + }} + /> + ); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component).toMatchSnapshot(); + }); + + test('should show filter button for filterable properties', async () => { const component = shallow( { return mockTooltipProperties; }} @@ -62,7 +85,7 @@ describe('FeatureProperties', () => { expect(component).toMatchSnapshot(); }); - test('should show only filter button for filterable properties', async () => { + test('should show view actions button when there are available actions', async () => { const component = shallow( { loadFeatureProperties={() => { return mockTooltipProperties; }} + getFilterActions={() => { + return [{ id: 'drilldown1' }]; + }} /> ); diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js index d91bc8e803ab9e..8547219b42e30d 100644 --- a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js +++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js @@ -4,20 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; -import { EuiLink } from '@elastic/eui'; +import React, { Component, Fragment } from 'react'; +import { EuiIcon, EuiLink } from '@elastic/eui'; import { FeatureProperties } from './feature_properties'; -import { FormattedMessage } from '@kbn/i18n/react'; import { GEO_JSON_TYPE, ES_GEO_FIELD_TYPE } from '../../../../common/constants'; import { FeatureGeometryFilterForm } from './feature_geometry_filter_form'; import { TooltipHeader } from './tooltip_header'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; const VIEWS = { PROPERTIES_VIEW: 'PROPERTIES_VIEW', GEOMETRY_FILTER_VIEW: 'GEOMETRY_FILTER_VIEW', + FILTER_ACTIONS_VIEW: 'FILTER_ACTIONS_VIEW', }; -export class FeaturesTooltip extends React.Component { +export class FeaturesTooltip extends Component { state = {}; static getDerivedStateFromProps(nextProps, prevState) { @@ -41,7 +43,11 @@ export class FeaturesTooltip extends React.Component { }; _showPropertiesView = () => { - this.setState({ view: VIEWS.PROPERTIES_VIEW }); + this.setState({ view: VIEWS.PROPERTIES_VIEW, filterView: null }); + }; + + _showFilterActionsView = (filterView) => { + this.setState({ view: VIEWS.FILTER_ACTIONS_VIEW, filterView }); }; _renderActions(geoFields) { @@ -96,6 +102,22 @@ export class FeaturesTooltip extends React.Component { }); }; + _renderBackButton(label) { + return ( + + ); + } + render() { if (!this.state.currentFeature) { return null; @@ -109,14 +131,36 @@ export class FeaturesTooltip extends React.Component { if (this.state.view === VIEWS.GEOMETRY_FILTER_VIEW && currentFeatureGeometry) { return ( - + + {this._renderBackButton( + i18n.translate('xpack.maps.tooltip.showGeometryFilterViewLinkLabel', { + defaultMessage: 'Filter by geometry', + }) + )} + + + ); + } + + if (this.state.view === VIEWS.FILTER_ACTIONS_VIEW) { + return ( + + {this._renderBackButton( + i18n.translate('xpack.maps.tooltip.showAddFilterActionsViewLabel', { + defaultMessage: 'Filter actions', + }) + )} + {this.state.filterView} + ); } @@ -137,6 +181,9 @@ export class FeaturesTooltip extends React.Component { showFilterButtons={!!this.props.addFilters && this.props.isLocked} onCloseTooltip={this.props.closeTooltip} addFilters={this.props.addFilters} + getFilterActions={this.props.getFilterActions} + getActionContext={this.props.getActionContext} + showFilterActions={this._showFilterActionsView} /> {this._renderActions(geoFields)} diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js index 6de936fa4a8f1f..49675ac6a39243 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js @@ -62,11 +62,12 @@ export class DrawControl extends React.Component { } }, 0); - _onDraw = (e) => { + _onDraw = async (e) => { if (!e.features.length) { return; } + let filter; if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { const circle = e.features[0]; const distanceKm = _.round( @@ -82,7 +83,7 @@ export class DrawControl extends React.Component { } else if (distanceKm <= 100) { precision = 3; } - const filter = createDistanceFilterWithMeta({ + filter = createDistanceFilterWithMeta({ alias: this.props.drawState.filterLabel, distanceKm, geoFieldName: this.props.drawState.geoFieldName, @@ -92,17 +93,12 @@ export class DrawControl extends React.Component { _.round(circle.properties.center[1], precision), ], }); - this.props.addFilters([filter]); - this.props.disableDrawState(); - return; - } - - const geometry = e.features[0].geometry; - // MapboxDraw returns coordinates with 12 decimals. Round to a more reasonable number - roundCoordinates(geometry.coordinates); + } else { + const geometry = e.features[0].geometry; + // MapboxDraw returns coordinates with 12 decimals. Round to a more reasonable number + roundCoordinates(geometry.coordinates); - try { - const filter = createSpatialFilterWithGeometry({ + filter = createSpatialFilterWithGeometry({ geometry: this.props.drawState.drawType === DRAW_TYPE.BOUNDS ? getBoundingBoxGeometry(geometry) @@ -113,7 +109,10 @@ export class DrawControl extends React.Component { geometryLabel: this.props.drawState.geometryLabel, relation: this.props.drawState.relation, }); - this.props.addFilters([filter]); + } + + try { + await this.props.addFilters([filter], this.props.drawState.actionId); } catch (error) { // TODO notify user why filter was not created console.error(error); diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js index 84a29db8525391..87d6f8e1d8e716 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js @@ -195,6 +195,8 @@ export class TooltipControl extends React.Component { mbMap={this.props.mbMap} layerList={this.props.layerList} addFilters={this.props.addFilters} + getFilterActions={this.props.getFilterActions} + getActionContext={this.props.getActionContext} renderTooltipContent={this.props.renderTooltipContent} geoFields={this.props.geoFields} features={features} diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js index 6c420576804084..4cfddf00340392 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js @@ -117,6 +117,8 @@ export class TooltipPopover extends Component { _renderTooltipContent = () => { const publicProps = { addFilters: this.props.addFilters, + getFilterActions: this.props.getFilterActions, + getActionContext: this.props.getActionContext, closeTooltip: this.props.closeTooltip, features: this.props.features, isLocked: this.props.isLocked, diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/plugins/maps/public/connected_components/map/mb/view.js index 5a38f6039ae4b3..22c374aceedd5d 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/view.js @@ -309,6 +309,8 @@ export class MBMap extends React.Component { diff --git a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx index beb1eb0947c50e..bf75c86ac249d8 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx @@ -11,6 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; import { Filter } from 'src/plugins/data/public'; +import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; // @ts-expect-error import { MBMap } from '../map/mb'; // @ts-expect-error @@ -35,7 +36,9 @@ import 'mapbox-gl/dist/mapbox-gl.css'; const RENDER_COMPLETE_EVENT = 'renderComplete'; interface Props { - addFilters: ((filters: Filter[]) => void) | null; + addFilters: ((filters: Filter[]) => Promise) | null; + getFilterActions?: () => Promise; + getActionContext?: () => ActionExecutionContext; areLayersLoaded: boolean; cancelAllInFlightRequests: () => void; exitFullScreen: () => void; @@ -183,6 +186,8 @@ export class MapContainer extends Component { render() { const { addFilters, + getFilterActions, + getActionContext, flyoutDisplay, isFullScreen, exitFullScreen, @@ -230,11 +235,18 @@ export class MapContainer extends Component { {!this.props.hideToolbarOverlay && ( - + )} diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js index a4f85163512f77..a9dc3f822060cd 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js @@ -12,14 +12,18 @@ import { FitToData } from './fit_to_data'; export class ToolbarOverlay extends React.Component { _renderToolsControl() { - const { addFilters, geoFields } = this.props; + const { addFilters, geoFields, getFilterActions, getActionContext } = this.props; if (!addFilters || !geoFields.length) { return null; } return ( - + ); } diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js index a06def086b8610..017f0369e0b73d 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js @@ -123,6 +123,8 @@ export class ToolsControl extends Component { className="mapDrawControl__geometryFilterForm" buttonLabel={DRAW_SHAPE_LABEL_SHORT} geoFields={this.props.geoFields} + getFilterActions={this.props.getFilterActions} + getActionContext={this.props.getActionContext} intitialGeometryLabel={i18n.translate( 'xpack.maps.toolbarOverlay.drawShape.initialGeometryLabel', { @@ -141,6 +143,8 @@ export class ToolsControl extends Component { className="mapDrawControl__geometryFilterForm" buttonLabel={DRAW_BOUNDS_LABEL_SHORT} geoFields={this.props.geoFields} + getFilterActions={this.props.getFilterActions} + getActionContext={this.props.getActionContext} intitialGeometryLabel={i18n.translate( 'xpack.maps.toolbarOverlay.drawBounds.initialGeometryLabel', { @@ -161,6 +165,8 @@ export class ToolsControl extends Component { geoFields={this.props.geoFields.filter(({ geoFieldType }) => { return geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT; })} + getFilterActions={this.props.getFilterActions} + getActionContext={this.props.getActionContext} onSubmit={this._initiateDistanceDraw} /> ), diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 43ff274b1353f3..1cb393bede9565 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -11,7 +11,12 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { Subscription } from 'rxjs'; import { Unsubscribe } from 'redux'; import { Embeddable, IContainer } from '../../../../../src/plugins/embeddable/public'; -import { APPLY_FILTER_TRIGGER } from '../../../../../src/plugins/ui_actions/public'; +import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public'; +import { + APPLY_FILTER_TRIGGER, + ActionExecutionContext, + TriggerContextMapping, +} from '../../../../../src/plugins/ui_actions/public'; import { esFilters, TimeRange, @@ -99,6 +104,10 @@ export class MapEmbeddable extends Embeddable this.onContainerStateChanged(input)); } + supportedTriggers(): Array { + return [APPLY_FILTER_TRIGGER]; + } + setRenderTooltipContent = (renderTooltipContent: RenderToolTipContent) => { this._renderTooltipContent = renderTooltipContent; }; @@ -226,6 +235,8 @@ export class MapEmbeddable extends Embeddable @@ -243,13 +254,36 @@ export class MapEmbeddable extends Embeddable(replaceLayerList(this._layerList)); } - addFilters = (filters: Filter[]) => { - getUiActions().executeTriggerActions(APPLY_FILTER_TRIGGER, { - embeddable: this, + addFilters = async (filters: Filter[], actionId: string = ACTION_GLOBAL_APPLY_FILTER) => { + const executeContext = { + ...this.getActionContext(), filters, + }; + const action = getUiActions().getAction(actionId); + if (!action) { + throw new Error('Unable to apply filter, could not locate action'); + } + action.execute(executeContext); + }; + + getFilterActions = async () => { + return await getUiActions().getTriggerCompatibleActions(APPLY_FILTER_TRIGGER, { + embeddable: this, + filters: [], }); }; + getActionContext = () => { + const trigger = getUiActions().getTrigger(APPLY_FILTER_TRIGGER); + if (!trigger) { + throw new Error('Unable to get context, could not locate trigger'); + } + return { + embeddable: this, + trigger, + } as ActionExecutionContext; + }; + destroy() { super.destroy(); if (this._unsubscribeFromStore) { diff --git a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js index a996910d4787a4..10754d20118e9b 100644 --- a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js +++ b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js @@ -13,31 +13,62 @@ export default function ({ getPageObjects, getService }) { const filterBar = getService('filterBar'); describe('tooltip filter actions', () => { - before(async () => { + async function loadDashboardAndOpenTooltip() { await kibanaServer.uiSettings.replace({ defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', }); await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); await PageObjects.dashboard.loadSavedDashboard('dash for tooltip filter action test'); await PageObjects.maps.lockTooltipAtPosition(200, -200); - }); + } + + describe('apply filter to current view', () => { + before(async () => { + await loadDashboardAndOpenTooltip(); + }); + + it('should display create filter button when tooltip is locked', async () => { + const exists = await testSubjects.exists('mapTooltipCreateFilterButton'); + expect(exists).to.be(true); + }); + + it('should create filters when create filter button is clicked', async () => { + await testSubjects.click('mapTooltipCreateFilterButton'); + await testSubjects.click('applyFiltersPopoverButton'); + + // TODO: Fix me #64861 + // const hasSourceFilter = await filterBar.hasFilter('name', 'charlie'); + // expect(hasSourceFilter).to.be(true); - it('should display create filter button when tooltip is locked', async () => { - const exists = await testSubjects.exists('mapTooltipCreateFilterButton'); - expect(exists).to.be(true); + const hasJoinFilter = await filterBar.hasFilter('shape_name', 'charlie'); + expect(hasJoinFilter).to.be(true); + }); }); - it('should create filters when create filter button is clicked', async () => { - await testSubjects.click('mapTooltipCreateFilterButton'); - await testSubjects.click('applyFiltersPopoverButton'); + describe('panel actions', () => { + before(async () => { + await loadDashboardAndOpenTooltip(); + }); + + it('should display more actions button when tooltip is locked', async () => { + const exists = await testSubjects.exists('mapTooltipMoreActionsButton'); + expect(exists).to.be(true); + }); + + it('should trigger drilldown action when clicked', async () => { + await testSubjects.click('mapTooltipMoreActionsButton'); + await testSubjects.click('mapFilterActionButton__drilldown1'); - // TODO: Fix me #64861 - // const hasSourceFilter = await filterBar.hasFilter('name', 'charlie'); - // expect(hasSourceFilter).to.be(true); + // Assert on new dashboard with filter from action + await PageObjects.dashboard.waitForRenderComplete(); + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.equal(2); - const hasJoinFilter = await filterBar.hasFilter('shape_name', 'charlie'); - expect(hasJoinFilter).to.be(true); + const hasJoinFilter = await filterBar.hasFilter('shape_name', 'charlie'); + expect(hasJoinFilter).to.be(true); + }); }); }); } diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index 198174bccb2863..0f1fd3c09d7065 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -1048,7 +1048,7 @@ "title" : "dash for tooltip filter action test", "hits" : 0, "description" : "Zoomed in so entire screen is covered by filter so click to open tooltip can not miss.", - "panelsJSON" : "[{\"gridData\":{\"x\":0,\"y\":0,\"w\":48,\"h\":26,\"i\":\"1\"},\"version\":\"8.0.0\",\"panelIndex\":\"1\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":-1.31919,\"lon\":59.53306,\"zoom\":9.67},\"isLayerTOCOpen\":false,\"openTOCDetails\":[\"n1t6f\"]},\"panelRefName\":\"panel_0\"}]", + "panelsJSON" : "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":48,\"h\":26,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":-1.31919,\"lon\":59.53306,\"zoom\":9.67},\"isLayerTOCOpen\":false,\"openTOCDetails\":[\"n1t6f\"],\"hiddenLayers\":[],\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"669a3521-1215-4228-9ced-77e2edf5ad17\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"drilldown1\",\"config\":{\"dashboardId\":\"19906970-2e40-11e9-85cb-6965aae20f13\",\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}}},\"panelRefName\":\"panel_0\"}]", "optionsJSON" : "{\"useMargins\":true,\"hidePanelTitles\":false}", "version" : 1, "timeRestore" : true, @@ -1071,9 +1071,9 @@ } ], "migrationVersion" : { - "dashboard" : "7.0.0" + "dashboard" : "7.3.0" }, - "updated_at" : "2019-06-14T14:09:25.039Z" + "updated_at" : "2020-08-26T14:32:27.854Z" } } } From 5f781dcfecc9bcd96d9fa5ecfe5cd3e1cdedf094 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 31 Aug 2020 08:44:27 +0200 Subject: [PATCH 098/216] add client-side feature usage API (#75486) * add client-side feature_usage API * use route context for notify feature usage route --- x-pack/plugins/licensing/public/mocks.ts | 3 + x-pack/plugins/licensing/public/plugin.ts | 5 +- .../services/feature_usage_service.mock.ts | 45 ++++++++++ .../services/feature_usage_service.test.ts | 69 ++++++++++++++++ .../public/services/feature_usage_service.ts | 68 +++++++++++++++ .../licensing/public/services/index.ts | 11 +++ x-pack/plugins/licensing/public/types.ts | 9 ++ x-pack/plugins/licensing/server/plugin.ts | 6 +- .../plugins/licensing/server/routes/index.ts | 5 ++ .../licensing/server/routes/internal/index.ts | 8 ++ .../routes/internal/notify_feature_usage.ts | 32 ++++++++ .../routes/internal/register_feature.ts | 43 ++++++++++ .../licensing_plugin/public/feature_usage.ts | 82 +++++++++++++++++++ x-pack/test/licensing_plugin/public/index.ts | 1 + 14 files changed, 384 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/licensing/public/services/feature_usage_service.mock.ts create mode 100644 x-pack/plugins/licensing/public/services/feature_usage_service.test.ts create mode 100644 x-pack/plugins/licensing/public/services/feature_usage_service.ts create mode 100644 x-pack/plugins/licensing/public/services/index.ts create mode 100644 x-pack/plugins/licensing/server/routes/internal/index.ts create mode 100644 x-pack/plugins/licensing/server/routes/internal/notify_feature_usage.ts create mode 100644 x-pack/plugins/licensing/server/routes/internal/register_feature.ts create mode 100644 x-pack/test/licensing_plugin/public/feature_usage.ts diff --git a/x-pack/plugins/licensing/public/mocks.ts b/x-pack/plugins/licensing/public/mocks.ts index 8421a343d91ca8..1ddde892de0d9b 100644 --- a/x-pack/plugins/licensing/public/mocks.ts +++ b/x-pack/plugins/licensing/public/mocks.ts @@ -6,12 +6,14 @@ import { BehaviorSubject } from 'rxjs'; import { LicensingPluginSetup, LicensingPluginStart } from './types'; import { licenseMock } from '../common/licensing.mock'; +import { featureUsageMock } from './services/feature_usage_service.mock'; const createSetupMock = () => { const license = licenseMock.createLicense(); const mock: jest.Mocked = { license$: new BehaviorSubject(license), refresh: jest.fn(), + featureUsage: featureUsageMock.createSetup(), }; mock.refresh.mockResolvedValue(license); @@ -23,6 +25,7 @@ const createStartMock = () => { const mock: jest.Mocked = { license$: new BehaviorSubject(license), refresh: jest.fn(), + featureUsage: featureUsageMock.createStart(), }; mock.refresh.mockResolvedValue(license); diff --git a/x-pack/plugins/licensing/public/plugin.ts b/x-pack/plugins/licensing/public/plugin.ts index ec42a73f610c02..aa0c25364f2c7d 100644 --- a/x-pack/plugins/licensing/public/plugin.ts +++ b/x-pack/plugins/licensing/public/plugin.ts @@ -6,12 +6,12 @@ import { Observable, Subject, Subscription } from 'rxjs'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; - import { ILicense } from '../common/types'; import { LicensingPluginSetup, LicensingPluginStart } from './types'; import { createLicenseUpdate } from '../common/license_update'; import { License } from '../common/license'; import { mountExpiredBanner } from './expired_banner'; +import { FeatureUsageService } from './services'; export const licensingSessionStorageKey = 'xpack.licensing'; @@ -39,6 +39,7 @@ export class LicensingPlugin implements Plugin Promise; private license$?: Observable; + private featureUsage = new FeatureUsageService(); constructor( context: PluginInitializerContext, @@ -116,6 +117,7 @@ export class LicensingPlugin implements Plugin => { + const mock = { + register: jest.fn(), + }; + + return mock; +}; + +const createStartMock = (): jest.Mocked => { + const mock = { + notifyUsage: jest.fn(), + }; + + return mock; +}; + +const createServiceMock = (): jest.Mocked> => { + const mock = { + setup: jest.fn(), + start: jest.fn(), + }; + + mock.setup.mockImplementation(() => createSetupMock()); + mock.start.mockImplementation(() => createStartMock()); + + return mock; +}; + +export const featureUsageMock = { + create: createServiceMock, + createSetup: createSetupMock, + createStart: createStartMock, +}; diff --git a/x-pack/plugins/licensing/public/services/feature_usage_service.test.ts b/x-pack/plugins/licensing/public/services/feature_usage_service.test.ts new file mode 100644 index 00000000000000..eba2d1e67b509f --- /dev/null +++ b/x-pack/plugins/licensing/public/services/feature_usage_service.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { httpServiceMock } from '../../../../../src/core/public/mocks'; +import { FeatureUsageService } from './feature_usage_service'; + +describe('FeatureUsageService', () => { + let http: ReturnType; + let service: FeatureUsageService; + + beforeEach(() => { + http = httpServiceMock.createSetupContract(); + service = new FeatureUsageService(); + }); + + describe('#setup', () => { + describe('#register', () => { + it('calls the endpoint with the correct parameters', async () => { + const setup = service.setup({ http }); + await setup.register('my-feature', 'platinum'); + expect(http.post).toHaveBeenCalledTimes(1); + expect(http.post).toHaveBeenCalledWith('/internal/licensing/feature_usage/register', { + body: JSON.stringify({ + featureName: 'my-feature', + licenseType: 'platinum', + }), + }); + }); + }); + }); + + describe('#start', () => { + describe('#notifyUsage', () => { + it('calls the endpoint with the correct parameters', async () => { + service.setup({ http }); + const start = service.start({ http }); + await start.notifyUsage('my-feature', 42); + + expect(http.post).toHaveBeenCalledTimes(1); + expect(http.post).toHaveBeenCalledWith('/internal/licensing/feature_usage/notify', { + body: JSON.stringify({ + featureName: 'my-feature', + lastUsed: 42, + }), + }); + }); + + it('correctly convert dates', async () => { + service.setup({ http }); + const start = service.start({ http }); + + const now = new Date(); + + await start.notifyUsage('my-feature', now); + + expect(http.post).toHaveBeenCalledTimes(1); + expect(http.post).toHaveBeenCalledWith('/internal/licensing/feature_usage/notify', { + body: JSON.stringify({ + featureName: 'my-feature', + lastUsed: now.getTime(), + }), + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/licensing/public/services/feature_usage_service.ts b/x-pack/plugins/licensing/public/services/feature_usage_service.ts new file mode 100644 index 00000000000000..588d22eeb818d7 --- /dev/null +++ b/x-pack/plugins/licensing/public/services/feature_usage_service.ts @@ -0,0 +1,68 @@ +/* + * 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 isDate from 'lodash/isDate'; +import type { HttpSetup, HttpStart } from 'src/core/public'; +import { LicenseType } from '../../common/types'; + +/** @public */ +export interface FeatureUsageServiceSetup { + /** + * Register a feature to be able to notify of it's usages using the {@link FeatureUsageServiceStart | service start contract}. + */ + register(featureName: string, licenseType: LicenseType): Promise; +} + +/** @public */ +export interface FeatureUsageServiceStart { + /** + * Notify of a registered feature usage at given time. + * + * @param featureName - the name of the feature to notify usage of + * @param usedAt - Either a `Date` or an unix timestamp with ms. If not specified, it will be set to the current time. + */ + notifyUsage(featureName: string, usedAt?: Date | number): Promise; +} + +interface SetupDeps { + http: HttpSetup; +} + +interface StartDeps { + http: HttpStart; +} + +/** + * @internal + */ +export class FeatureUsageService { + public setup({ http }: SetupDeps): FeatureUsageServiceSetup { + return { + register: async (featureName, licenseType) => { + await http.post('/internal/licensing/feature_usage/register', { + body: JSON.stringify({ + featureName, + licenseType, + }), + }); + }, + }; + } + + public start({ http }: StartDeps): FeatureUsageServiceStart { + return { + notifyUsage: async (featureName, usedAt = Date.now()) => { + const lastUsed = isDate(usedAt) ? usedAt.getTime() : usedAt; + await http.post('/internal/licensing/feature_usage/notify', { + body: JSON.stringify({ + featureName, + lastUsed, + }), + }); + }, + }; + } +} diff --git a/x-pack/plugins/licensing/public/services/index.ts b/x-pack/plugins/licensing/public/services/index.ts new file mode 100644 index 00000000000000..fc890dd3c927d6 --- /dev/null +++ b/x-pack/plugins/licensing/public/services/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + FeatureUsageService, + FeatureUsageServiceSetup, + FeatureUsageServiceStart, +} from './feature_usage_service'; diff --git a/x-pack/plugins/licensing/public/types.ts b/x-pack/plugins/licensing/public/types.ts index 71a4a452d163d5..43b146c51d9a8f 100644 --- a/x-pack/plugins/licensing/public/types.ts +++ b/x-pack/plugins/licensing/public/types.ts @@ -6,6 +6,7 @@ import { Observable } from 'rxjs'; import { ILicense } from '../common/types'; +import { FeatureUsageServiceSetup, FeatureUsageServiceStart } from './services'; /** @public */ export interface LicensingPluginSetup { @@ -19,6 +20,10 @@ export interface LicensingPluginSetup { * @deprecated in favour of the counterpart provided from start contract */ refresh(): Promise; + /** + * APIs to register licensed feature usage. + */ + featureUsage: FeatureUsageServiceSetup; } /** @public */ @@ -31,4 +36,8 @@ export interface LicensingPluginStart { * Triggers licensing information re-fetch. */ refresh(): Promise; + /** + * APIs to manage licensed feature usage. + */ + featureUsage: FeatureUsageServiceStart; } diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index 6cdba0ac466446..2ee8d264195717 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -133,7 +133,9 @@ export class LicensingPlugin implements Plugin ) { registerInfoRoute(router); registerFeatureUsageRoute(router, getStartServices); + registerRegisterFeatureRoute(router, featureUsageSetup); + registerNotifyFeatureUsageRoute(router); } diff --git a/x-pack/plugins/licensing/server/routes/internal/index.ts b/x-pack/plugins/licensing/server/routes/internal/index.ts new file mode 100644 index 00000000000000..a3b06c223fc125 --- /dev/null +++ b/x-pack/plugins/licensing/server/routes/internal/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { registerNotifyFeatureUsageRoute } from './notify_feature_usage'; +export { registerRegisterFeatureRoute } from './register_feature'; diff --git a/x-pack/plugins/licensing/server/routes/internal/notify_feature_usage.ts b/x-pack/plugins/licensing/server/routes/internal/notify_feature_usage.ts new file mode 100644 index 00000000000000..ec70472574be3f --- /dev/null +++ b/x-pack/plugins/licensing/server/routes/internal/notify_feature_usage.ts @@ -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 { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; + +export function registerNotifyFeatureUsageRoute(router: IRouter) { + router.post( + { + path: '/internal/licensing/feature_usage/notify', + validate: { + body: schema.object({ + featureName: schema.string(), + lastUsed: schema.number(), + }), + }, + }, + async (context, request, response) => { + const { featureName, lastUsed } = request.body; + + context.licensing.featureUsage.notifyUsage(featureName, lastUsed); + + return response.ok({ + body: { + success: true, + }, + }); + } + ); +} diff --git a/x-pack/plugins/licensing/server/routes/internal/register_feature.ts b/x-pack/plugins/licensing/server/routes/internal/register_feature.ts new file mode 100644 index 00000000000000..14f7952f86f5ac --- /dev/null +++ b/x-pack/plugins/licensing/server/routes/internal/register_feature.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; +import { LicenseType, LICENSE_TYPE } from '../../../common/types'; +import { FeatureUsageServiceSetup } from '../../services'; + +export function registerRegisterFeatureRoute( + router: IRouter, + featureUsageSetup: FeatureUsageServiceSetup +) { + router.post( + { + path: '/internal/licensing/feature_usage/register', + validate: { + body: schema.object({ + featureName: schema.string(), + licenseType: schema.string({ + validate: (value) => { + if (!(value in LICENSE_TYPE)) { + return `Invalid license type: ${value}`; + } + }, + }), + }), + }, + }, + async (context, request, response) => { + const { featureName, licenseType } = request.body; + + featureUsageSetup.register(featureName, licenseType as LicenseType); + + return response.ok({ + body: { + success: true, + }, + }); + } + ); +} diff --git a/x-pack/test/licensing_plugin/public/feature_usage.ts b/x-pack/test/licensing_plugin/public/feature_usage.ts new file mode 100644 index 00000000000000..15d302d71bfabf --- /dev/null +++ b/x-pack/test/licensing_plugin/public/feature_usage.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../services'; +import { + LicensingPluginSetup, + LicensingPluginStart, + LicenseType, +} from '../../../plugins/licensing/public'; +import '../../../../test/plugin_functional/plugins/core_provider_plugin/types'; + +interface FeatureUsage { + last_used?: number; + license_level: LicenseType; + name: string; +} + +// eslint-disable-next-line import/no-default-export +export default function (ftrContext: FtrProviderContext) { + const { getService, getPageObjects } = ftrContext; + const supertest = getService('supertest'); + const browser = getService('browser'); + const PageObjects = getPageObjects(['common', 'security']); + + const registerFeature = async (featureName: string, licenseType: LicenseType) => { + await browser.executeAsync( + async (feature, type, cb) => { + const { setup } = window._coreProvider; + const licensing: LicensingPluginSetup = setup.plugins.licensing; + await licensing.featureUsage.register(feature, type); + cb(); + }, + featureName, + licenseType + ); + }; + + const notifyFeatureUsage = async (featureName: string, lastUsed: number) => { + await browser.executeAsync( + async (feature, time, cb) => { + const { start } = window._coreProvider; + const licensing: LicensingPluginStart = start.plugins.licensing; + await licensing.featureUsage.notifyUsage(feature, time); + cb(); + }, + featureName, + lastUsed + ); + }; + + describe('feature_usage API', () => { + before(async () => { + await PageObjects.security.login(); + }); + + it('allows to register features to the server', async () => { + await registerFeature('test-client-A', 'gold'); + await registerFeature('test-client-B', 'enterprise'); + + const response = await supertest.get('/api/licensing/feature_usage').expect(200); + const features = response.body.features.map(({ name }: FeatureUsage) => name); + + expect(features).to.contain('test-client-A'); + expect(features).to.contain('test-client-B'); + }); + + it('allows to notify feature usage', async () => { + const now = new Date(); + + await notifyFeatureUsage('test-client-A', now.getTime()); + + const response = await supertest.get('/api/licensing/feature_usage').expect(200); + const features = response.body.features as FeatureUsage[]; + + expect(features.find((f) => f.name === 'test-client-A')?.last_used).to.be(now.toISOString()); + expect(features.find((f) => f.name === 'test-client-B')?.last_used).to.be(null); + }); + }); +} diff --git a/x-pack/test/licensing_plugin/public/index.ts b/x-pack/test/licensing_plugin/public/index.ts index 86a3c21cfdb395..268a74c56bd72f 100644 --- a/x-pack/test/licensing_plugin/public/index.ts +++ b/x-pack/test/licensing_plugin/public/index.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../services'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Licensing plugin public client', function () { this.tags('ciGroup2'); + loadTestFile(require.resolve('./feature_usage')); // MUST BE LAST! CHANGES LICENSE TYPE! loadTestFile(require.resolve('./updates')); }); From 9ddd49a9f0d080dda5161eab0dd4f48d7baa9ebd Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 31 Aug 2020 12:40:44 +0300 Subject: [PATCH 099/216] Hides advanced json for count metric (#74636) * remove advanced json for count agg * Remove only advanced json from count agg * use Constant from data plugin * add the logic to data plugin * remove json arg from function definition * remove unecessary translations Co-authored-by: Elastic Machine --- .../data/common/search/aggs/agg_type.test.ts | 11 +++++++++++ src/plugins/data/common/search/aggs/agg_type.ts | 17 +++++++++++------ .../data/common/search/aggs/metrics/count.ts | 1 + .../common/search/aggs/metrics/count_fn.test.ts | 14 -------------- .../data/common/search/aggs/metrics/count_fn.ts | 12 +----------- .../public/components/agg_params_helper.ts | 2 +- .../apps/visualize/_point_series_options.js | 5 +++++ .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 9 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/plugins/data/common/search/aggs/agg_type.test.ts b/src/plugins/data/common/search/aggs/agg_type.test.ts index 2fcc6b97b1cc6f..bf1136159dfe86 100644 --- a/src/plugins/data/common/search/aggs/agg_type.test.ts +++ b/src/plugins/data/common/search/aggs/agg_type.test.ts @@ -99,6 +99,17 @@ describe('AggType Class', () => { expect(aggType.params[1].name).toBe('customLabel'); }); + test('disables json param', () => { + const aggType = new AggType({ + name: 'name', + title: 'title', + json: false, + }); + + expect(aggType.params.length).toBe(1); + expect(aggType.params[0].name).toBe('customLabel'); + }); + test('can disable customLabel', () => { const aggType = new AggType({ name: 'smart agg', diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts index 0ba2bb66e77581..2ee604c1bf25da 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -47,6 +47,7 @@ export interface AggTypeConfig< getRequestAggs?: ((aggConfig: TAggConfig) => TAggConfig[]) | (() => TAggConfig[] | void); getResponseAggs?: ((aggConfig: TAggConfig) => TAggConfig[]) | (() => TAggConfig[] | void); customLabels?: boolean; + json?: boolean; decorateAggConfig?: () => any; postFlightRequest?: ( resp: any, @@ -235,13 +236,17 @@ export class AggType< if (config.params && config.params.length && config.params[0] instanceof BaseParamType) { this.params = config.params as TParam[]; } else { - // always append the raw JSON param + // always append the raw JSON param unless it is configured to false const params: any[] = config.params ? [...config.params] : []; - params.push({ - name: 'json', - type: 'json', - advanced: true, - }); + + if (config.json !== false) { + params.push({ + name: 'json', + type: 'json', + advanced: true, + }); + } + // always append custom label if (config.customLabels !== false) { diff --git a/src/plugins/data/common/search/aggs/metrics/count.ts b/src/plugins/data/common/search/aggs/metrics/count.ts index d990599586e81c..9c9f36651f4d23 100644 --- a/src/plugins/data/common/search/aggs/metrics/count.ts +++ b/src/plugins/data/common/search/aggs/metrics/count.ts @@ -28,6 +28,7 @@ export const getCountMetricAgg = () => defaultMessage: 'Count', }), hasNoDsl: true, + json: false, makeLabel() { return i18n.translate('data.search.aggs.metrics.countLabel', { defaultMessage: 'Count', diff --git a/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts index 846feb9296fca2..32189f07581e6e 100644 --- a/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts @@ -34,7 +34,6 @@ describe('agg_expression_functions', () => { "id": undefined, "params": Object { "customLabel": undefined, - "json": undefined, }, "schema": undefined, "type": "count", @@ -42,18 +41,5 @@ describe('agg_expression_functions', () => { } `); }); - - test('correctly parses json string argument', () => { - const actual = fn({ - json: '{ "foo": true }', - }); - - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); - }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/count_fn.ts b/src/plugins/data/common/search/aggs/metrics/count_fn.ts index 338ca18209299f..7d4616ffdc6191 100644 --- a/src/plugins/data/common/search/aggs/metrics/count_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/count_fn.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; const fnName = 'aggCount'; @@ -55,12 +54,6 @@ export const aggCount = (): FunctionDefinition => ({ defaultMessage: 'Schema to use for this aggregation', }), }, - json: { - types: ['string'], - help: i18n.translate('data.search.aggs.metrics.count.json.help', { - defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', - }), - }, customLabel: { types: ['string'], help: i18n.translate('data.search.aggs.metrics.count.customLabel.help', { @@ -78,10 +71,7 @@ export const aggCount = (): FunctionDefinition => ({ enabled, schema, type: METRIC_TYPES.COUNT, - params: { - ...rest, - json: getParsedValue(args, 'json'), - }, + params: rest, }, }; }, diff --git a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts index ef2f937c8547cf..b13ca32601aa95 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts @@ -26,7 +26,7 @@ import { IAggType, IndexPattern, IndexPatternField, -} from 'src/plugins/data/public'; +} from '../../../data/public'; import { filterAggTypes, filterAggTypeFields } from '../agg_filters'; import { groupAndSortBy, ComboBoxGroupedOptions } from '../utils'; import { AggTypeState, AggParamsState } from './agg_params_state'; diff --git a/test/functional/apps/visualize/_point_series_options.js b/test/functional/apps/visualize/_point_series_options.js index d08bfe3b909139..c88670ee8b7414 100644 --- a/test/functional/apps/visualize/_point_series_options.js +++ b/test/functional/apps/visualize/_point_series_options.js @@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }) { const retry = getService('retry'); const kibanaServer = getService('kibanaServer'); const browser = getService('browser'); + const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects([ 'visualize', 'header', @@ -148,6 +149,10 @@ export default function ({ getService, getPageObjects }) { }); }); + it('should not show advanced json for count agg', async function () { + await testSubjects.missingOrFail('advancedParams-1'); + }); + it('should put secondary axis on the right', async function () { const length = await PageObjects.visChart.getRightValueAxes(); expect(length).to.be(1); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index dc07044ce8ed72..07b646df74b9f5 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1115,7 +1115,6 @@ "data.search.aggs.metrics.count.customLabel.help": "このアグリゲーションのカスタムラベルを表します", "data.search.aggs.metrics.count.enabled.help": "このアグリゲーションが有効かどうかを指定します", "data.search.aggs.metrics.count.id.help": "このアグリゲーションのID", - "data.search.aggs.metrics.count.json.help": "アグリゲーションがElasticsearchに送信されるときに含める高度なJSON", "data.search.aggs.metrics.count.schema.help": "このアグリゲーションで使用するスキーマ", "data.search.aggs.metrics.countLabel": "カウント", "data.search.aggs.metrics.countTitle": "カウント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 144bc1cac18526..ffd7d0cfb0f87f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1116,7 +1116,6 @@ "data.search.aggs.metrics.count.customLabel.help": "表示此聚合的定制标签", "data.search.aggs.metrics.count.enabled.help": "指定是否启用此聚合", "data.search.aggs.metrics.count.id.help": "此聚合的 ID", - "data.search.aggs.metrics.count.json.help": "聚合发送至 Elasticsearch 时要包括的高级 json", "data.search.aggs.metrics.count.schema.help": "要用于此聚合的方案", "data.search.aggs.metrics.countLabel": "计数", "data.search.aggs.metrics.countTitle": "计数", From aac57fb1a8494d386b37bdfe21ada3cd9c8d7494 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 31 Aug 2020 11:45:57 +0200 Subject: [PATCH 100/216] [ILM] Add support for frozen phase in UI (#75968) * ILM public side changes to support frozen phase This is a very faithful duplication of the cold phase. We are also excluding the snapshot action as well as the unfollow action as these are features that require higher than basic license privilege. * added "frozen" to the server side schema * add frozen phases component * fix i18n and update jest tests * Slight restructuring to phase types and fix copy paste issues. - also fixed TS error introduced after TS v4 upgrade (delete) * fix hot phase type and remove type constraint from error messages * update validation logic Co-authored-by: Elastic Machine --- .../extend_index_management.test.js.snap | 4 + .../public/application/constants/policy.ts | 11 + .../edit_policy/components/min_age_input.tsx | 11 +- .../components/node_allocation.tsx | 6 +- .../components/set_priority_input.tsx | 6 +- .../sections/edit_policy/edit_policy.tsx | 16 +- .../edit_policy/phases/cold_phase.tsx | 7 +- .../edit_policy/phases/delete_phase.tsx | 7 +- .../edit_policy/phases/frozen_phase.tsx | 210 ++++++++++++++++++ .../sections/edit_policy/phases/hot_phase.tsx | 7 +- .../sections/edit_policy/phases/index.ts | 1 + .../edit_policy/phases/warm_phase.tsx | 7 +- .../services/policies/cold_phase.ts | 4 +- .../services/policies/frozen_phase.ts | 161 ++++++++++++++ .../services/policies/policy_serialization.ts | 11 + .../services/policies/policy_validation.ts | 18 +- .../application/services/policies/types.ts | 66 ++++-- .../public/extend_index_management/index.js | 6 + .../api/policies/register_create_route.ts | 18 ++ 19 files changed, 527 insertions(+), 50 deletions(-) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/frozen_phase.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap index 38dd49a286b588..39eb54b941ac4b 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap @@ -76,6 +76,10 @@ Array [ "value": "warm", "view": "Warm", }, + Object { + "value": "frozen", + "view": "Frozen", + }, Object { "value": "cold", "view": "Cold", diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts index 3a19f03547b5b2..fb626e7d7fe769 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts @@ -10,6 +10,7 @@ import { DeletePhase, HotPhase, WarmPhase, + FrozenPhase, } from '../services/policies/types'; export const defaultNewHotPhase: HotPhase = { @@ -47,6 +48,16 @@ export const defaultNewColdPhase: ColdPhase = { phaseIndexPriority: '0', }; +export const defaultNewFrozenPhase: FrozenPhase = { + phaseEnabled: false, + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + selectedNodeAttrs: '', + selectedReplicaCount: '', + freezeEnabled: false, + phaseIndexPriority: '0', +}; + export const defaultNewDeletePhase: DeletePhase = { phaseEnabled: false, selectedMinimumAge: '0', diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx index 11b743ecc4bb65..5128ba1c881a0c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx @@ -12,7 +12,7 @@ import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from import { LearnMoreLink } from './learn_more_link'; import { ErrableFormRow } from './form_errors'; import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; -import { ColdPhase, DeletePhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; +import { PhaseWithMinAge, Phases } from '../../../services/policies/types'; function getTimingLabelForPhase(phase: keyof Phases) { // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. @@ -27,6 +27,11 @@ function getTimingLabelForPhase(phase: keyof Phases) { defaultMessage: 'Timing for cold phase', }); + case 'frozen': + return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseFrozen.minimumAgeLabel', { + defaultMessage: 'Timing for frozen phase', + }); + case 'delete': return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeLabel', { defaultMessage: 'Timing for delete phase', @@ -63,7 +68,7 @@ function getUnitsAriaLabelForPhase(phase: keyof Phases) { } } -interface Props { +interface Props { rolloverEnabled: boolean; errors?: PhaseValidationErrors; phase: keyof Phases & string; @@ -72,7 +77,7 @@ interface Props { isShowingErrors: boolean; } -export const MinAgeInput = ({ +export const MinAgeInput = ({ rolloverEnabled, errors, phaseData, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx index 0ce2c0d7ea5663..b4ff62bfb03dcd 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx @@ -20,7 +20,7 @@ import { LearnMoreLink } from './learn_more_link'; import { ErrableFormRow } from './form_errors'; import { useLoadNodes } from '../../../services/api'; import { NodeAttrsDetails } from './node_attrs_details'; -import { ColdPhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; +import { PhaseWithAllocationAction, Phases } from '../../../services/policies/types'; import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; const learnMoreLink = ( @@ -38,14 +38,14 @@ const learnMoreLink = ( ); -interface Props { +interface Props { phase: keyof Phases & string; errors?: PhaseValidationErrors; phaseData: T; setPhaseData: (dataKey: keyof T & string, value: string) => void; isShowingErrors: boolean; } -export const NodeAllocation = ({ +export const NodeAllocation = ({ phase, setPhaseData, errors, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx index 1da7508049f247..1505532a2b16e7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx @@ -10,17 +10,17 @@ import { EuiFieldNumber, EuiTextColor, EuiDescribedFormGroup } from '@elastic/eu import { LearnMoreLink } from './'; import { OptionalLabel } from './'; import { ErrableFormRow } from './'; -import { ColdPhase, HotPhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; +import { PhaseWithIndexPriority, Phases } from '../../../services/policies/types'; import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; -interface Props { +interface Props { errors?: PhaseValidationErrors; phase: keyof Phases & string; phaseData: T; setPhaseData: (dataKey: keyof T & string, value: any) => void; isShowingErrors: boolean; } -export const SetPriorityInput = ({ +export const SetPriorityInput = ({ errors, phaseData, phase, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index c99d01b5466792..db58c64a8ae8cd 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -28,7 +28,7 @@ import { import { toasts } from '../../services/notification'; -import { Policy, PolicyFromES } from '../../services/policies/types'; +import { Phases, Policy, PolicyFromES } from '../../services/policies/types'; import { validatePolicy, ValidationErrors, @@ -42,7 +42,7 @@ import { } from '../../services/policies/policy_serialization'; import { ErrableFormRow, LearnMoreLink, PolicyJsonFlyout } from './components'; -import { ColdPhase, DeletePhase, HotPhase, WarmPhase } from './phases'; +import { ColdPhase, DeletePhase, FrozenPhase, HotPhase, WarmPhase } from './phases'; interface Props { policies: PolicyFromES[]; @@ -118,7 +118,7 @@ export const EditPolicy: React.FunctionComponent = ({ setIsShowingPolicyJsonFlyout(!isShowingPolicyJsonFlyout); }; - const setPhaseData = (phase: 'hot' | 'warm' | 'cold' | 'delete', key: string, value: any) => { + const setPhaseData = (phase: keyof Phases, key: string, value: any) => { setPolicy({ ...policy, phases: { @@ -303,6 +303,16 @@ export const EditPolicy: React.FunctionComponent = ({ + 0} + setPhaseData={(key, value) => setPhaseData('frozen', key, value)} + phaseData={policy.phases.frozen} + hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} + /> + + + 0} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx index fb32752fe24ea6..9df6da7a88b2f8 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/eui'; import { ColdPhase as ColdPhaseInterface, Phases } from '../../../services/policies/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; import { LearnMoreLink, @@ -36,9 +36,8 @@ const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeInd defaultMessage: 'Freeze index', }); -const coldProperty = propertyof('cold'); -const phaseProperty = (propertyName: keyof ColdPhaseInterface) => - propertyof(propertyName); +const coldProperty: keyof Phases = 'cold'; +const phaseProperty = (propertyName: keyof ColdPhaseInterface) => propertyName; interface Props { setPhaseData: (key: keyof ColdPhaseInterface & string, value: string | boolean) => void; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx index d3c73090f25f2d..eab93777a72bde 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx @@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui'; import { DeletePhase as DeletePhaseInterface, Phases } from '../../../services/policies/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; import { ActiveBadge, @@ -20,9 +20,8 @@ import { SnapshotPolicies, } from '../components'; -const deleteProperty = propertyof('delete'); -const phaseProperty = (propertyName: keyof DeletePhaseInterface) => - propertyof(propertyName); +const deleteProperty: keyof Phases = 'delete'; +const phaseProperty = (propertyName: keyof DeletePhaseInterface) => propertyName; interface Props { setPhaseData: (key: keyof DeletePhaseInterface & string, value: string | boolean) => void; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx new file mode 100644 index 00000000000000..782906a56a9baf --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx @@ -0,0 +1,210 @@ +/* + * 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, { PureComponent, Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiFieldNumber, + EuiDescribedFormGroup, + EuiSwitch, + EuiTextColor, +} from '@elastic/eui'; + +import { FrozenPhase as FrozenPhaseInterface, Phases } from '../../../services/policies/types'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; + +import { + LearnMoreLink, + ActiveBadge, + PhaseErrorMessage, + OptionalLabel, + ErrableFormRow, + MinAgeInput, + NodeAllocation, + SetPriorityInput, +} from '../components'; + +const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.frozenPhase.freezeIndexLabel', { + defaultMessage: 'Freeze index', +}); + +const frozenProperty: keyof Phases = 'frozen'; +const phaseProperty = (propertyName: keyof FrozenPhaseInterface) => propertyName; + +interface Props { + setPhaseData: (key: keyof FrozenPhaseInterface & string, value: string | boolean) => void; + phaseData: FrozenPhaseInterface; + isShowingErrors: boolean; + errors?: PhaseValidationErrors; + hotPhaseRolloverEnabled: boolean; +} +export class FrozenPhase extends PureComponent { + render() { + const { + setPhaseData, + phaseData, + errors, + isShowingErrors, + hotPhaseRolloverEnabled, + } = this.props; + + return ( +
+ +

+ +

{' '} + {phaseData.phaseEnabled && !isShowingErrors ? : null} + +
+ } + titleSize="s" + description={ + +

+ +

+ + } + id={`${frozenProperty}-${phaseProperty('phaseEnabled')}`} + checked={phaseData.phaseEnabled} + onChange={(e) => { + setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); + }} + aria-controls="frozenPhaseContent" + /> +
+ } + fullWidth + > + + {phaseData.phaseEnabled ? ( + + + errors={errors} + phaseData={phaseData} + phase={frozenProperty} + isShowingErrors={isShowingErrors} + setPhaseData={setPhaseData} + rolloverEnabled={hotPhaseRolloverEnabled} + /> + + + + phase={frozenProperty} + setPhaseData={setPhaseData} + errors={errors} + phaseData={phaseData} + isShowingErrors={isShowingErrors} + /> + + + + + + + + } + isShowingErrors={isShowingErrors} + errors={errors?.freezeEnabled} + helpText={i18n.translate( + 'xpack.indexLifecycleMgmt.frozenPhase.replicaCountHelpText', + { + defaultMessage: 'By default, the number of replicas remains the same.', + } + )} + > + { + setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value); + }} + min={0} + /> + + + + + ) : ( +
+ )} + + + {phaseData.phaseEnabled ? ( + + + + + } + description={ + + {' '} + + + } + fullWidth + titleSize="xs" + > + { + setPhaseData(phaseProperty('freezeEnabled'), e.target.checked); + }} + label={freezeLabel} + aria-label={freezeLabel} + /> + + + errors={errors} + phaseData={phaseData} + phase={frozenProperty} + isShowingErrors={isShowingErrors} + setPhaseData={setPhaseData} + /> + + ) : null} +
+ ); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx index 22f0114d16afe4..106e3b9139a9b1 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/eui'; import { HotPhase as HotPhaseInterface, Phases } from '../../../services/policies/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; import { LearnMoreLink, @@ -112,9 +112,8 @@ const maxAgeUnits = [ }), }, ]; -const hotProperty = propertyof('hot'); -const phaseProperty = (propertyName: keyof HotPhaseInterface) => - propertyof(propertyName); +const hotProperty: keyof Phases = 'hot'; +const phaseProperty = (propertyName: keyof HotPhaseInterface) => propertyName; interface Props { errors?: PhaseValidationErrors; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts index 8d1ace5950497b..d59f2ff6413fd4 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts @@ -7,4 +7,5 @@ export { HotPhase } from './hot_phase'; export { WarmPhase } from './warm_phase'; export { ColdPhase } from './cold_phase'; +export { FrozenPhase } from './frozen_phase'; export { DeletePhase } from './delete_phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx index f7b8c60a5c71f1..2733d01ac222d3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx @@ -30,7 +30,7 @@ import { } from '../components'; import { Phases, WarmPhase as WarmPhaseInterface } from '../../../services/policies/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; const shrinkLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', { defaultMessage: 'Shrink index', @@ -47,9 +47,8 @@ const forcemergeLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.force defaultMessage: 'Force merge data', }); -const warmProperty = propertyof('warm'); -const phaseProperty = (propertyName: keyof WarmPhaseInterface) => - propertyof(propertyName); +const warmProperty: keyof Phases = 'warm'; +const phaseProperty = (propertyName: keyof WarmPhaseInterface) => propertyName; interface Props { setPhaseData: (key: keyof WarmPhaseInterface & string, value: boolean | string) => void; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts index 6cc43042ed4ff0..7fa82a004b872e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts @@ -152,9 +152,9 @@ export const validateColdPhase = (phase: ColdPhase): PhaseValidationErrors { + const phase = { ...frozenPhaseInitialization }; + if (phaseSerialized === undefined || phaseSerialized === null) { + return phase; + } + + phase.phaseEnabled = true; + + if (phaseSerialized.min_age) { + const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); + phase.selectedMinimumAge = minAge; + phase.selectedMinimumAgeUnits = minAgeUnits; + } + + if (phaseSerialized.actions) { + const actions = phaseSerialized.actions; + if (actions.allocate) { + const allocate = actions.allocate; + if (allocate.require) { + Object.entries(allocate.require).forEach((entry) => { + phase.selectedNodeAttrs = entry.join(':'); + }); + if (allocate.number_of_replicas) { + phase.selectedReplicaCount = allocate.number_of_replicas.toString(); + } + } + } + + if (actions.freeze) { + phase.freezeEnabled = true; + } + + if (actions.set_priority) { + phase.phaseIndexPriority = actions.set_priority.priority + ? actions.set_priority.priority.toString() + : ''; + } + } + + return phase; +}; + +export const frozenPhaseToES = ( + phase: FrozenPhase, + originalPhase?: SerializedFrozenPhase +): SerializedFrozenPhase => { + if (!originalPhase) { + originalPhase = { ...serializedPhaseInitialization }; + } + + const esPhase = { ...originalPhase }; + + if (isNumber(phase.selectedMinimumAge)) { + esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; + } + + esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; + + if (phase.selectedNodeAttrs) { + const [name, value] = phase.selectedNodeAttrs.split(':'); + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.require = { + [name]: value, + }; + } else { + if (esPhase.actions.allocate) { + // @ts-expect-error + delete esPhase.actions.allocate.require; + } + } + + if (isNumber(phase.selectedReplicaCount)) { + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.number_of_replicas = parseInt(phase.selectedReplicaCount, 10); + } else { + if (esPhase.actions.allocate) { + // @ts-expect-error + delete esPhase.actions.allocate.number_of_replicas; + } + } + + if ( + esPhase.actions.allocate && + !esPhase.actions.allocate.require && + !isNumber(esPhase.actions.allocate.number_of_replicas) && + isEmpty(esPhase.actions.allocate.include) && + isEmpty(esPhase.actions.allocate.exclude) + ) { + // remove allocate action if it does not define require or number of nodes + // and both include and exclude are empty objects (ES will fail to parse if we don't) + delete esPhase.actions.allocate; + } + + if (phase.freezeEnabled) { + esPhase.actions.freeze = {}; + } else { + delete esPhase.actions.freeze; + } + + if (isNumber(phase.phaseIndexPriority)) { + esPhase.actions.set_priority = { + priority: parseInt(phase.phaseIndexPriority, 10), + }; + } else { + delete esPhase.actions.set_priority; + } + + return esPhase; +}; + +export const validateFrozenPhase = (phase: FrozenPhase): PhaseValidationErrors => { + if (!phase.phaseEnabled) { + return {}; + } + + const phaseErrors = {} as PhaseValidationErrors; + + // index priority is optional, but if it's set, it needs to be a positive number + if (phase.phaseIndexPriority) { + if (!isNumber(phase.phaseIndexPriority)) { + phaseErrors.phaseIndexPriority = [numberRequiredMessage]; + } else if (parseInt(phase.phaseIndexPriority, 10) < 0) { + phaseErrors.phaseIndexPriority = [positiveNumberRequiredMessage]; + } + } + + // min age needs to be a positive number + if (!isNumber(phase.selectedMinimumAge)) { + phaseErrors.selectedMinimumAge = [numberRequiredMessage]; + } else if (parseInt(phase.selectedMinimumAge, 10) < 0) { + phaseErrors.selectedMinimumAge = [positiveNumberRequiredMessage]; + } + + return { ...phaseErrors }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts index 3953521df18179..807a6fe8ec3959 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts @@ -9,6 +9,7 @@ import { defaultNewDeletePhase, defaultNewHotPhase, defaultNewWarmPhase, + defaultNewFrozenPhase, serializedPhaseInitialization, } from '../../constants'; @@ -17,6 +18,7 @@ import { Policy, PolicyFromES, SerializedPolicy } from './types'; import { hotPhaseFromES, hotPhaseToES } from './hot_phase'; import { warmPhaseFromES, warmPhaseToES } from './warm_phase'; import { coldPhaseFromES, coldPhaseToES } from './cold_phase'; +import { frozenPhaseFromES, frozenPhaseToES } from './frozen_phase'; import { deletePhaseFromES, deletePhaseToES } from './delete_phase'; export const splitSizeAndUnits = (field: string): { size: string; units: string } => { @@ -53,6 +55,7 @@ export const initializeNewPolicy = (newPolicyName: string = ''): Policy => { hot: { ...defaultNewHotPhase }, warm: { ...defaultNewWarmPhase }, cold: { ...defaultNewColdPhase }, + frozen: { ...defaultNewFrozenPhase }, delete: { ...defaultNewDeletePhase }, }, }; @@ -70,6 +73,7 @@ export const deserializePolicy = (policy: PolicyFromES): Policy => { hot: hotPhaseFromES(phases.hot), warm: warmPhaseFromES(phases.warm), cold: coldPhaseFromES(phases.cold), + frozen: frozenPhaseFromES(phases.frozen), delete: deletePhaseFromES(phases.delete), }, }; @@ -94,6 +98,13 @@ export const serializePolicy = ( serializedPolicy.phases.cold = coldPhaseToES(policy.phases.cold, originalEsPolicy.phases.cold); } + if (policy.phases.frozen.phaseEnabled) { + serializedPolicy.phases.frozen = frozenPhaseToES( + policy.phases.frozen, + originalEsPolicy.phases.frozen + ); + } + if (policy.phases.delete.phaseEnabled) { serializedPolicy.phases.delete = deletePhaseToES( policy.phases.delete, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts index 545488be2cd5e0..6fdbc4babd3f3f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts @@ -9,7 +9,17 @@ import { validateHotPhase } from './hot_phase'; import { validateWarmPhase } from './warm_phase'; import { validateColdPhase } from './cold_phase'; import { validateDeletePhase } from './delete_phase'; -import { ColdPhase, DeletePhase, HotPhase, Phase, Policy, PolicyFromES, WarmPhase } from './types'; +import { validateFrozenPhase } from './frozen_phase'; + +import { + ColdPhase, + DeletePhase, + FrozenPhase, + HotPhase, + Policy, + PolicyFromES, + WarmPhase, +} from './types'; export const propertyof = (propertyName: keyof T & string) => propertyName; @@ -100,7 +110,7 @@ export const policyNameAlreadyUsedErrorMessage = i18n.translate( defaultMessage: 'That policy name is already used.', } ); -export type PhaseValidationErrors = { +export type PhaseValidationErrors = { [P in keyof Partial]: string[]; }; @@ -108,6 +118,7 @@ export interface ValidationErrors { hot: PhaseValidationErrors; warm: PhaseValidationErrors; cold: PhaseValidationErrors; + frozen: PhaseValidationErrors; delete: PhaseValidationErrors; policyName: string[]; } @@ -148,12 +159,14 @@ export const validatePolicy = ( const hotPhaseErrors = validateHotPhase(policy.phases.hot); const warmPhaseErrors = validateWarmPhase(policy.phases.warm); const coldPhaseErrors = validateColdPhase(policy.phases.cold); + const frozenPhaseErrors = validateFrozenPhase(policy.phases.frozen); const deletePhaseErrors = validateDeletePhase(policy.phases.delete); const isValid = policyNameErrors.length === 0 && Object.keys(hotPhaseErrors).length === 0 && Object.keys(warmPhaseErrors).length === 0 && Object.keys(coldPhaseErrors).length === 0 && + Object.keys(frozenPhaseErrors).length === 0 && Object.keys(deletePhaseErrors).length === 0; return [ isValid, @@ -162,6 +175,7 @@ export const validatePolicy = ( hot: hotPhaseErrors, warm: warmPhaseErrors, cold: coldPhaseErrors, + frozen: frozenPhaseErrors, delete: deletePhaseErrors, }, ]; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts index 2e2ed5b38bb871..3d4c73cf4a82cf 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts @@ -13,6 +13,7 @@ export interface Phases { hot?: SerializedHotPhase; warm?: SerializedWarmPhase; cold?: SerializedColdPhase; + frozen?: SerializedFrozenPhase; delete?: SerializedDeletePhase; } @@ -68,6 +69,16 @@ export interface SerializedColdPhase extends SerializedPhase { }; } +export interface SerializedFrozenPhase extends SerializedPhase { + actions: { + freeze?: {}; + allocate?: AllocateAction; + set_priority?: { + priority: number | null; + }; + }; +} + export interface SerializedDeletePhase extends SerializedPhase { actions: { wait_for_snapshot?: { @@ -94,47 +105,66 @@ export interface Policy { hot: HotPhase; warm: WarmPhase; cold: ColdPhase; + frozen: FrozenPhase; delete: DeletePhase; }; } -export interface Phase { +export interface CommonPhaseSettings { phaseEnabled: boolean; } -export interface HotPhase extends Phase { + +export interface PhaseWithMinAge { + selectedMinimumAge: string; + selectedMinimumAgeUnits: string; +} + +export interface PhaseWithAllocationAction { + selectedNodeAttrs: string; + selectedReplicaCount: string; +} + +export interface PhaseWithIndexPriority { + phaseIndexPriority: string; +} + +export interface HotPhase extends CommonPhaseSettings, PhaseWithIndexPriority { rolloverEnabled: boolean; selectedMaxSizeStored: string; selectedMaxSizeStoredUnits: string; selectedMaxDocuments: string; selectedMaxAge: string; selectedMaxAgeUnits: string; - phaseIndexPriority: string; } -export interface WarmPhase extends Phase { +export interface WarmPhase + extends CommonPhaseSettings, + PhaseWithMinAge, + PhaseWithAllocationAction, + PhaseWithIndexPriority { warmPhaseOnRollover: boolean; - selectedMinimumAge: string; - selectedMinimumAgeUnits: string; - selectedNodeAttrs: string; - selectedReplicaCount: string; shrinkEnabled: boolean; selectedPrimaryShardCount: string; forceMergeEnabled: boolean; selectedForceMergeSegments: string; - phaseIndexPriority: string; } -export interface ColdPhase extends Phase { - selectedMinimumAge: string; - selectedMinimumAgeUnits: string; - selectedNodeAttrs: string; - selectedReplicaCount: string; +export interface ColdPhase + extends CommonPhaseSettings, + PhaseWithMinAge, + PhaseWithAllocationAction, + PhaseWithIndexPriority { freezeEnabled: boolean; - phaseIndexPriority: string; } -export interface DeletePhase extends Phase { - selectedMinimumAge: string; - selectedMinimumAgeUnits: string; +export interface FrozenPhase + extends CommonPhaseSettings, + PhaseWithMinAge, + PhaseWithAllocationAction, + PhaseWithIndexPriority { + freezeEnabled: boolean; +} + +export interface DeletePhase extends CommonPhaseSettings, PhaseWithMinAge { waitForSnapshotPolicy: string; } diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js index a1eac5264bb6a8..8d01f4a4c200e9 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js @@ -176,6 +176,12 @@ export const ilmFilterExtension = (indices) => { defaultMessage: 'Warm', }), }, + { + value: 'frozen', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.frozenLabel', { + defaultMessage: 'Frozen', + }), + }, { value: 'cold', view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.coldLabel', { 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 2d02802119e47b..9b51164fd4c28e 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 @@ -104,6 +104,23 @@ const coldPhaseSchema = schema.maybe( }) ); +const frozenPhaseSchema = schema.maybe( + schema.object({ + min_age: minAgeSchema, + actions: schema.object({ + set_priority: setPrioritySchema, + unfollow: unfollowSchema, + allocate: allocateSchema, + freeze: schema.maybe(schema.object({})), // Freeze has no options + searchable_snapshot: schema.maybe( + schema.object({ + snapshot_repository: schema.string(), + }) + ), + }), + }) +); + const deletePhaseSchema = schema.maybe( schema.object({ min_age: minAgeSchema, @@ -129,6 +146,7 @@ const bodySchema = schema.object({ hot: hotPhaseSchema, warm: warmPhaseSchema, cold: coldPhaseSchema, + frozen: frozenPhaseSchema, delete: deletePhaseSchema, }), }); From 647f397c50a74cf72c268a432e01311745a5b303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Mon, 31 Aug 2020 10:46:29 +0100 Subject: [PATCH 101/216] [APM] Chart units don't update when toggling the chart legends (#74931) * changing unit when legend is disabled * changing unit when legend is disabled * show individual units in the tooltip * addressing PR comment * increasing duration threshold * change formatter based on available legends * addressing PR comment Co-authored-by: Elastic Machine --- .../shared/charts/CustomPlot/index.js | 5 + .../TransactionCharts/BrowserLineChart.tsx | 14 +- .../TransactionLineChart/index.tsx | 16 +- .../charts/TransactionCharts/helper.test.ts | 69 +++++ .../charts/TransactionCharts/helper.tsx | 35 +++ .../shared/charts/TransactionCharts/index.tsx | 277 ++++++------------ .../charts/TransactionCharts/ml_header.tsx | 96 ++++++ .../TransactionCharts/use_formatter.test.tsx | 109 +++++++ .../charts/TransactionCharts/use_formatter.ts | 30 ++ .../formatters/__test__/duration.test.ts | 7 +- .../apm/public/utils/formatters/duration.ts | 4 +- 11 files changed, 457 insertions(+), 205 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.test.ts create mode 100644 x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ml_header.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js index 7e74961e57ea1c..41925d651e3617 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js @@ -79,6 +79,10 @@ export class InnerCustomPlot extends PureComponent { return i === _i ? !disabledValue : !!disabledValue; }); + if (typeof this.props.onToggleLegend === 'function') { + this.props.onToggleLegend(nextSeriesEnabledState); + } + return { seriesEnabledState: nextSeriesEnabledState, }; @@ -235,6 +239,7 @@ InnerCustomPlot.propTypes = { }) ), noHits: PropTypes.bool, + onToggleLegend: PropTypes.func, }; InnerCustomPlot.defaultProps = { diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx index 0e19c63775d311..40caf351559183 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiTitle } from '@elastic/eui'; -import { TransactionLineChart } from './TransactionLineChart'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useAvgDurationByBrowser } from '../../../../hooks/useAvgDurationByBrowser'; +import { getDurationFormatter } from '../../../../utils/formatters'; import { - getMaxY, getResponseTimeTickFormatter, getResponseTimeTooltipFormatter, -} from '.'; -import { getDurationFormatter } from '../../../../utils/formatters'; -import { useAvgDurationByBrowser } from '../../../../hooks/useAvgDurationByBrowser'; + getMaxY, +} from './helper'; +import { TransactionLineChart } from './TransactionLineChart'; export function BrowserLineChart() { const { data } = useAvgDurationByBrowser(); diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx index eaad883d2f9f63..07b7f01194d5c8 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx @@ -5,22 +5,13 @@ */ import React, { useCallback } from 'react'; -import { - Coordinate, - RectCoordinate, -} from '../../../../../../typings/timeseries'; +import { Coordinate, TimeSeries } from '../../../../../../typings/timeseries'; import { useChartsSync } from '../../../../../hooks/useChartsSync'; // @ts-ignore import CustomPlot from '../../CustomPlot'; interface Props { - series: Array<{ - color: string; - title: React.ReactNode; - titleShort?: React.ReactNode; - data: Array; - type: string; - }>; + series: TimeSeries[]; truncateLegends?: boolean; tickFormatY: (y: number) => React.ReactNode; formatTooltipValue: (c: Coordinate) => React.ReactNode; @@ -28,6 +19,7 @@ interface Props { height?: number; stacked?: boolean; onHover?: () => void; + onToggleLegend?: (disabledSeriesState: boolean[]) => void; } function TransactionLineChart(props: Props) { @@ -40,6 +32,7 @@ function TransactionLineChart(props: Props) { truncateLegends, stacked = false, onHover, + onToggleLegend, } = props; const syncedChartsProps = useChartsSync(); @@ -66,6 +59,7 @@ function TransactionLineChart(props: Props) { height={height} truncateLegends={truncateLegends} {...(stacked ? { stackBy: 'y' } : {})} + onToggleLegend={onToggleLegend} /> ); } diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.test.ts b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.test.ts new file mode 100644 index 00000000000000..a476892fa4a3f4 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { + getResponseTimeTickFormatter, + getResponseTimeTooltipFormatter, + getMaxY, +} from './helper'; +import { + getDurationFormatter, + toMicroseconds, +} from '../../../../utils/formatters'; +import { TimeSeries } from '../../../../../typings/timeseries'; + +describe('transaction chart helper', () => { + describe('getResponseTimeTickFormatter', () => { + it('formattes time tick in minutes', () => { + const formatter = getDurationFormatter(toMicroseconds(11, 'minutes')); + const timeTickFormatter = getResponseTimeTickFormatter(formatter); + expect(timeTickFormatter(toMicroseconds(60, 'seconds'))).toEqual( + '1.0 min' + ); + }); + it('formattes time tick in seconds', () => { + const formatter = getDurationFormatter(toMicroseconds(11, 'seconds')); + const timeTickFormatter = getResponseTimeTickFormatter(formatter); + expect(timeTickFormatter(toMicroseconds(6, 'seconds'))).toEqual('6.0 s'); + }); + }); + describe('getResponseTimeTooltipFormatter', () => { + const formatter = getDurationFormatter(toMicroseconds(11, 'minutes')); + const tooltipFormatter = getResponseTimeTooltipFormatter(formatter); + it("doesn't format invalid y coordinate", () => { + expect(tooltipFormatter({ x: 1, y: undefined })).toEqual('N/A'); + expect(tooltipFormatter({ x: 1, y: null })).toEqual('N/A'); + }); + it('formattes tooltip in minutes', () => { + expect( + tooltipFormatter({ x: 1, y: toMicroseconds(60, 'seconds') }) + ).toEqual('1.0 min'); + }); + }); + describe('getMaxY', () => { + it('returns zero when empty time series', () => { + expect(getMaxY([])).toEqual(0); + }); + it('returns zero for invalid y coordinate', () => { + const timeSeries = ([ + { data: [{ x: 1 }, { x: 2 }, { x: 3, y: -1 }] }, + ] as unknown) as TimeSeries[]; + expect(getMaxY(timeSeries)).toEqual(0); + }); + it('returns the max y coordinate', () => { + const timeSeries = ([ + { + data: [ + { x: 1, y: 10 }, + { x: 2, y: 5 }, + { x: 3, y: 1 }, + ], + }, + ] as unknown) as TimeSeries[]; + expect(getMaxY(timeSeries)).toEqual(10); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.tsx new file mode 100644 index 00000000000000..f11a33f932553b --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/helper.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 { flatten } from 'lodash'; +import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; +import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; +import { TimeSeries, Coordinate } from '../../../../../typings/timeseries'; +import { TimeFormatter } from '../../../../utils/formatters'; + +export function getResponseTimeTickFormatter(formatter: TimeFormatter) { + return (t: number) => { + return formatter(t).formatted; + }; +} + +export function getResponseTimeTooltipFormatter(formatter: TimeFormatter) { + return (coordinate: Coordinate) => { + return isValidCoordinateValue(coordinate.y) + ? formatter(coordinate.y).formatted + : NOT_AVAILABLE_LABEL; + }; +} + +export function getMaxY(timeSeries: TimeSeries[]) { + const coordinates = flatten( + timeSeries.map((serie: TimeSeries) => serie.data as Coordinate[]) + ); + + const numbers: number[] = coordinates.map((c: Coordinate) => (c.y ? c.y : 0)); + + return Math.max(...numbers, 0); +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx index 1f80dbf5f4d95f..d11925dc0303d2 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx @@ -8,38 +8,34 @@ import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, - EuiIconTip, EuiPanel, - EuiText, - EuiTitle, EuiSpacer, + EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Location } from 'history'; -import React, { Component } from 'react'; -import { isEmpty, flatten } from 'lodash'; -import styled from 'styled-components'; +import React from 'react'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; -import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; -import { ITransactionChartData } from '../../../../selectors/chartSelectors'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { - tpmUnit, - TimeFormatter, - getDurationFormatter, - asDecimal, -} from '../../../../utils/formatters'; -import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink'; + TRANSACTION_PAGE_LOAD, + TRANSACTION_REQUEST, + TRANSACTION_ROUTE_CHANGE, +} from '../../../../../common/transaction_types'; +import { Coordinate } from '../../../../../typings/timeseries'; import { LicenseContext } from '../../../../context/LicenseContext'; -import { TransactionLineChart } from './TransactionLineChart'; +import { IUrlParams } from '../../../../context/UrlParamsContext/types'; +import { ITransactionChartData } from '../../../../selectors/chartSelectors'; +import { asDecimal, tpmUnit } from '../../../../utils/formatters'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; import { BrowserLineChart } from './BrowserLineChart'; import { DurationByCountryMap } from './DurationByCountryMap'; import { - TRANSACTION_PAGE_LOAD, - TRANSACTION_ROUTE_CHANGE, - TRANSACTION_REQUEST, -} from '../../../../../common/transaction_types'; + getResponseTimeTickFormatter, + getResponseTimeTooltipFormatter, +} from './helper'; +import { MLHeader } from './ml_header'; +import { TransactionLineChart } from './TransactionLineChart'; +import { useFormatter } from './use_formatter'; interface TransactionChartProps { charts: ITransactionChartData; @@ -47,181 +43,96 @@ interface TransactionChartProps { urlParams: IUrlParams; } -const ShiftedIconWrapper = styled.span` - padding-right: 5px; - position: relative; - top: -1px; - display: inline-block; -`; - -const ShiftedEuiText = styled(EuiText)` - position: relative; - top: 5px; -`; - -export function getResponseTimeTickFormatter(formatter: TimeFormatter) { - return (t: number) => formatter(t).formatted; -} - -export function getResponseTimeTooltipFormatter(formatter: TimeFormatter) { - return (p: Coordinate) => { - return isValidCoordinateValue(p.y) - ? formatter(p.y).formatted - : NOT_AVAILABLE_LABEL; - }; -} - -export function getMaxY(responseTimeSeries: TimeSeries[]) { - const coordinates = flatten( - responseTimeSeries.map((serie: TimeSeries) => serie.data as Coordinate[]) - ); - - const numbers: number[] = coordinates.map((c: Coordinate) => (c.y ? c.y : 0)); - - return Math.max(...numbers, 0); -} - -export class TransactionCharts extends Component { - public getTPMFormatter = (t: number) => { - const { urlParams } = this.props; +export function TransactionCharts({ + charts, + location, + urlParams, +}: TransactionChartProps) { + const getTPMFormatter = (t: number) => { const unit = tpmUnit(urlParams.transactionType); return `${asDecimal(t)} ${unit}`; }; - public getTPMTooltipFormatter = (p: Coordinate) => { + const getTPMTooltipFormatter = (p: Coordinate) => { return isValidCoordinateValue(p.y) - ? this.getTPMFormatter(p.y) + ? getTPMFormatter(p.y) : NOT_AVAILABLE_LABEL; }; - public renderMLHeader(hasValidMlLicense: boolean | undefined) { - const { mlJobId } = this.props.charts; - - if (!hasValidMlLicense || !mlJobId) { - return null; - } - - const { serviceName, kuery, transactionType } = this.props.urlParams; - if (!serviceName) { - return null; - } + const { transactionType } = urlParams; - const hasKuery = !isEmpty(kuery); - const icon = hasKuery ? ( - - ) : ( - - ); - - return ( - - - {icon} - - {i18n.translate( - 'xpack.apm.metrics.transactionChart.machineLearningLabel', - { - defaultMessage: 'Machine learning:', - } - )}{' '} - - - View Job - - - - ); - } + const { responseTimeSeries, tpmSeries } = charts; - public render() { - const { charts, urlParams } = this.props; - const { responseTimeSeries, tpmSeries } = charts; - const { transactionType } = urlParams; - const maxY = getMaxY(responseTimeSeries); - const formatter = getDurationFormatter(maxY); + const { formatter, setDisabledSeriesState } = useFormatter( + responseTimeSeries + ); - return ( - <> - - - - - - - - {responseTimeLabel(transactionType)} - - - - {(license) => - this.renderMLHeader(license?.getFeature('ml').isAvailable) - } - - - + + + + + + + + {responseTimeLabel(transactionType)} + + + + {(license) => ( + )} - /> - - - - - - - - - {tpmLabel(transactionType)} - - - - - - - {transactionType === TRANSACTION_PAGE_LOAD && ( - <> - - - - - - - - - - - - - - - )} - - ); - } + + + + + + + + + + + + {tpmLabel(transactionType)} + + + + + + + {transactionType === TRANSACTION_PAGE_LOAD && ( + <> + + + + + + + + + + + + + + + )} + + ); } function tpmLabel(type?: string) { diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ml_header.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ml_header.tsx new file mode 100644 index 00000000000000..f829b5841efa93 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ml_header.tsx @@ -0,0 +1,96 @@ +/* + * 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 { EuiIconTip } from '@elastic/eui'; +import { isEmpty } from 'lodash'; +import React from 'react'; +import { EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { EuiText } from '@elastic/eui'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink'; + +interface Props { + hasValidMlLicense?: boolean; + mlJobId?: string; +} + +const ShiftedIconWrapper = styled.span` + padding-right: 5px; + position: relative; + top: -1px; + display: inline-block; +`; + +const ShiftedEuiText = styled(EuiText)` + position: relative; + top: 5px; +`; + +export function MLHeader({ hasValidMlLicense, mlJobId }: Props) { + const { urlParams } = useUrlParams(); + + if (!hasValidMlLicense || !mlJobId) { + return null; + } + + const { serviceName, kuery, transactionType } = urlParams; + if (!serviceName) { + return null; + } + + const hasKuery = !isEmpty(kuery); + const icon = hasKuery ? ( +
' + entry.label + '
' + fragments.join("") + '
'; - if (options.legend.container != null) - $(options.legend.container).html(table); - else { - var pos = "", - p = options.legend.position, - m = options.legend.margin; - if (m[0] == null) - m = [m, m]; - if (p.charAt(0) == "n") - pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; - else if (p.charAt(0) == "s") - pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; - if (p.charAt(1) == "e") - pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; - else if (p.charAt(1) == "w") - pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; - var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(placeholder); - if (options.legend.backgroundOpacity != 0.0) { - // put in the transparent background - // separately to avoid blended labels and - // label boxes - var c = options.legend.backgroundColor; - if (c == null) { - c = options.grid.backgroundColor; - if (c && typeof c == "string") - c = $.color.parse(c); - else - c = $.color.extract(legend, 'background-color'); - c.a = 1; - c = c.toString(); - } - var div = legend.children(); - $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); - } - } - } - - - // interactive features - - var highlights = [], - redrawTimeout = null; - - // returns the data item the mouse is over, or null if none is found - function findNearbyItem(mouseX, mouseY, seriesFilter) { - var maxDistance = options.grid.mouseActiveRadius, - smallestDistance = maxDistance * maxDistance + 1, - item = null, foundPoint = false, i, j, ps; - - for (i = series.length - 1; i >= 0; --i) { - if (!seriesFilter(series[i])) - continue; - - var s = series[i], - axisx = s.xaxis, - axisy = s.yaxis, - points = s.datapoints.points, - mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster - my = axisy.c2p(mouseY), - maxx = maxDistance / axisx.scale, - maxy = maxDistance / axisy.scale; - - ps = s.datapoints.pointsize; - // with inverse transforms, we can't use the maxx/maxy - // optimization, sadly - if (axisx.options.inverseTransform) - maxx = Number.MAX_VALUE; - if (axisy.options.inverseTransform) - maxy = Number.MAX_VALUE; - - if (s.lines.show || s.points.show) { - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1]; - if (x == null) - continue; - - // For points and lines, the cursor must be within a - // certain distance to the data point - if (x - mx > maxx || x - mx < -maxx || - y - my > maxy || y - my < -maxy) - continue; - - // We have to calculate distances in pixels, not in - // data units, because the scales of the axes may be different - var dx = Math.abs(axisx.p2c(x) - mouseX), - dy = Math.abs(axisy.p2c(y) - mouseY), - dist = dx * dx + dy * dy; // we save the sqrt - - // use <= to ensure last point takes precedence - // (last generally means on top of) - if (dist < smallestDistance) { - smallestDistance = dist; - item = [i, j / ps]; - } - } - } - - if (s.bars.show && !item) { // no other point can be nearby - - var barLeft, barRight; - - switch (s.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -s.bars.barWidth; - break; - default: - barLeft = -s.bars.barWidth / 2; - } - - barRight = barLeft + s.bars.barWidth; - - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1], b = points[j + 2]; - if (x == null) - continue; - - // for a bar graph, the cursor must be inside the bar - if (series[i].bars.horizontal ? - (mx <= Math.max(b, x) && mx >= Math.min(b, x) && - my >= y + barLeft && my <= y + barRight) : - (mx >= x + barLeft && mx <= x + barRight && - my >= Math.min(b, y) && my <= Math.max(b, y))) - item = [i, j / ps]; - } - } - } - - if (item) { - i = item[0]; - j = item[1]; - ps = series[i].datapoints.pointsize; - - return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), - dataIndex: j, - series: series[i], - seriesIndex: i }; - } - - return null; - } - - function onMouseMove(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return s["hoverable"] != false; }); - } - - function onMouseLeave(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return false; }); - } - - function onClick(e) { - triggerClickHoverEvent("plotclick", e, - function (s) { return s["clickable"] != false; }); - } - - // trigger click or hover event (they send the same parameters - // so we share their code) - function triggerClickHoverEvent(eventname, event, seriesFilter) { - var offset = eventHolder.offset(), - canvasX = event.pageX - offset.left - plotOffset.left, - canvasY = event.pageY - offset.top - plotOffset.top, - pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); - - pos.pageX = event.pageX; - pos.pageY = event.pageY; - - var item = findNearbyItem(canvasX, canvasY, seriesFilter); - - if (item) { - // fill in mouse pos for any listeners out there - item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10); - item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10); - } - - if (options.grid.autoHighlight) { - // clear auto-highlights - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.auto == eventname && - !(item && h.series == item.series && - h.point[0] == item.datapoint[0] && - h.point[1] == item.datapoint[1])) - unhighlight(h.series, h.point); - } - - if (item) - highlight(item.series, item.datapoint, eventname); - } - - placeholder.trigger(eventname, [ pos, item ]); - } - - function triggerRedrawOverlay() { - var t = options.interaction.redrawOverlayInterval; - if (t == -1) { // skip event queue - drawOverlay(); - return; - } - - if (!redrawTimeout) - redrawTimeout = setTimeout(drawOverlay, t); - } - - function drawOverlay() { - redrawTimeout = null; - - // draw highlights - octx.save(); - overlay.clear(); - octx.translate(plotOffset.left, plotOffset.top); - - var i, hi; - for (i = 0; i < highlights.length; ++i) { - hi = highlights[i]; - - if (hi.series.bars.show) - drawBarHighlight(hi.series, hi.point); - else - drawPointHighlight(hi.series, hi.point); - } - octx.restore(); - - executeHooks(hooks.drawOverlay, [octx]); - } - - function highlight(s, point, auto) { - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") { - var ps = s.datapoints.pointsize; - point = s.datapoints.points.slice(ps * point, ps * (point + 1)); - } - - var i = indexOfHighlight(s, point); - if (i == -1) { - highlights.push({ series: s, point: point, auto: auto }); - - triggerRedrawOverlay(); - } - else if (!auto) - highlights[i].auto = false; - } - - function unhighlight(s, point) { - if (s == null && point == null) { - highlights = []; - triggerRedrawOverlay(); - return; - } - - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") { - var ps = s.datapoints.pointsize; - point = s.datapoints.points.slice(ps * point, ps * (point + 1)); - } - - var i = indexOfHighlight(s, point); - if (i != -1) { - highlights.splice(i, 1); - - triggerRedrawOverlay(); - } - } - - function indexOfHighlight(s, p) { - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.series == s && h.point[0] == p[0] - && h.point[1] == p[1]) - return i; - } - return -1; - } - - function drawPointHighlight(series, point) { - var x = point[0], y = point[1], - axisx = series.xaxis, axisy = series.yaxis, - highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); - - if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - return; - - var pointRadius = series.points.radius + series.points.lineWidth / 2; - octx.lineWidth = pointRadius; - octx.strokeStyle = highlightColor; - var radius = 1.5 * pointRadius; - x = axisx.p2c(x); - y = axisy.p2c(y); - - octx.beginPath(); - if (series.points.symbol == "circle") - octx.arc(x, y, radius, 0, 2 * Math.PI, false); - else - series.points.symbol(octx, x, y, radius, false); - octx.closePath(); - octx.stroke(); - } - - function drawBarHighlight(series, point) { - var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(), - fillStyle = highlightColor, - barLeft; - - switch (series.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -series.bars.barWidth; - break; - default: - barLeft = -series.bars.barWidth / 2; - } - - octx.lineWidth = series.bars.lineWidth; - octx.strokeStyle = highlightColor; - - drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, - function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); - } - - function getColorOrGradient(spec, bottom, top, defaultColor) { - if (typeof spec == "string") - return spec; - else { - // assume this is a gradient spec; IE currently only - // supports a simple vertical gradient properly, so that's - // what we support too - var gradient = ctx.createLinearGradient(0, top, 0, bottom); - - for (var i = 0, l = spec.colors.length; i < l; ++i) { - var c = spec.colors[i]; - if (typeof c != "string") { - var co = $.color.parse(defaultColor); - if (c.brightness != null) - co = co.scale('rgb', c.brightness); - if (c.opacity != null) - co.a *= c.opacity; - c = co.toString(); - } - gradient.addColorStop(i / (l - 1), c); - } - - return gradient; - } - } - } - - // Add the plot function to the top level of the jQuery object - - $.plot = function(placeholder, data, options) { - //var t0 = new Date(); - var plot = new Plot($(placeholder), data, options, $.plot.plugins); - //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); - return plot; - }; - - $.plot.version = "0.8.3"; - - $.plot.plugins = []; - - // Also add the plot function as a chainable property - - $.fn.plot = function(data, options) { - return this.each(function() { - $.plot(this, data, options); - }); - }; - - // round to nearby lower multiple of base - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.log.js b/src/legacy/ui/public/flot-charts/jquery.flot.log.js deleted file mode 100644 index e32bf5cf7e8173..00000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.log.js +++ /dev/null @@ -1,163 +0,0 @@ -/* @notice - * - * Pretty handling of logarithmic axes. - * Copyright (c) 2007-2014 IOLA and Ole Laursen. - * Licensed under the MIT license. - * Created by Arne de Laat - * Set axis.mode to "log" and make the axis logarithmic using transform: - * axis: { - * mode: 'log', - * transform: function(v) {v <= 0 ? Math.log(v) / Math.LN10 : null}, - * inverseTransform: function(v) {Math.pow(10, v)} - * } - * The transform filters negative and zero values, because those are - * invalid on logarithmic scales. - * This plugin tries to create good looking logarithmic ticks, using - * unicode superscript characters. If all data to be plotted is between two - * powers of ten then the default flot tick generator and renderer are - * used. Logarithmic ticks are places at powers of ten and at half those - * values if there are not to many ticks already (e.g. [1, 5, 10, 50, 100]). - * For details, see https://github.com/flot/flot/pull/1328 -*/ - -(function($) { - - function log10(value) { - /* Get the Log10 of the value - */ - return Math.log(value) / Math.LN10; - } - - function floorAsLog10(value) { - /* Get power of the first power of 10 below the value - */ - return Math.floor(log10(value)); - } - - function ceilAsLog10(value) { - /* Get power of the first power of 10 above the value - */ - return Math.ceil(log10(value)); - } - - - // round to nearby lower multiple of base - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - - function getUnicodePower(power) { - var superscripts = ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"], - result = "", - str_power = "" + power; - for (var i = 0; i < str_power.length; i++) { - if (str_power[i] === "+") { - } - else if (str_power[i] === "-") { - result += "⁻"; - } - else { - result += superscripts[str_power[i]]; - } - } - return result; - } - - function init(plot) { - plot.hooks.processOptions.push(function (plot) { - $.each(plot.getAxes(), function(axisName, axis) { - - var opts = axis.options; - - if (opts.mode === "log") { - - axis.tickGenerator = function (axis) { - - var ticks = [], - end = ceilAsLog10(axis.max), - start = floorAsLog10(axis.min), - tick = Number.NaN, - i = 0; - - if (axis.min === null || axis.min <= 0) { - // Bad minimum, make ticks from 1 (10**0) to max - start = 0; - axis.min = 0.6; - } - - if (end <= start) { - // Start less than end?! - ticks = [1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, - 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, - 1e7, 1e8, 1e9]; - } - else if (log10(axis.max) - log10(axis.datamin) < 1) { - // Default flot generator incase no powers of 10 - // are between start and end - var prev; - start = floorInBase(axis.min, axis.tickSize); - do { - prev = tick; - tick = start + i * axis.tickSize; - ticks.push(tick); - ++i; - } while (tick < axis.max && tick !== prev); - } - else { - // Make ticks at each power of ten - for (; i <= (end - start); i++) { - tick = Math.pow(10, start + i); - ticks.push(tick); - } - - var length = ticks.length; - - // If not to many ticks also put a tick between - // the powers of ten - if (end - start < 6) { - for (var j = 1; j < length * 2 - 1; j += 2) { - tick = ticks[j - 1] * 5; - ticks.splice(j, 0, tick); - } - } - } - return ticks; - }; - - axis.tickFormatter = function (value, axis) { - var formatted; - if (log10(axis.max) - log10(axis.datamin) < 1) { - // Default flot formatter - var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; - formatted = "" + Math.round(value * factor) / factor; - if (axis.tickDecimals !== null) { - var decimal = formatted.indexOf("."); - var precision = decimal === -1 ? 0 : formatted.length - decimal - 1; - if (precision < axis.tickDecimals) { - return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); - } - } - } - else { - var multiplier = "", - exponential = parseFloat(value).toExponential(0), - power = getUnicodePower(exponential.slice(2)); - if (exponential[0] !== "1") { - multiplier = exponential[0] + "x"; - } - formatted = multiplier + "10" + power; - } - return formatted; - }; - } - }); - }); - } - - $.plot.plugins.push({ - init: init, - name: "log", - version: "0.9" - }); - -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.navigate.js b/src/legacy/ui/public/flot-charts/jquery.flot.navigate.js deleted file mode 100644 index 13fb7f17d04b24..00000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.navigate.js +++ /dev/null @@ -1,346 +0,0 @@ -/* Flot plugin for adding the ability to pan and zoom the plot. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The default behaviour is double click and scrollwheel up/down to zoom in, drag -to pan. The plugin defines plot.zoom({ center }), plot.zoomOut() and -plot.pan( offset ) so you easily can add custom controls. It also fires -"plotpan" and "plotzoom" events, useful for synchronizing plots. - -The plugin supports these options: - - zoom: { - interactive: false - trigger: "dblclick" // or "click" for single click - amount: 1.5 // 2 = 200% (zoom in), 0.5 = 50% (zoom out) - } - - pan: { - interactive: false - cursor: "move" // CSS mouse cursor value used when dragging, e.g. "pointer" - frameRate: 20 - } - - xaxis, yaxis, x2axis, y2axis: { - zoomRange: null // or [ number, number ] (min range, max range) or false - panRange: null // or [ number, number ] (min, max) or false - } - -"interactive" enables the built-in drag/click behaviour. If you enable -interactive for pan, then you'll have a basic plot that supports moving -around; the same for zoom. - -"amount" specifies the default amount to zoom in (so 1.5 = 150%) relative to -the current viewport. - -"cursor" is a standard CSS mouse cursor string used for visual feedback to the -user when dragging. - -"frameRate" specifies the maximum number of times per second the plot will -update itself while the user is panning around on it (set to null to disable -intermediate pans, the plot will then not update until the mouse button is -released). - -"zoomRange" is the interval in which zooming can happen, e.g. with zoomRange: -[1, 100] the zoom will never scale the axis so that the difference between min -and max is smaller than 1 or larger than 100. You can set either end to null -to ignore, e.g. [1, null]. If you set zoomRange to false, zooming on that axis -will be disabled. - -"panRange" confines the panning to stay within a range, e.g. with panRange: -[-10, 20] panning stops at -10 in one end and at 20 in the other. Either can -be null, e.g. [-10, null]. If you set panRange to false, panning on that axis -will be disabled. - -Example API usage: - - plot = $.plot(...); - - // zoom default amount in on the pixel ( 10, 20 ) - plot.zoom({ center: { left: 10, top: 20 } }); - - // zoom out again - plot.zoomOut({ center: { left: 10, top: 20 } }); - - // zoom 200% in on the pixel (10, 20) - plot.zoom({ amount: 2, center: { left: 10, top: 20 } }); - - // pan 100 pixels to the left and 20 down - plot.pan({ left: -100, top: 20 }) - -Here, "center" specifies where the center of the zooming should happen. Note -that this is defined in pixel space, not the space of the data points (you can -use the p2c helpers on the axes in Flot to help you convert between these). - -"amount" is the amount to zoom the viewport relative to the current range, so -1 is 100% (i.e. no change), 1.5 is 150% (zoom in), 0.7 is 70% (zoom out). You -can set the default in the options. - -*/ - -// First two dependencies, jquery.event.drag.js and -// jquery.mousewheel.js, we put them inline here to save people the -// effort of downloading them. - -/* -jquery.event.drag.js ~ v1.5 ~ Copyright (c) 2008, Three Dub Media (http://threedubmedia.com) -Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-LICENSE.txt -*/ -(function(a){function e(h){var k,j=this,l=h.data||{};if(l.elem)j=h.dragTarget=l.elem,h.dragProxy=d.proxy||j,h.cursorOffsetX=l.pageX-l.left,h.cursorOffsetY=l.pageY-l.top,h.offsetX=h.pageX-h.cursorOffsetX,h.offsetY=h.pageY-h.cursorOffsetY;else if(d.dragging||l.which>0&&h.which!=l.which||a(h.target).is(l.not))return;switch(h.type){case"mousedown":return a.extend(l,a(j).offset(),{elem:j,target:h.target,pageX:h.pageX,pageY:h.pageY}),b.add(document,"mousemove mouseup",e,l),i(j,!1),d.dragging=null,!1;case!d.dragging&&"mousemove":if(g(h.pageX-l.pageX)+g(h.pageY-l.pageY) max) { - // make sure min < max - var tmp = min; - min = max; - max = tmp; - } - - //Check that we are in panRange - if (pr) { - if (pr[0] != null && min < pr[0]) { - min = pr[0]; - } - if (pr[1] != null && max > pr[1]) { - max = pr[1]; - } - } - - var range = max - min; - if (zr && - ((zr[0] != null && range < zr[0] && amount >1) || - (zr[1] != null && range > zr[1] && amount <1))) - return; - - opts.min = min; - opts.max = max; - }); - - plot.setupGrid(); - plot.draw(); - - if (!args.preventEvent) - plot.getPlaceholder().trigger("plotzoom", [ plot, args ]); - }; - - plot.pan = function (args) { - var delta = { - x: +args.left, - y: +args.top - }; - - if (isNaN(delta.x)) - delta.x = 0; - if (isNaN(delta.y)) - delta.y = 0; - - $.each(plot.getAxes(), function (_, axis) { - var opts = axis.options, - min, max, d = delta[axis.direction]; - - min = axis.c2p(axis.p2c(axis.min) + d), - max = axis.c2p(axis.p2c(axis.max) + d); - - var pr = opts.panRange; - if (pr === false) // no panning on this axis - return; - - if (pr) { - // check whether we hit the wall - if (pr[0] != null && pr[0] > min) { - d = pr[0] - min; - min += d; - max += d; - } - - if (pr[1] != null && pr[1] < max) { - d = pr[1] - max; - min += d; - max += d; - } - } - - opts.min = min; - opts.max = max; - }); - - plot.setupGrid(); - plot.draw(); - - if (!args.preventEvent) - plot.getPlaceholder().trigger("plotpan", [ plot, args ]); - }; - - function shutdown(plot, eventHolder) { - eventHolder.unbind(plot.getOptions().zoom.trigger, onZoomClick); - eventHolder.unbind("mousewheel", onMouseWheel); - eventHolder.unbind("dragstart", onDragStart); - eventHolder.unbind("drag", onDrag); - eventHolder.unbind("dragend", onDragEnd); - if (panTimeout) - clearTimeout(panTimeout); - } - - plot.hooks.bindEvents.push(bindEvents); - plot.hooks.shutdown.push(shutdown); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'navigate', - version: '1.3' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.pie.js b/src/legacy/ui/public/flot-charts/jquery.flot.pie.js deleted file mode 100644 index 06f900bdc950fa..00000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.pie.js +++ /dev/null @@ -1,824 +0,0 @@ -/* Flot plugin for rendering pie charts. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin assumes that each series has a single data value, and that each -value is a positive integer or zero. Negative numbers don't make sense for a -pie chart, and have unpredictable results. The values do NOT need to be -passed in as percentages; the plugin will calculate the total and per-slice -percentages internally. - -* Created by Brian Medendorp - -* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars - -The plugin supports these options: - - series: { - pie: { - show: true/false - radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto' - innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect - startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result - tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show) - offset: { - top: integer value to move the pie up or down - left: integer value to move the pie left or right, or 'auto' - }, - stroke: { - color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#FFF') - width: integer pixel width of the stroke - }, - label: { - show: true/false, or 'auto' - formatter: a user-defined function that modifies the text/style of the label text - radius: 0-1 for percentage of fullsize, or a specified pixel length - background: { - color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#000') - opacity: 0-1 - }, - threshold: 0-1 for the percentage value at which to hide labels (if they're too small) - }, - combine: { - threshold: 0-1 for the percentage value at which to combine slices (if they're too small) - color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined - label: any text value of what the combined slice should be labeled - } - highlight: { - opacity: 0-1 - } - } - } - -More detail and specific examples can be found in the included HTML file. - -*/ - -import { i18n } from '@kbn/i18n'; - -(function($) { - // Maximum redraw attempts when fitting labels within the plot - - var REDRAW_ATTEMPTS = 10; - - // Factor by which to shrink the pie when fitting labels within the plot - - var REDRAW_SHRINK = 0.95; - - function init(plot) { - - var canvas = null, - target = null, - options = null, - maxRadius = null, - centerLeft = null, - centerTop = null, - processed = false, - ctx = null; - - // interactive variables - - var highlights = []; - - // add hook to determine if pie plugin in enabled, and then perform necessary operations - - plot.hooks.processOptions.push(function(plot, options) { - if (options.series.pie.show) { - - options.grid.show = false; - - // set labels.show - - if (options.series.pie.label.show == "auto") { - if (options.legend.show) { - options.series.pie.label.show = false; - } else { - options.series.pie.label.show = true; - } - } - - // set radius - - if (options.series.pie.radius == "auto") { - if (options.series.pie.label.show) { - options.series.pie.radius = 3/4; - } else { - options.series.pie.radius = 1; - } - } - - // ensure sane tilt - - if (options.series.pie.tilt > 1) { - options.series.pie.tilt = 1; - } else if (options.series.pie.tilt < 0) { - options.series.pie.tilt = 0; - } - } - }); - - plot.hooks.bindEvents.push(function(plot, eventHolder) { - var options = plot.getOptions(); - if (options.series.pie.show) { - if (options.grid.hoverable) { - eventHolder.unbind("mousemove").mousemove(onMouseMove); - } - if (options.grid.clickable) { - eventHolder.unbind("click").click(onClick); - } - } - }); - - plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) { - var options = plot.getOptions(); - if (options.series.pie.show) { - processDatapoints(plot, series, data, datapoints); - } - }); - - plot.hooks.drawOverlay.push(function(plot, octx) { - var options = plot.getOptions(); - if (options.series.pie.show) { - drawOverlay(plot, octx); - } - }); - - plot.hooks.draw.push(function(plot, newCtx) { - var options = plot.getOptions(); - if (options.series.pie.show) { - draw(plot, newCtx); - } - }); - - function processDatapoints(plot, series, datapoints) { - if (!processed) { - processed = true; - canvas = plot.getCanvas(); - target = $(canvas).parent(); - options = plot.getOptions(); - plot.setData(combine(plot.getData())); - } - } - - function combine(data) { - - var total = 0, - combined = 0, - numCombined = 0, - color = options.series.pie.combine.color, - newdata = []; - - // Fix up the raw data from Flot, ensuring the data is numeric - - for (var i = 0; i < data.length; ++i) { - - var value = data[i].data; - - // If the data is an array, we'll assume that it's a standard - // Flot x-y pair, and are concerned only with the second value. - - // Note how we use the original array, rather than creating a - // new one; this is more efficient and preserves any extra data - // that the user may have stored in higher indexes. - - if ($.isArray(value) && value.length == 1) { - value = value[0]; - } - - if ($.isArray(value)) { - // Equivalent to $.isNumeric() but compatible with jQuery < 1.7 - if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) { - value[1] = +value[1]; - } else { - value[1] = 0; - } - } else if (!isNaN(parseFloat(value)) && isFinite(value)) { - value = [1, +value]; - } else { - value = [1, 0]; - } - - data[i].data = [value]; - } - - // Sum up all the slices, so we can calculate percentages for each - - for (var i = 0; i < data.length; ++i) { - total += data[i].data[0][1]; - } - - // Count the number of slices with percentages below the combine - // threshold; if it turns out to be just one, we won't combine. - - for (var i = 0; i < data.length; ++i) { - var value = data[i].data[0][1]; - if (value / total <= options.series.pie.combine.threshold) { - combined += value; - numCombined++; - if (!color) { - color = data[i].color; - } - } - } - - for (var i = 0; i < data.length; ++i) { - var value = data[i].data[0][1]; - if (numCombined < 2 || value / total > options.series.pie.combine.threshold) { - newdata.push( - $.extend(data[i], { /* extend to allow keeping all other original data values - and using them e.g. in labelFormatter. */ - data: [[1, value]], - color: data[i].color, - label: data[i].label, - angle: value * Math.PI * 2 / total, - percent: value / (total / 100) - }) - ); - } - } - - if (numCombined > 1) { - newdata.push({ - data: [[1, combined]], - color: color, - label: options.series.pie.combine.label, - angle: combined * Math.PI * 2 / total, - percent: combined / (total / 100) - }); - } - - return newdata; - } - - function draw(plot, newCtx) { - - if (!target) { - return; // if no series were passed - } - - var canvasWidth = plot.getPlaceholder().width(), - canvasHeight = plot.getPlaceholder().height(), - legendWidth = target.children().filter(".legend").children().width() || 0; - - ctx = newCtx; - - // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE! - - // When combining smaller slices into an 'other' slice, we need to - // add a new series. Since Flot gives plugins no way to modify the - // list of series, the pie plugin uses a hack where the first call - // to processDatapoints results in a call to setData with the new - // list of series, then subsequent processDatapoints do nothing. - - // The plugin-global 'processed' flag is used to control this hack; - // it starts out false, and is set to true after the first call to - // processDatapoints. - - // Unfortunately this turns future setData calls into no-ops; they - // call processDatapoints, the flag is true, and nothing happens. - - // To fix this we'll set the flag back to false here in draw, when - // all series have been processed, so the next sequence of calls to - // processDatapoints once again starts out with a slice-combine. - // This is really a hack; in 0.9 we need to give plugins a proper - // way to modify series before any processing begins. - - processed = false; - - // calculate maximum radius and center point - - maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2; - centerTop = canvasHeight / 2 + options.series.pie.offset.top; - centerLeft = canvasWidth / 2; - - if (options.series.pie.offset.left == "auto") { - if (options.legend.position.match("w")) { - centerLeft += legendWidth / 2; - } else { - centerLeft -= legendWidth / 2; - } - if (centerLeft < maxRadius) { - centerLeft = maxRadius; - } else if (centerLeft > canvasWidth - maxRadius) { - centerLeft = canvasWidth - maxRadius; - } - } else { - centerLeft += options.series.pie.offset.left; - } - - var slices = plot.getData(), - attempts = 0; - - // Keep shrinking the pie's radius until drawPie returns true, - // indicating that all the labels fit, or we try too many times. - - do { - if (attempts > 0) { - maxRadius *= REDRAW_SHRINK; - } - attempts += 1; - clear(); - if (options.series.pie.tilt <= 0.8) { - drawShadow(); - } - } while (!drawPie() && attempts < REDRAW_ATTEMPTS) - - if (attempts >= REDRAW_ATTEMPTS) { - clear(); - const errorMessage = i18n.translate('common.ui.flotCharts.pie.unableToDrawLabelsInsideCanvasErrorMessage', { - defaultMessage: 'Could not draw pie with labels contained inside canvas', - }); - target.prepend(`
${errorMessage}
`); - } - - if (plot.setSeries && plot.insertLegend) { - plot.setSeries(slices); - plot.insertLegend(); - } - - // we're actually done at this point, just defining internal functions at this point - - function clear() { - ctx.clearRect(0, 0, canvasWidth, canvasHeight); - target.children().filter(".pieLabel, .pieLabelBackground").remove(); - } - - function drawShadow() { - - var shadowLeft = options.series.pie.shadow.left; - var shadowTop = options.series.pie.shadow.top; - var edge = 10; - var alpha = options.series.pie.shadow.alpha; - var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) { - return; // shadow would be outside canvas, so don't draw it - } - - ctx.save(); - ctx.translate(shadowLeft,shadowTop); - ctx.globalAlpha = alpha; - ctx.fillStyle = "#000"; - - // center and rotate to starting position - - ctx.translate(centerLeft,centerTop); - ctx.scale(1, options.series.pie.tilt); - - //radius -= edge; - - for (var i = 1; i <= edge; i++) { - ctx.beginPath(); - ctx.arc(0, 0, radius, 0, Math.PI * 2, false); - ctx.fill(); - radius -= i; - } - - ctx.restore(); - } - - function drawPie() { - - var startAngle = Math.PI * options.series.pie.startAngle; - var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - // center and rotate to starting position - - ctx.save(); - ctx.translate(centerLeft,centerTop); - ctx.scale(1, options.series.pie.tilt); - //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera - - // draw slices - - ctx.save(); - var currentAngle = startAngle; - for (var i = 0; i < slices.length; ++i) { - slices[i].startAngle = currentAngle; - drawSlice(slices[i].angle, slices[i].color, true); - } - ctx.restore(); - - // draw slice outlines - - if (options.series.pie.stroke.width > 0) { - ctx.save(); - ctx.lineWidth = options.series.pie.stroke.width; - currentAngle = startAngle; - for (var i = 0; i < slices.length; ++i) { - drawSlice(slices[i].angle, options.series.pie.stroke.color, false); - } - ctx.restore(); - } - - // draw donut hole - - drawDonutHole(ctx); - - ctx.restore(); - - // Draw the labels, returning true if they fit within the plot - - if (options.series.pie.label.show) { - return drawLabels(); - } else return true; - - function drawSlice(angle, color, fill) { - - if (angle <= 0 || isNaN(angle)) { - return; - } - - if (fill) { - ctx.fillStyle = color; - } else { - ctx.strokeStyle = color; - ctx.lineJoin = "round"; - } - - ctx.beginPath(); - if (Math.abs(angle - Math.PI * 2) > 0.000000001) { - ctx.moveTo(0, 0); // Center of the pie - } - - //ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera - ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false); - ctx.arc(0, 0, radius,currentAngle + angle / 2, currentAngle + angle, false); - ctx.closePath(); - //ctx.rotate(angle); // This doesn't work properly in Opera - currentAngle += angle; - - if (fill) { - ctx.fill(); - } else { - ctx.stroke(); - } - } - - function drawLabels() { - - var currentAngle = startAngle; - var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius; - - for (var i = 0; i < slices.length; ++i) { - if (slices[i].percent >= options.series.pie.label.threshold * 100) { - if (!drawLabel(slices[i], currentAngle, i)) { - return false; - } - } - currentAngle += slices[i].angle; - } - - return true; - - function drawLabel(slice, startAngle, index) { - - if (slice.data[0][1] == 0) { - return true; - } - - // format label text - - var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter; - - if (lf) { - text = lf(slice.label, slice); - } else { - text = slice.label; - } - - if (plf) { - text = plf(text, slice); - } - - var halfAngle = ((startAngle + slice.angle) + startAngle) / 2; - var x = centerLeft + Math.round(Math.cos(halfAngle) * radius); - var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt; - - var html = "" + text + ""; - target.append(html); - - var label = target.children("#pieLabel" + index); - var labelTop = (y - label.height() / 2); - var labelLeft = (x - label.width() / 2); - - label.css("top", labelTop); - label.css("left", labelLeft); - - // check to make sure that the label is not outside the canvas - - if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) { - return false; - } - - if (options.series.pie.label.background.opacity != 0) { - - // put in the transparent background separately to avoid blended labels and label boxes - - var c = options.series.pie.label.background.color; - - if (c == null) { - c = slice.color; - } - - var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;"; - $("
") - .css("opacity", options.series.pie.label.background.opacity) - .insertBefore(label); - } - - return true; - } // end individual label function - } // end drawLabels function - } // end drawPie function - } // end draw function - - // Placed here because it needs to be accessed from multiple locations - - function drawDonutHole(layer) { - if (options.series.pie.innerRadius > 0) { - - // subtract the center - - layer.save(); - var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius; - layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color - layer.beginPath(); - layer.fillStyle = options.series.pie.stroke.color; - layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); - layer.fill(); - layer.closePath(); - layer.restore(); - - // add inner stroke - - layer.save(); - layer.beginPath(); - layer.strokeStyle = options.series.pie.stroke.color; - layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); - layer.stroke(); - layer.closePath(); - layer.restore(); - - // TODO: add extra shadow inside hole (with a mask) if the pie is tilted. - } - } - - //-- Additional Interactive related functions -- - - function isPointInPoly(poly, pt) { - for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) - ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1])) - && (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) - && (c = !c); - return c; - } - - function findNearbySlice(mouseX, mouseY) { - - var slices = plot.getData(), - options = plot.getOptions(), - radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius, - x, y; - - for (var i = 0; i < slices.length; ++i) { - - var s = slices[i]; - - if (s.pie.show) { - - ctx.save(); - ctx.beginPath(); - ctx.moveTo(0, 0); // Center of the pie - //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here. - ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false); - ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false); - ctx.closePath(); - x = mouseX - centerLeft; - y = mouseY - centerTop; - - if (ctx.isPointInPath) { - if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) { - ctx.restore(); - return { - datapoint: [s.percent, s.data], - dataIndex: 0, - series: s, - seriesIndex: i - }; - } - } else { - - // excanvas for IE doesn;t support isPointInPath, this is a workaround. - - var p1X = radius * Math.cos(s.startAngle), - p1Y = radius * Math.sin(s.startAngle), - p2X = radius * Math.cos(s.startAngle + s.angle / 4), - p2Y = radius * Math.sin(s.startAngle + s.angle / 4), - p3X = radius * Math.cos(s.startAngle + s.angle / 2), - p3Y = radius * Math.sin(s.startAngle + s.angle / 2), - p4X = radius * Math.cos(s.startAngle + s.angle / 1.5), - p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5), - p5X = radius * Math.cos(s.startAngle + s.angle), - p5Y = radius * Math.sin(s.startAngle + s.angle), - arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]], - arrPoint = [x, y]; - - // TODO: perhaps do some mathematical trickery here with the Y-coordinate to compensate for pie tilt? - - if (isPointInPoly(arrPoly, arrPoint)) { - ctx.restore(); - return { - datapoint: [s.percent, s.data], - dataIndex: 0, - series: s, - seriesIndex: i - }; - } - } - - ctx.restore(); - } - } - - return null; - } - - function onMouseMove(e) { - triggerClickHoverEvent("plothover", e); - } - - function onClick(e) { - triggerClickHoverEvent("plotclick", e); - } - - // trigger click or hover event (they send the same parameters so we share their code) - - function triggerClickHoverEvent(eventname, e) { - - var offset = plot.offset(); - var canvasX = parseInt(e.pageX - offset.left); - var canvasY = parseInt(e.pageY - offset.top); - var item = findNearbySlice(canvasX, canvasY); - - if (options.grid.autoHighlight) { - - // clear auto-highlights - - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.auto == eventname && !(item && h.series == item.series)) { - unhighlight(h.series); - } - } - } - - // highlight the slice - - if (item) { - highlight(item.series, eventname); - } - - // trigger any hover bind events - - var pos = { pageX: e.pageX, pageY: e.pageY }; - target.trigger(eventname, [pos, item]); - } - - function highlight(s, auto) { - //if (typeof s == "number") { - // s = series[s]; - //} - - var i = indexOfHighlight(s); - - if (i == -1) { - highlights.push({ series: s, auto: auto }); - plot.triggerRedrawOverlay(); - } else if (!auto) { - highlights[i].auto = false; - } - } - - function unhighlight(s) { - if (s == null) { - highlights = []; - plot.triggerRedrawOverlay(); - } - - //if (typeof s == "number") { - // s = series[s]; - //} - - var i = indexOfHighlight(s); - - if (i != -1) { - highlights.splice(i, 1); - plot.triggerRedrawOverlay(); - } - } - - function indexOfHighlight(s) { - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.series == s) - return i; - } - return -1; - } - - function drawOverlay(plot, octx) { - - var options = plot.getOptions(); - - var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - octx.save(); - octx.translate(centerLeft, centerTop); - octx.scale(1, options.series.pie.tilt); - - for (var i = 0; i < highlights.length; ++i) { - drawHighlight(highlights[i].series); - } - - drawDonutHole(octx); - - octx.restore(); - - function drawHighlight(series) { - - if (series.angle <= 0 || isNaN(series.angle)) { - return; - } - - //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString(); - octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor - octx.beginPath(); - if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) { - octx.moveTo(0, 0); // Center of the pie - } - octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false); - octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false); - octx.closePath(); - octx.fill(); - } - } - } // end init (plugin body) - - // define pie specific options and their default values - - var options = { - series: { - pie: { - show: false, - radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value) - innerRadius: 0, /* for donut */ - startAngle: 3/2, - tilt: 1, - shadow: { - left: 5, // shadow left offset - top: 15, // shadow top offset - alpha: 0.02 // shadow alpha - }, - offset: { - top: 0, - left: "auto" - }, - stroke: { - color: "#fff", - width: 1 - }, - label: { - show: "auto", - formatter: function(label, slice) { - return "
" + label + "
" + Math.round(slice.percent) + "%
"; - }, // formatter function - radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value) - background: { - color: null, - opacity: 0 - }, - threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow) - }, - combine: { - threshold: -1, // percentage at which to combine little slices into one larger slice - color: null, // color to give the new slice (auto-generated if null) - label: "Other" // label to give the new slice - }, - highlight: { - //color: "#fff", // will add this functionality once parseColor is available - opacity: 0.5 - } - } - } - }; - - $.plot.plugins.push({ - init: init, - options: options, - name: "pie", - version: "1.1" - }); - -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.resize.js b/src/legacy/ui/public/flot-charts/jquery.flot.resize.js deleted file mode 100644 index 8a626dda0addbc..00000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.resize.js +++ /dev/null @@ -1,59 +0,0 @@ -/* Flot plugin for automatically redrawing plots as the placeholder resizes. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -It works by listening for changes on the placeholder div (through the jQuery -resize event plugin) - if the size changes, it will redraw the plot. - -There are no options. If you need to disable the plugin for some plots, you -can just fix the size of their placeholders. - -*/ - -/* Inline dependency: - * jQuery resize event - v1.1 - 3/14/2010 - * http://benalman.com/projects/jquery-resize-plugin/ - * - * Copyright (c) 2010 "Cowboy" Ben Alman - * Dual licensed under the MIT and GPL licenses. - * http://benalman.com/about/license/ - */ -(function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this); - -(function ($) { - var options = { }; // no options - - function init(plot) { - function onResize() { - var placeholder = plot.getPlaceholder(); - - // somebody might have hidden us and we can't plot - // when we don't have the dimensions - if (placeholder.width() == 0 || placeholder.height() == 0) - return; - - plot.resize(); - plot.setupGrid(); - plot.draw(); - } - - function bindEvents(plot, eventHolder) { - plot.getPlaceholder().resize(onResize); - } - - function shutdown(plot, eventHolder) { - plot.getPlaceholder().unbind("resize", onResize); - } - - plot.hooks.bindEvents.push(bindEvents); - plot.hooks.shutdown.push(shutdown); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'resize', - version: '1.0' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.selection.js b/src/legacy/ui/public/flot-charts/jquery.flot.selection.js deleted file mode 100644 index c8707b30f4e6ff..00000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.selection.js +++ /dev/null @@ -1,360 +0,0 @@ -/* Flot plugin for selecting regions of a plot. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin supports these options: - -selection: { - mode: null or "x" or "y" or "xy", - color: color, - shape: "round" or "miter" or "bevel", - minSize: number of pixels -} - -Selection support is enabled by setting the mode to one of "x", "y" or "xy". -In "x" mode, the user will only be able to specify the x range, similarly for -"y" mode. For "xy", the selection becomes a rectangle where both ranges can be -specified. "color" is color of the selection (if you need to change the color -later on, you can get to it with plot.getOptions().selection.color). "shape" -is the shape of the corners of the selection. - -"minSize" is the minimum size a selection can be in pixels. This value can -be customized to determine the smallest size a selection can be and still -have the selection rectangle be displayed. When customizing this value, the -fact that it refers to pixels, not axis units must be taken into account. -Thus, for example, if there is a bar graph in time mode with BarWidth set to 1 -minute, setting "minSize" to 1 will not make the minimum selection size 1 -minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent -"plotunselected" events from being fired when the user clicks the mouse without -dragging. - -When selection support is enabled, a "plotselected" event will be emitted on -the DOM element you passed into the plot function. The event handler gets a -parameter with the ranges selected on the axes, like this: - - placeholder.bind( "plotselected", function( event, ranges ) { - alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to) - // similar for yaxis - with multiple axes, the extra ones are in - // x2axis, x3axis, ... - }); - -The "plotselected" event is only fired when the user has finished making the -selection. A "plotselecting" event is fired during the process with the same -parameters as the "plotselected" event, in case you want to know what's -happening while it's happening, - -A "plotunselected" event with no arguments is emitted when the user clicks the -mouse to remove the selection. As stated above, setting "minSize" to 0 will -destroy this behavior. - -The plugin also adds the following methods to the plot object: - -- setSelection( ranges, preventEvent ) - - Set the selection rectangle. The passed in ranges is on the same form as - returned in the "plotselected" event. If the selection mode is "x", you - should put in either an xaxis range, if the mode is "y" you need to put in - an yaxis range and both xaxis and yaxis if the selection mode is "xy", like - this: - - setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } }); - - setSelection will trigger the "plotselected" event when called. If you don't - want that to happen, e.g. if you're inside a "plotselected" handler, pass - true as the second parameter. If you are using multiple axes, you can - specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of - xaxis, the plugin picks the first one it sees. - -- clearSelection( preventEvent ) - - Clear the selection rectangle. Pass in true to avoid getting a - "plotunselected" event. - -- getSelection() - - Returns the current selection in the same format as the "plotselected" - event. If there's currently no selection, the function returns null. - -*/ - -(function ($) { - function init(plot) { - var selection = { - first: { x: -1, y: -1}, second: { x: -1, y: -1}, - show: false, - active: false - }; - - // FIXME: The drag handling implemented here should be - // abstracted out, there's some similar code from a library in - // the navigation plugin, this should be massaged a bit to fit - // the Flot cases here better and reused. Doing this would - // make this plugin much slimmer. - var savedhandlers = {}; - - var mouseUpHandler = null; - - function onMouseMove(e) { - if (selection.active) { - updateSelection(e); - - plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); - } - } - - function onMouseDown(e) { - if (e.which != 1) // only accept left-click - return; - - // cancel out any text selections - document.body.focus(); - - // prevent text selection and drag in old-school browsers - if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) { - savedhandlers.onselectstart = document.onselectstart; - document.onselectstart = function () { return false; }; - } - if (document.ondrag !== undefined && savedhandlers.ondrag == null) { - savedhandlers.ondrag = document.ondrag; - document.ondrag = function () { return false; }; - } - - setSelectionPos(selection.first, e); - - selection.active = true; - - // this is a bit silly, but we have to use a closure to be - // able to whack the same handler again - mouseUpHandler = function (e) { onMouseUp(e); }; - - $(document).one("mouseup", mouseUpHandler); - } - - function onMouseUp(e) { - mouseUpHandler = null; - - // revert drag stuff for old-school browsers - if (document.onselectstart !== undefined) - document.onselectstart = savedhandlers.onselectstart; - if (document.ondrag !== undefined) - document.ondrag = savedhandlers.ondrag; - - // no more dragging - selection.active = false; - updateSelection(e); - - if (selectionIsSane()) - triggerSelectedEvent(); - else { - // this counts as a clear - plot.getPlaceholder().trigger("plotunselected", [ ]); - plot.getPlaceholder().trigger("plotselecting", [ null ]); - } - - return false; - } - - function getSelection() { - if (!selectionIsSane()) - return null; - - if (!selection.show) return null; - - var r = {}, c1 = selection.first, c2 = selection.second; - $.each(plot.getAxes(), function (name, axis) { - if (axis.used) { - var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); - r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) }; - } - }); - return r; - } - - function triggerSelectedEvent() { - var r = getSelection(); - - plot.getPlaceholder().trigger("plotselected", [ r ]); - - // backwards-compat stuff, to be removed in future - if (r.xaxis && r.yaxis) - plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); - } - - function clamp(min, value, max) { - return value < min ? min: (value > max ? max: value); - } - - function setSelectionPos(pos, e) { - var o = plot.getOptions(); - var offset = plot.getPlaceholder().offset(); - var plotOffset = plot.getPlotOffset(); - pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width()); - pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height()); - - if (o.selection.mode == "y") - pos.x = pos == selection.first ? 0 : plot.width(); - - if (o.selection.mode == "x") - pos.y = pos == selection.first ? 0 : plot.height(); - } - - function updateSelection(pos) { - if (pos.pageX == null) - return; - - setSelectionPos(selection.second, pos); - if (selectionIsSane()) { - selection.show = true; - plot.triggerRedrawOverlay(); - } - else - clearSelection(true); - } - - function clearSelection(preventEvent) { - if (selection.show) { - selection.show = false; - plot.triggerRedrawOverlay(); - if (!preventEvent) - plot.getPlaceholder().trigger("plotunselected", [ ]); - } - } - - // function taken from markings support in Flot - function extractRange(ranges, coord) { - var axis, from, to, key, axes = plot.getAxes(); - - for (var k in axes) { - axis = axes[k]; - if (axis.direction == coord) { - key = coord + axis.n + "axis"; - if (!ranges[key] && axis.n == 1) - key = coord + "axis"; // support x1axis as xaxis - if (ranges[key]) { - from = ranges[key].from; - to = ranges[key].to; - break; - } - } - } - - // backwards-compat stuff - to be removed in future - if (!ranges[key]) { - axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0]; - from = ranges[coord + "1"]; - to = ranges[coord + "2"]; - } - - // auto-reverse as an added bonus - if (from != null && to != null && from > to) { - var tmp = from; - from = to; - to = tmp; - } - - return { from: from, to: to, axis: axis }; - } - - function setSelection(ranges, preventEvent) { - var axis, range, o = plot.getOptions(); - - if (o.selection.mode == "y") { - selection.first.x = 0; - selection.second.x = plot.width(); - } - else { - range = extractRange(ranges, "x"); - - selection.first.x = range.axis.p2c(range.from); - selection.second.x = range.axis.p2c(range.to); - } - - if (o.selection.mode == "x") { - selection.first.y = 0; - selection.second.y = plot.height(); - } - else { - range = extractRange(ranges, "y"); - - selection.first.y = range.axis.p2c(range.from); - selection.second.y = range.axis.p2c(range.to); - } - - selection.show = true; - plot.triggerRedrawOverlay(); - if (!preventEvent && selectionIsSane()) - triggerSelectedEvent(); - } - - function selectionIsSane() { - var minSize = plot.getOptions().selection.minSize; - return Math.abs(selection.second.x - selection.first.x) >= minSize && - Math.abs(selection.second.y - selection.first.y) >= minSize; - } - - plot.clearSelection = clearSelection; - plot.setSelection = setSelection; - plot.getSelection = getSelection; - - plot.hooks.bindEvents.push(function(plot, eventHolder) { - var o = plot.getOptions(); - if (o.selection.mode != null) { - eventHolder.mousemove(onMouseMove); - eventHolder.mousedown(onMouseDown); - } - }); - - - plot.hooks.drawOverlay.push(function (plot, ctx) { - // draw selection - if (selection.show && selectionIsSane()) { - var plotOffset = plot.getPlotOffset(); - var o = plot.getOptions(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - var c = $.color.parse(o.selection.color); - - ctx.strokeStyle = c.scale('a', 0.8).toString(); - ctx.lineWidth = 1; - ctx.lineJoin = o.selection.shape; - ctx.fillStyle = c.scale('a', 0.4).toString(); - - var x = Math.min(selection.first.x, selection.second.x) + 0.5, - y = Math.min(selection.first.y, selection.second.y) + 0.5, - w = Math.abs(selection.second.x - selection.first.x) - 1, - h = Math.abs(selection.second.y - selection.first.y) - 1; - - ctx.fillRect(x, y, w, h); - ctx.strokeRect(x, y, w, h); - - ctx.restore(); - } - }); - - plot.hooks.shutdown.push(function (plot, eventHolder) { - eventHolder.unbind("mousemove", onMouseMove); - eventHolder.unbind("mousedown", onMouseDown); - - if (mouseUpHandler) - $(document).unbind("mouseup", mouseUpHandler); - }); - - } - - $.plot.plugins.push({ - init: init, - options: { - selection: { - mode: null, // one of null, "x", "y" or "xy" - color: "#e8cfac", - shape: "round", // one of "round", "miter", or "bevel" - minSize: 5 // minimum number of pixels - } - }, - name: 'selection', - version: '1.1' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.stack.js b/src/legacy/ui/public/flot-charts/jquery.flot.stack.js deleted file mode 100644 index 0d91c0f3c01608..00000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.stack.js +++ /dev/null @@ -1,188 +0,0 @@ -/* Flot plugin for stacking data sets rather than overlaying them. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin assumes the data is sorted on x (or y if stacking horizontally). -For line charts, it is assumed that if a line has an undefined gap (from a -null point), then the line above it should have the same gap - insert zeros -instead of "null" if you want another behaviour. This also holds for the start -and end of the chart. Note that stacking a mix of positive and negative values -in most instances doesn't make sense (so it looks weird). - -Two or more series are stacked when their "stack" attribute is set to the same -key (which can be any number or string or just "true"). To specify the default -stack, you can set the stack option like this: - - series: { - stack: null/false, true, or a key (number/string) - } - -You can also specify it for a single series, like this: - - $.plot( $("#placeholder"), [{ - data: [ ... ], - stack: true - }]) - -The stacking order is determined by the order of the data series in the array -(later series end up on top of the previous). - -Internally, the plugin modifies the datapoints in each series, adding an -offset to the y value. For line series, extra data points are inserted through -interpolation. If there's a second y value, it's also adjusted (e.g for bar -charts or filled areas). - -*/ - -(function ($) { - var options = { - series: { stack: null } // or number/string - }; - - function init(plot) { - function findMatchingSeries(s, allseries) { - var res = null; - for (var i = 0; i < allseries.length; ++i) { - if (s == allseries[i]) - break; - - if (allseries[i].stack == s.stack) - res = allseries[i]; - } - - return res; - } - - function stackData(plot, s, datapoints) { - if (s.stack == null || s.stack === false) - return; - - var other = findMatchingSeries(s, plot.getData()); - if (!other) - return; - - var ps = datapoints.pointsize, - points = datapoints.points, - otherps = other.datapoints.pointsize, - otherpoints = other.datapoints.points, - newpoints = [], - px, py, intery, qx, qy, bottom, - withlines = s.lines.show, - horizontal = s.bars.horizontal, - withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), - withsteps = withlines && s.lines.steps, - fromgap = true, - keyOffset = horizontal ? 1 : 0, - accumulateOffset = horizontal ? 0 : 1, - i = 0, j = 0, l, m; - - while (true) { - if (i >= points.length) - break; - - l = newpoints.length; - - if (points[i] == null) { - // copy gaps - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - i += ps; - } - else if (j >= otherpoints.length) { - // for lines, we can't use the rest of the points - if (!withlines) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - } - i += ps; - } - else if (otherpoints[j] == null) { - // oops, got a gap - for (m = 0; m < ps; ++m) - newpoints.push(null); - fromgap = true; - j += otherps; - } - else { - // cases where we actually got two points - px = points[i + keyOffset]; - py = points[i + accumulateOffset]; - qx = otherpoints[j + keyOffset]; - qy = otherpoints[j + accumulateOffset]; - bottom = 0; - - if (px == qx) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - newpoints[l + accumulateOffset] += qy; - bottom = qy; - - i += ps; - j += otherps; - } - else if (px > qx) { - // we got past point below, might need to - // insert interpolated extra point - if (withlines && i > 0 && points[i - ps] != null) { - intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); - newpoints.push(qx); - newpoints.push(intery + qy); - for (m = 2; m < ps; ++m) - newpoints.push(points[i + m]); - bottom = qy; - } - - j += otherps; - } - else { // px < qx - if (fromgap && withlines) { - // if we come from a gap, we just skip this point - i += ps; - continue; - } - - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - // we might be able to interpolate a point below, - // this can give us a better y - if (withlines && j > 0 && otherpoints[j - otherps] != null) - bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); - - newpoints[l + accumulateOffset] += bottom; - - i += ps; - } - - fromgap = false; - - if (l != newpoints.length && withbottom) - newpoints[l + 2] += bottom; - } - - // maintain the line steps invariant - if (withsteps && l != newpoints.length && l > 0 - && newpoints[l] != null - && newpoints[l] != newpoints[l - ps] - && newpoints[l + 1] != newpoints[l - ps + 1]) { - for (m = 0; m < ps; ++m) - newpoints[l + ps + m] = newpoints[l + m]; - newpoints[l + 1] = newpoints[l - ps + 1]; - } - } - - datapoints.points = newpoints; - } - - plot.hooks.processDatapoints.push(stackData); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'stack', - version: '1.2' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.symbol.js b/src/legacy/ui/public/flot-charts/jquery.flot.symbol.js deleted file mode 100644 index 79f634971b6fa6..00000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.symbol.js +++ /dev/null @@ -1,71 +0,0 @@ -/* Flot plugin that adds some extra symbols for plotting points. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The symbols are accessed as strings through the standard symbol options: - - series: { - points: { - symbol: "square" // or "diamond", "triangle", "cross" - } - } - -*/ - -(function ($) { - function processRawData(plot, series, datapoints) { - // we normalize the area of each symbol so it is approximately the - // same as a circle of the given radius - - var handlers = { - square: function (ctx, x, y, radius, shadow) { - // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 - var size = radius * Math.sqrt(Math.PI) / 2; - ctx.rect(x - size, y - size, size + size, size + size); - }, - diamond: function (ctx, x, y, radius, shadow) { - // pi * r^2 = 2s^2 => s = r * sqrt(pi/2) - var size = radius * Math.sqrt(Math.PI / 2); - ctx.moveTo(x - size, y); - ctx.lineTo(x, y - size); - ctx.lineTo(x + size, y); - ctx.lineTo(x, y + size); - ctx.lineTo(x - size, y); - }, - triangle: function (ctx, x, y, radius, shadow) { - // pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3)) - var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3)); - var height = size * Math.sin(Math.PI / 3); - ctx.moveTo(x - size/2, y + height/2); - ctx.lineTo(x + size/2, y + height/2); - if (!shadow) { - ctx.lineTo(x, y - height/2); - ctx.lineTo(x - size/2, y + height/2); - } - }, - cross: function (ctx, x, y, radius, shadow) { - // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 - var size = radius * Math.sqrt(Math.PI) / 2; - ctx.moveTo(x - size, y - size); - ctx.lineTo(x + size, y + size); - ctx.moveTo(x - size, y + size); - ctx.lineTo(x + size, y - size); - } - }; - - var s = series.points.symbol; - if (handlers[s]) - series.points.symbol = handlers[s]; - } - - function init(plot) { - plot.hooks.processDatapoints.push(processRawData); - } - - $.plot.plugins.push({ - init: init, - name: 'symbols', - version: '1.0' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.threshold.js b/src/legacy/ui/public/flot-charts/jquery.flot.threshold.js deleted file mode 100644 index 8c99c401d87e5a..00000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.threshold.js +++ /dev/null @@ -1,142 +0,0 @@ -/* Flot plugin for thresholding data. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin supports these options: - - series: { - threshold: { - below: number - color: colorspec - } - } - -It can also be applied to a single series, like this: - - $.plot( $("#placeholder"), [{ - data: [ ... ], - threshold: { ... } - }]) - -An array can be passed for multiple thresholding, like this: - - threshold: [{ - below: number1 - color: color1 - },{ - below: number2 - color: color2 - }] - -These multiple threshold objects can be passed in any order since they are -sorted by the processing function. - -The data points below "below" are drawn with the specified color. This makes -it easy to mark points below 0, e.g. for budget data. - -Internally, the plugin works by splitting the data into two series, above and -below the threshold. The extra series below the threshold will have its label -cleared and the special "originSeries" attribute set to the original series. -You may need to check for this in hover events. - -*/ - -(function ($) { - var options = { - series: { threshold: null } // or { below: number, color: color spec} - }; - - function init(plot) { - function thresholdData(plot, s, datapoints, below, color) { - var ps = datapoints.pointsize, i, x, y, p, prevp, - thresholded = $.extend({}, s); // note: shallow copy - - thresholded.datapoints = { points: [], pointsize: ps, format: datapoints.format }; - thresholded.label = null; - thresholded.color = color; - thresholded.threshold = null; - thresholded.originSeries = s; - thresholded.data = []; - - var origpoints = datapoints.points, - addCrossingPoints = s.lines.show; - - var threspoints = []; - var newpoints = []; - var m; - - for (i = 0; i < origpoints.length; i += ps) { - x = origpoints[i]; - y = origpoints[i + 1]; - - prevp = p; - if (y < below) - p = threspoints; - else - p = newpoints; - - if (addCrossingPoints && prevp != p && x != null - && i > 0 && origpoints[i - ps] != null) { - var interx = x + (below - y) * (x - origpoints[i - ps]) / (y - origpoints[i - ps + 1]); - prevp.push(interx); - prevp.push(below); - for (m = 2; m < ps; ++m) - prevp.push(origpoints[i + m]); - - p.push(null); // start new segment - p.push(null); - for (m = 2; m < ps; ++m) - p.push(origpoints[i + m]); - p.push(interx); - p.push(below); - for (m = 2; m < ps; ++m) - p.push(origpoints[i + m]); - } - - p.push(x); - p.push(y); - for (m = 2; m < ps; ++m) - p.push(origpoints[i + m]); - } - - datapoints.points = newpoints; - thresholded.datapoints.points = threspoints; - - if (thresholded.datapoints.points.length > 0) { - var origIndex = $.inArray(s, plot.getData()); - // Insert newly-generated series right after original one (to prevent it from becoming top-most) - plot.getData().splice(origIndex + 1, 0, thresholded); - } - - // FIXME: there are probably some edge cases left in bars - } - - function processThresholds(plot, s, datapoints) { - if (!s.threshold) - return; - - if (s.threshold instanceof Array) { - s.threshold.sort(function(a, b) { - return a.below - b.below; - }); - - $(s.threshold).each(function(i, th) { - thresholdData(plot, s, datapoints, th.below, th.color); - }); - } - else { - thresholdData(plot, s, datapoints, s.threshold.below, s.threshold.color); - } - } - - plot.hooks.processDatapoints.push(processThresholds); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'threshold', - version: '1.2' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.time.js b/src/legacy/ui/public/flot-charts/jquery.flot.time.js deleted file mode 100644 index 7612a033027646..00000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.time.js +++ /dev/null @@ -1,473 +0,0 @@ -/* Pretty handling of time axes. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -Set axis.mode to "time" to enable. See the section "Time series data" in -API.txt for details. - -*/ - -import { i18n } from '@kbn/i18n'; - -(function($) { - - var options = { - xaxis: { - timezone: null, // "browser" for local to the client or timezone for timezone-js - timeformat: null, // format string to use - twelveHourClock: false, // 12 or 24 time in time mode - monthNames: null // list of names of months - } - }; - - // round to nearby lower multiple of base - - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - - // Returns a string with the date d formatted according to fmt. - // A subset of the Open Group's strftime format is supported. - - function formatDate(d, fmt, monthNames, dayNames) { - - if (typeof d.strftime == "function") { - return d.strftime(fmt); - } - - var leftPad = function(n, pad) { - n = "" + n; - pad = "" + (pad == null ? "0" : pad); - return n.length == 1 ? pad + n : n; - }; - - var r = []; - var escape = false; - var hours = d.getHours(); - var isAM = hours < 12; - - if (monthNames == null) { - monthNames = [ - i18n.translate('common.ui.flotCharts.janLabel', { - defaultMessage: 'Jan', - }), i18n.translate('common.ui.flotCharts.febLabel', { - defaultMessage: 'Feb', - }), i18n.translate('common.ui.flotCharts.marLabel', { - defaultMessage: 'Mar', - }), i18n.translate('common.ui.flotCharts.aprLabel', { - defaultMessage: 'Apr', - }), i18n.translate('common.ui.flotCharts.mayLabel', { - defaultMessage: 'May', - }), i18n.translate('common.ui.flotCharts.junLabel', { - defaultMessage: 'Jun', - }), i18n.translate('common.ui.flotCharts.julLabel', { - defaultMessage: 'Jul', - }), i18n.translate('common.ui.flotCharts.augLabel', { - defaultMessage: 'Aug', - }), i18n.translate('common.ui.flotCharts.sepLabel', { - defaultMessage: 'Sep', - }), i18n.translate('common.ui.flotCharts.octLabel', { - defaultMessage: 'Oct', - }), i18n.translate('common.ui.flotCharts.novLabel', { - defaultMessage: 'Nov', - }), i18n.translate('common.ui.flotCharts.decLabel', { - defaultMessage: 'Dec', - })]; - } - - if (dayNames == null) { - dayNames = [i18n.translate('common.ui.flotCharts.sunLabel', { - defaultMessage: 'Sun', - }), i18n.translate('common.ui.flotCharts.monLabel', { - defaultMessage: 'Mon', - }), i18n.translate('common.ui.flotCharts.tueLabel', { - defaultMessage: 'Tue', - }), i18n.translate('common.ui.flotCharts.wedLabel', { - defaultMessage: 'Wed', - }), i18n.translate('common.ui.flotCharts.thuLabel', { - defaultMessage: 'Thu', - }), i18n.translate('common.ui.flotCharts.friLabel', { - defaultMessage: 'Fri', - }), i18n.translate('common.ui.flotCharts.satLabel', { - defaultMessage: 'Sat', - })]; - } - - var hours12; - - if (hours > 12) { - hours12 = hours - 12; - } else if (hours == 0) { - hours12 = 12; - } else { - hours12 = hours; - } - - for (var i = 0; i < fmt.length; ++i) { - - var c = fmt.charAt(i); - - if (escape) { - switch (c) { - case 'a': c = "" + dayNames[d.getDay()]; break; - case 'b': c = "" + monthNames[d.getMonth()]; break; - case 'd': c = leftPad(d.getDate()); break; - case 'e': c = leftPad(d.getDate(), " "); break; - case 'h': // For back-compat with 0.7; remove in 1.0 - case 'H': c = leftPad(hours); break; - case 'I': c = leftPad(hours12); break; - case 'l': c = leftPad(hours12, " "); break; - case 'm': c = leftPad(d.getMonth() + 1); break; - case 'M': c = leftPad(d.getMinutes()); break; - // quarters not in Open Group's strftime specification - case 'q': - c = "" + (Math.floor(d.getMonth() / 3) + 1); break; - case 'S': c = leftPad(d.getSeconds()); break; - case 'y': c = leftPad(d.getFullYear() % 100); break; - case 'Y': c = "" + d.getFullYear(); break; - case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; - case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; - case 'w': c = "" + d.getDay(); break; - } - r.push(c); - escape = false; - } else { - if (c == "%") { - escape = true; - } else { - r.push(c); - } - } - } - - return r.join(""); - } - - // To have a consistent view of time-based data independent of which time - // zone the client happens to be in we need a date-like object independent - // of time zones. This is done through a wrapper that only calls the UTC - // versions of the accessor methods. - - function makeUtcWrapper(d) { - - function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { - sourceObj[sourceMethod] = function() { - return targetObj[targetMethod].apply(targetObj, arguments); - }; - }; - - var utc = { - date: d - }; - - // support strftime, if found - - if (d.strftime != undefined) { - addProxyMethod(utc, "strftime", d, "strftime"); - } - - addProxyMethod(utc, "getTime", d, "getTime"); - addProxyMethod(utc, "setTime", d, "setTime"); - - var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; - - for (var p = 0; p < props.length; p++) { - addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); - addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); - } - - return utc; - }; - - // select time zone strategy. This returns a date-like object tied to the - // desired timezone - - function dateGenerator(ts, opts) { - if (opts.timezone == "browser") { - return new Date(ts); - } else if (!opts.timezone || opts.timezone == "utc") { - return makeUtcWrapper(new Date(ts)); - } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { - var d = new timezoneJS.Date(); - // timezone-js is fickle, so be sure to set the time zone before - // setting the time. - d.setTimezone(opts.timezone); - d.setTime(ts); - return d; - } else { - return makeUtcWrapper(new Date(ts)); - } - } - - // map of app. size of time units in milliseconds - - var timeUnitSize = { - "second": 1000, - "minute": 60 * 1000, - "hour": 60 * 60 * 1000, - "day": 24 * 60 * 60 * 1000, - "month": 30 * 24 * 60 * 60 * 1000, - "quarter": 3 * 30 * 24 * 60 * 60 * 1000, - "year": 365.2425 * 24 * 60 * 60 * 1000 - }; - - // the allowed tick sizes, after 1 year we use - // an integer algorithm - - var baseSpec = [ - [1, "second"], [2, "second"], [5, "second"], [10, "second"], - [30, "second"], - [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], - [30, "minute"], - [1, "hour"], [2, "hour"], [4, "hour"], - [8, "hour"], [12, "hour"], - [1, "day"], [2, "day"], [3, "day"], - [0.25, "month"], [0.5, "month"], [1, "month"], - [2, "month"] - ]; - - // we don't know which variant(s) we'll need yet, but generating both is - // cheap - - var specMonths = baseSpec.concat([[3, "month"], [6, "month"], - [1, "year"]]); - var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], - [1, "year"]]); - - function init(plot) { - plot.hooks.processOptions.push(function (plot, options) { - $.each(plot.getAxes(), function(axisName, axis) { - - var opts = axis.options; - - if (opts.mode == "time") { - axis.tickGenerator = function(axis) { - - var ticks = []; - var d = dateGenerator(axis.min, opts); - var minSize = 0; - - // make quarter use a possibility if quarters are - // mentioned in either of these options - - var spec = (opts.tickSize && opts.tickSize[1] === - "quarter") || - (opts.minTickSize && opts.minTickSize[1] === - "quarter") ? specQuarters : specMonths; - - if (opts.minTickSize != null) { - if (typeof opts.tickSize == "number") { - minSize = opts.tickSize; - } else { - minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; - } - } - - for (var i = 0; i < spec.length - 1; ++i) { - if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] - + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 - && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { - break; - } - } - - var size = spec[i][0]; - var unit = spec[i][1]; - - // special-case the possibility of several years - - if (unit == "year") { - - // if given a minTickSize in years, just use it, - // ensuring that it's an integer - - if (opts.minTickSize != null && opts.minTickSize[1] == "year") { - size = Math.floor(opts.minTickSize[0]); - } else { - - var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); - var norm = (axis.delta / timeUnitSize.year) / magn; - - if (norm < 1.5) { - size = 1; - } else if (norm < 3) { - size = 2; - } else if (norm < 7.5) { - size = 5; - } else { - size = 10; - } - - size *= magn; - } - - // minimum size for years is 1 - - if (size < 1) { - size = 1; - } - } - - axis.tickSize = opts.tickSize || [size, unit]; - var tickSize = axis.tickSize[0]; - unit = axis.tickSize[1]; - - var step = tickSize * timeUnitSize[unit]; - - if (unit == "second") { - d.setSeconds(floorInBase(d.getSeconds(), tickSize)); - } else if (unit == "minute") { - d.setMinutes(floorInBase(d.getMinutes(), tickSize)); - } else if (unit == "hour") { - d.setHours(floorInBase(d.getHours(), tickSize)); - } else if (unit == "month") { - d.setMonth(floorInBase(d.getMonth(), tickSize)); - } else if (unit == "quarter") { - d.setMonth(3 * floorInBase(d.getMonth() / 3, - tickSize)); - } else if (unit == "year") { - d.setFullYear(floorInBase(d.getFullYear(), tickSize)); - } - - // reset smaller components - - d.setMilliseconds(0); - - if (step >= timeUnitSize.minute) { - d.setSeconds(0); - } - if (step >= timeUnitSize.hour) { - d.setMinutes(0); - } - if (step >= timeUnitSize.day) { - d.setHours(0); - } - if (step >= timeUnitSize.day * 4) { - d.setDate(1); - } - if (step >= timeUnitSize.month * 2) { - d.setMonth(floorInBase(d.getMonth(), 3)); - } - if (step >= timeUnitSize.quarter * 2) { - d.setMonth(floorInBase(d.getMonth(), 6)); - } - if (step >= timeUnitSize.year) { - d.setMonth(0); - } - - var carry = 0; - var v = Number.NaN; - var prev; - - do { - - prev = v; - v = d.getTime(); - ticks.push(v); - - if (unit == "month" || unit == "quarter") { - if (tickSize < 1) { - - // a bit complicated - we'll divide the - // month/quarter up but we need to take - // care of fractions so we don't end up in - // the middle of a day - - d.setDate(1); - var start = d.getTime(); - d.setMonth(d.getMonth() + - (unit == "quarter" ? 3 : 1)); - var end = d.getTime(); - d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); - carry = d.getHours(); - d.setHours(0); - } else { - d.setMonth(d.getMonth() + - tickSize * (unit == "quarter" ? 3 : 1)); - } - } else if (unit == "year") { - d.setFullYear(d.getFullYear() + tickSize); - } else { - d.setTime(v + step); - } - } while (v < axis.max && v != prev); - - return ticks; - }; - - axis.tickFormatter = function (v, axis) { - - var d = dateGenerator(v, axis.options); - - // first check global format - - if (opts.timeformat != null) { - return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); - } - - // possibly use quarters if quarters are mentioned in - // any of these places - - var useQuarters = (axis.options.tickSize && - axis.options.tickSize[1] == "quarter") || - (axis.options.minTickSize && - axis.options.minTickSize[1] == "quarter"); - - var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; - var span = axis.max - axis.min; - var suffix = (opts.twelveHourClock) ? " %p" : ""; - var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; - var fmt; - - if (t < timeUnitSize.minute) { - fmt = hourCode + ":%M:%S" + suffix; - } else if (t < timeUnitSize.day) { - if (span < 2 * timeUnitSize.day) { - fmt = hourCode + ":%M" + suffix; - } else { - fmt = "%b %d " + hourCode + ":%M" + suffix; - } - } else if (t < timeUnitSize.month) { - fmt = "%b %d"; - } else if ((useQuarters && t < timeUnitSize.quarter) || - (!useQuarters && t < timeUnitSize.year)) { - if (span < timeUnitSize.year) { - fmt = "%b"; - } else { - fmt = "%b %Y"; - } - } else if (useQuarters && t < timeUnitSize.year) { - if (span < timeUnitSize.year) { - fmt = "Q%q"; - } else { - fmt = "Q%q %Y"; - } - } else { - fmt = "%Y"; - } - - var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); - - return rt; - }; - } - }); - }); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'time', - version: '1.0' - }); - - // Time-axis support used to be in Flot core, which exposed the - // formatDate function on the plot object. Various plugins depend - // on the function, so we need to re-expose it here. - - $.plot.formatDate = formatDate; - $.plot.dateGenerator = dateGenerator; - -})(jQuery); diff --git a/src/legacy/ui/public/i18n/__snapshots__/index.test.tsx.snap b/src/legacy/ui/public/i18n/__snapshots__/index.test.tsx.snap deleted file mode 100644 index fd6a0a07ba39ce..00000000000000 --- a/src/legacy/ui/public/i18n/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,10 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ui/i18n renders children and forwards properties 1`] = ` -
- Context: - - Child: some prop:100500 - -
-`; diff --git a/src/legacy/ui/public/i18n/index.test.tsx b/src/legacy/ui/public/i18n/index.test.tsx deleted file mode 100644 index be8ab4cf8d6962..00000000000000 --- a/src/legacy/ui/public/i18n/index.test.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 { render } from 'enzyme'; -import PropTypes from 'prop-types'; -import React from 'react'; - -jest.mock('angular-sanitize', () => {}); -jest.mock('ui/new_platform', () => ({ - npStart: { - core: { - i18n: { Context: ({ children }: any) =>
Context: {children}
}, - }, - }, -})); - -import { wrapInI18nContext } from '.'; - -describe('ui/i18n', () => { - test('renders children and forwards properties', () => { - const mockPropTypes = { - stringProp: PropTypes.string.isRequired, - numberProp: PropTypes.number, - }; - - const WrappedComponent = wrapInI18nContext( - class extends React.PureComponent<{ [P in keyof typeof mockPropTypes]: unknown }> { - public static propTypes = mockPropTypes; - - public render() { - return ( - - Child: {this.props.stringProp}:{this.props.numberProp} - - ); - } - } - ); - - expect(WrappedComponent.propTypes).toBe(mockPropTypes); - expect( - render() - ).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/i18n/index.tsx b/src/legacy/ui/public/i18n/index.tsx deleted file mode 100644 index 290e82a1334b96..00000000000000 --- a/src/legacy/ui/public/i18n/index.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 React from 'react'; -// required for `ngSanitize` angular module -import 'angular-sanitize'; - -import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -import { npStart } from 'ui/new_platform'; - -export const I18nContext = npStart.core.i18n.Context; - -export function wrapInI18nContext

(ComponentToWrap: React.ComponentType

) { - const ContextWrapper: React.FC

= (props) => { - return ( - - - - ); - }; - - // Original propTypes from the wrapped component should be re-exposed - // since it will be used by reactDirective Angular service - // that will rely on propTypes to watch attributes with these names - ContextWrapper.propTypes = ComponentToWrap.propTypes; - - return ContextWrapper; -} - -uiModules - .get('i18n', ['ngSanitize']) - .provider('i18n', I18nProvider) - .filter('i18n', i18nFilter) - .directive('i18nId', i18nDirective); diff --git a/src/legacy/ui/public/indexed_array/__tests__/indexed_array.js b/src/legacy/ui/public/indexed_array/__tests__/indexed_array.js deleted file mode 100644 index df96a58a6e99fa..00000000000000 --- a/src/legacy/ui/public/indexed_array/__tests__/indexed_array.js +++ /dev/null @@ -1,208 +0,0 @@ -/* - * 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 _ from 'lodash'; -import expect from '@kbn/expect'; -import { IndexedArray } from '..'; - -// this is generally a data-structure that IndexedArray is good for managing -const users = [ - { name: 'John', id: 6, username: 'beast', group: 'admins' }, - { name: 'Anon', id: 0, username: 'shhhh', group: 'secret' }, - { name: 'Fern', id: 42, username: 'kitty', group: 'editor' }, - { name: 'Mary', id: 55, username: 'sheep', group: 'editor' }, -]; - -// this is how we used to accomplish this, before IndexedArray -users.byName = _.keyBy(users, 'name'); -users.byUsername = _.keyBy(users, 'username'); -users.byGroup = _.groupBy(users, 'group'); -users.inIdOrder = _.sortBy(users, 'id'); - -// then things started becoming unruly... so IndexedArray! - -describe('IndexedArray', function () { - describe('Basics', function () { - let reg; - - beforeEach(function () { - reg = new IndexedArray(); - }); - - it('Extends Array', function () { - expect(reg).to.be.a(Array); - }); - - it('fails basic lodash check', function () { - expect(Array.isArray(reg)).to.be(false); - }); - - it('clones to an object', function () { - expect(_.isObject(_.clone(reg))).to.be(true); - expect(Array.isArray(_.clone(reg))).to.be(false); - }); - }); - - describe('Indexing', function () { - it('provides the initial set', function () { - const reg = new IndexedArray({ - initialSet: [1, 2, 3], - }); - - expect(reg).to.have.length(3); - - reg.forEach(function (v, i) { - expect(v).to.eql(i + 1); - }); - }); - - it('indexes the initial set', function () { - const reg = new IndexedArray({ - index: ['username'], - initialSet: users, - }); - - expect(reg).to.have.property('byUsername'); - expect(reg.byUsername).to.eql(users.byUsername); - }); - - it('updates indices after values are added', function () { - // split up the user list, and add it in chunks - const firstUser = users.slice(0, 1).pop(); - const otherUsers = users.slice(1); - - // start off with all but the first - const reg = new IndexedArray({ - group: ['group'], - order: ['id'], - initialSet: otherUsers, - }); - - // add the first - reg.push(firstUser); - - // end up with the same structure that is in the users fixture - expect(Object.keys(reg.byGroup).length).to.be(Object.keys(users.byGroup).length); - for (const group of Object.keys(reg.byGroup)) { - expect(reg.byGroup[group].toJSON()).to.eql(users.byGroup[group]); - } - - expect(reg.inIdOrder).to.eql(users.inIdOrder); - }); - - it('updates indices after values are removed', function () { - // start off with all - const reg = new IndexedArray({ - group: ['group'], - order: ['id'], - initialSet: users, - }); - - // remove the last - reg.pop(); - - const expectedCount = users.length - 1; - // indexed lists should be updated - expect(reg).to.have.length(expectedCount); - - const sumOfGroups = _.reduce( - reg.byGroup, - function (note, group) { - return note + group.length; - }, - 0 - ); - expect(sumOfGroups).to.eql(expectedCount); - }); - - it('removes items based on a predicate', function () { - const reg = new IndexedArray({ - group: ['group'], - order: ['id'], - initialSet: users, - }); - - reg.remove({ name: 'John' }); - - expect(_.isEqual(reg.raw, reg.slice(0))).to.be(true); - expect(reg.length).to.be(3); - expect(reg[0].name).to.be('Anon'); - }); - - it('updates indices after values are re-ordered', function () { - const rawUsers = users.slice(0); - - // collect and shuffle the ids available - let ids = []; - _.times(rawUsers.length, function (i) { - ids.push(i); - }); - ids = _.shuffle(ids); - - // move something here - const toI = ids.shift(); - // from here - const fromI = ids.shift(); - // do the move - const move = function (arr) { - arr.splice(toI, 0, arr.splice(fromI, 1)[0]); - }; - - const reg = new IndexedArray({ - index: ['username'], - initialSet: rawUsers, - }); - - const index = reg.byUsername; - - move(reg); - - expect(reg.byUsername).to.eql(index); - expect(reg.byUsername).to.not.be(index); - }); - }); - - describe('Ordering', function () { - it('ordering is case insensitive', function () { - const reg = new IndexedArray({ - index: ['title'], - order: ['title'], - initialSet: [{ title: 'APM' }, { title: 'Advanced Settings' }], - }); - - const ordered = reg.inTitleOrder; - expect(ordered[0].title).to.be('Advanced Settings'); - expect(ordered[1].title).to.be('APM'); - }); - - it('ordering handles numbers', function () { - const reg = new IndexedArray({ - index: ['id'], - order: ['id'], - initialSet: users, - }); - - const ordered = reg.inIdOrder; - expect(ordered[0].id).to.be(0); - expect(ordered[1].id).to.be(6); - expect(ordered[2].id).to.be(42); - expect(ordered[3].id).to.be(55); - }); - }); -}); diff --git a/src/legacy/ui/public/indexed_array/__tests__/inflector.js b/src/legacy/ui/public/indexed_array/__tests__/inflector.js deleted file mode 100644 index 49ac79094e5018..00000000000000 --- a/src/legacy/ui/public/indexed_array/__tests__/inflector.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 { inflector } from '../inflector'; -import expect from '@kbn/expect'; - -describe('IndexedArray Inflector', function () { - it('returns a function', function () { - const getter = inflector(); - expect(getter).to.be.a('function'); - }); - - describe('fn', function () { - it('prepends a prefix', function () { - const inflect = inflector('my'); - - expect(inflect('Family')).to.be('myFamily'); - expect(inflect('family')).to.be('myFamily'); - expect(inflect('fAmIlY')).to.be('myFAmIlY'); - }); - - it('adds both a prefix and suffix', function () { - const inflect = inflector('foo', 'Bar'); - - expect(inflect('box')).to.be('fooBoxBar'); - expect(inflect('box.car.MAX')).to.be('fooBoxCarMaxBar'); - expect(inflect('BaZzY')).to.be('fooBaZzYBar'); - }); - - it('ignores prefix if it is already at the end of the inflected string', function () { - const inflect = inflector('foo', 'Bar'); - expect(inflect('fooBox')).to.be('fooBoxBar'); - expect(inflect('FooBox')).to.be('FooBoxBar'); - }); - - it('ignores postfix if it is already at the end of the inflected string', function () { - const inflect = inflector('foo', 'Bar'); - expect(inflect('bar')).to.be('fooBar'); - expect(inflect('showBoxBar')).to.be('fooShowBoxBar'); - }); - - it('works with "name"', function () { - const inflect = inflector('in', 'Order'); - expect(inflect('name')).to.be('inNameOrder'); - }); - }); -}); diff --git a/src/legacy/ui/public/indexed_array/helpers/organize_by.test.ts b/src/legacy/ui/public/indexed_array/helpers/organize_by.test.ts deleted file mode 100644 index fc4ca8469382ad..00000000000000 --- a/src/legacy/ui/public/indexed_array/helpers/organize_by.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 { groupBy } from 'lodash'; -import { organizeBy } from './organize_by'; - -describe('organizeBy', () => { - test('it works', () => { - const col = [ - { - name: 'one', - roles: ['user', 'admin', 'owner'], - }, - { - name: 'two', - roles: ['user'], - }, - { - name: 'three', - roles: ['user'], - }, - { - name: 'four', - roles: ['user', 'admin'], - }, - ]; - - const resp = organizeBy(col, 'roles'); - expect(resp).toHaveProperty('user'); - expect(resp.user.length).toBe(4); - - expect(resp).toHaveProperty('admin'); - expect(resp.admin.length).toBe(2); - - expect(resp).toHaveProperty('owner'); - expect(resp.owner.length).toBe(1); - }); - - test('behaves just like groupBy in normal scenarios', () => { - const col = [{ name: 'one' }, { name: 'two' }, { name: 'three' }, { name: 'four' }]; - - const orgs = organizeBy(col, 'name'); - const groups = groupBy(col, 'name'); - - expect(orgs).toEqual(groups); - }); -}); diff --git a/src/legacy/ui/public/indexed_array/helpers/organize_by.ts b/src/legacy/ui/public/indexed_array/helpers/organize_by.ts deleted file mode 100644 index e923767c892cd5..00000000000000 --- a/src/legacy/ui/public/indexed_array/helpers/organize_by.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 { each, isFunction } from 'lodash'; - -/** - * Like _.groupBy, but allows specifying multiple groups for a - * single object. - * - * organizeBy([{ a: [1, 2, 3] }, { b: true, a: [1, 4] }], 'a') - * // Object {1: Array[2], 2: Array[1], 3: Array[1], 4: Array[1]} - * - * _.groupBy([{ a: [1, 2, 3] }, { b: true, a: [1, 4] }], 'a') - * // Object {'1,2,3': Array[1], '1,4': Array[1]} - * - * @param {array} collection - the list of values to organize - * @param {Function} callback - either a property name, or a callback. - * @return {object} - */ -export function organizeBy(collection: object[], callback: ((obj: object) => string) | string) { - const buckets: { [key: string]: object[] } = {}; - - function add(key: string, obj: object) { - if (!buckets[key]) { - buckets[key] = []; - } - buckets[key].push(obj); - } - - each(collection, (obj: Record) => { - const keys = isFunction(callback) ? callback(obj) : obj[callback]; - - if (!Array.isArray(keys)) { - add(keys, obj); - return; - } - - let length = keys.length; - while (length-- > 0) { - add(keys[length], obj); - } - }); - - return buckets; -} diff --git a/src/legacy/ui/public/indexed_array/index.d.ts b/src/legacy/ui/public/indexed_array/index.d.ts deleted file mode 100644 index 21c0a818731ac5..00000000000000 --- a/src/legacy/ui/public/indexed_array/index.d.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 { ListIterator } from 'lodash'; - -interface IndexedArrayConfig { - index?: string[]; - group?: string[]; - order?: string[]; - initialSet?: T[]; - immutable?: boolean; -} - -declare class IndexedArray extends Array { - public immutable: boolean; - public raw: T[]; - - // These may not actually be present, as they are dynamically defined - public inOrder: T[]; - public byType: Record; - public byName: Record; - - constructor(config: IndexedArrayConfig); - - public remove(predicate: ListIterator): T[]; - - public toJSON(): T[]; -} diff --git a/src/legacy/ui/public/indexed_array/index.js b/src/legacy/ui/public/indexed_array/index.js deleted file mode 100644 index 6a42961c9e680b..00000000000000 --- a/src/legacy/ui/public/indexed_array/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 { IndexedArray } from './indexed_array'; diff --git a/src/legacy/ui/public/indexed_array/indexed_array.js b/src/legacy/ui/public/indexed_array/indexed_array.js deleted file mode 100644 index b9a427b8da7adc..00000000000000 --- a/src/legacy/ui/public/indexed_array/indexed_array.js +++ /dev/null @@ -1,235 +0,0 @@ -/* - * 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 _ from 'lodash'; -import { inflector } from './inflector'; -import { organizeBy } from './helpers/organize_by'; - -const pathGetter = _(_.get).rearg(1, 0).ary(2); -const inflectIndex = inflector('by'); -const inflectOrder = inflector('in', 'Order'); - -const CLEAR_CACHE = {}; -const OPT_NAMES = ['index', 'group', 'order', 'initialSet', 'immutable']; - -/** - * Generic extension of Array class, which will index (and reindex) the - * objects it contains based on their properties. - * - * @param {Object} config describes the properties of this registry object - * @param {Array} [config.index] a list of props/paths that should be used to index the docs. - * @param {Array} [config.group] a list of keys/paths to group docs by. - * @param {Array} [config.order] a list of keys/paths to order the keys by. - * @param {Array} [config.initialSet] the initial dataset the IndexedArray should contain. - * @param {boolean} [config.immutable] a flag that hints to people reading the implementation that this IndexedArray - * should not be modified - */ - -export class IndexedArray { - static OPT_NAMES = OPT_NAMES; - - constructor(config) { - config = _.pick(config || {}, OPT_NAMES); - - // use defineProperty so that value can't be changed - Object.defineProperty(this, 'raw', { value: [] }); - - this._indexNames = _.union( - this._setupIndex(config.group, inflectIndex, organizeByIndexedArray(config)), - this._setupIndex(config.index, inflectIndex, _.keyBy), - this._setupIndex(config.order, inflectOrder, (raw, pluckValue) => { - return [...raw].sort((itemA, itemB) => { - const a = pluckValue(itemA); - const b = pluckValue(itemB); - if (typeof a === 'number' && typeof b === 'number') { - return a - b; - } - return String(a).toLowerCase().localeCompare(String(b).toLowerCase()); - }); - }) - ); - - if (config.initialSet) { - this.push.apply(this, config.initialSet); - } - - Object.defineProperty(this, 'immutable', { value: !!config.immutable }); - } - - /** - * Remove items from this based on a predicate - * @param {Function|Object|string} predicate - the predicate used to decide what is removed - * @return {array} - the removed data - */ - remove(predicate) { - this._assertMutable('remove'); - const out = _.remove(this, predicate); - _.remove(this.raw, predicate); - this._clearIndices(); - return out; - } - - /** - * provide a hook for the JSON serializer - * @return {array} - a plain, vanilla array with our same data - */ - toJSON() { - return this.raw; - } - - // wrappers for mutable Array methods - copyWithin(...args) { - return this._mutation('copyWithin', args); - } - fill(...args) { - return this._mutation('fill', args); - } - pop(...args) { - return this._mutation('pop', args); - } - push(...args) { - return this._mutation('push', args); - } - reverse(...args) { - return this._mutation('reverse', args); - } - shift(...args) { - return this._mutation('shift', args); - } - sort(...args) { - return this._mutation('sort', args); - } - splice(...args) { - return this._mutation('splice', args); - } - unshift(...args) { - return this._mutation('unshift', args); - } - - /** - * If this instance of IndexedArray is not mutable, throw an error - * @private - * @param {String} methodName - user facing method name, for error message - * @return {undefined} - */ - _assertMutable(methodName) { - if (this.immutable) { - throw new Error(`${methodName}() is not allowed on immutable IndexedArray instances`); - } - } - - /** - * Execute some mutable method from the Array prototype - * on the IndexedArray and this.raw - * - * @private - * @param {string} methodName - * @param {Array} args - * @return {any} - */ - _mutation(methodName, args) { - this._assertMutable(methodName); - super[methodName].apply(this, args); - this._clearIndices(); - return super[methodName].apply(this.raw, args); - } - - /** - * Create indices for a group of object properties. getters and setters are used to - * read and control the indices. - * @private - * @param {string[]} props - the properties that should be used to index docs - * @param {function} inflect - a function that will be called with a property name, and - * creates the public property at which the index will be exposed - * @param {function} op - the function that will be used to create the indices, it is passed - * the raw representation of the registry, and a getter for reading the - * right prop - * - * @returns {string[]} - the public keys of all indices created - */ - _setupIndex(props, inflect, op) { - // shortcut for empty props - if (!props || props.length === 0) return; - - return props.map((prop) => { - const indexName = inflect(prop); - const getIndexValueFromItem = pathGetter.partial(prop).value(); - let cache; - - Object.defineProperty(this, indexName, { - enumerable: false, - configurable: false, - - set: (val) => { - // can't set any value other than the CLEAR_CACHE constant - if (val === CLEAR_CACHE) { - cache = false; - } else { - throw new TypeError(indexName + ' can not be set, it is a computed index of values'); - } - }, - get: () => { - if (!cache) { - cache = op(this.raw, getIndexValueFromItem); - } - - return cache; - }, - }); - - return indexName; - }); - } - - /** - * Clear cached index/group/order caches so they will be recreated - * on next access - * @private - * @return {undefined} - */ - _clearIndices() { - this._indexNames.forEach((name) => { - this[name] = CLEAR_CACHE; - }); - } -} - -// using traditional `extends Array` syntax doesn't work with babel -// See https://babeljs.io/docs/usage/caveats/ -Object.setPrototypeOf(IndexedArray.prototype, Array.prototype); - -// Similar to `organizeBy` but returns IndexedArrays instead of normal Arrays. -function organizeByIndexedArray(config) { - return (...args) => { - const grouped = organizeBy(...args); - - return _.reduce( - grouped, - (acc, value, group) => { - acc[group] = new IndexedArray({ - ...config, - initialSet: value, - }); - - return acc; - }, - {} - ); - }; -} diff --git a/src/legacy/ui/public/indexed_array/inflector.js b/src/legacy/ui/public/indexed_array/inflector.js deleted file mode 100644 index e034146f5f62f3..00000000000000 --- a/src/legacy/ui/public/indexed_array/inflector.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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. - */ - -function upFirst(str, total) { - return str.charAt(0).toUpperCase() + (total ? str.substr(1).toLowerCase() : str.substr(1)); -} - -function startsWith(str, test) { - return str.substr(0, test.length).toLowerCase() === test.toLowerCase(); -} - -function endsWith(str, test) { - const tooShort = str.length < test.length; - if (tooShort) return; - - return str.substr(str.length - test.length).toLowerCase() === test.toLowerCase(); -} - -export function inflector(prefix, postfix) { - return function inflect(key) { - let inflected; - - if (key.indexOf('.') !== -1) { - inflected = key - .split('.') - .map(function (step, i) { - return i === 0 ? step : upFirst(step, true); - }) - .join(''); - } else { - inflected = key; - } - - if (prefix && !startsWith(key, prefix)) { - inflected = prefix + upFirst(inflected); - } - - if (postfix && !endsWith(key, postfix)) { - inflected = inflected + postfix; - } - - return inflected; - }; -} diff --git a/src/legacy/ui/public/kfetch/__mocks__/index.ts b/src/legacy/ui/public/kfetch/__mocks__/index.ts deleted file mode 100644 index 1a128e2b85260f..00000000000000 --- a/src/legacy/ui/public/kfetch/__mocks__/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 kfetch = () => Promise.resolve(); diff --git a/src/legacy/ui/public/kfetch/_import_objects.ndjson b/src/legacy/ui/public/kfetch/_import_objects.ndjson deleted file mode 100644 index 3511fb44cdfb24..00000000000000 --- a/src/legacy/ui/public/kfetch/_import_objects.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Log Agents","uiStateJSON":"{}","visState":"{\"title\":\"Log Agents\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{\"text\":\"agent.raw: Descending\"}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"agent.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}"},"id":"082f1d60-a2e7-11e7-bb30-233be9be6a15","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"f1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","version":1} diff --git a/src/legacy/ui/public/kfetch/index.ts b/src/legacy/ui/public/kfetch/index.ts deleted file mode 100644 index 105df171ad3702..00000000000000 --- a/src/legacy/ui/public/kfetch/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { npSetup } from 'ui/new_platform'; -import { createKfetch, KFetchKibanaOptions, KFetchOptions } from './kfetch'; -export { addInterceptor, KFetchOptions, KFetchQuery } from './kfetch'; - -const kfetchInstance = createKfetch(npSetup.core.http); - -export const kfetch = (options: KFetchOptions, kfetchOptions?: KFetchKibanaOptions) => { - return kfetchInstance(options, kfetchOptions); -}; diff --git a/src/legacy/ui/public/kfetch/kfetch.test.mocks.ts b/src/legacy/ui/public/kfetch/kfetch.test.mocks.ts deleted file mode 100644 index ea066b3623f134..00000000000000 --- a/src/legacy/ui/public/kfetch/kfetch.test.mocks.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 { setup } from '../../../../test_utils/public/http_test_setup'; - -jest.doMock('ui/new_platform', () => ({ - npSetup: { - core: setup(), - }, -})); diff --git a/src/legacy/ui/public/kfetch/kfetch.test.ts b/src/legacy/ui/public/kfetch/kfetch.test.ts deleted file mode 100644 index c45a142d54e9b5..00000000000000 --- a/src/legacy/ui/public/kfetch/kfetch.test.ts +++ /dev/null @@ -1,487 +0,0 @@ -/* - * 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. - */ - -// @ts-ignore -import fetchMock from 'fetch-mock/es5/client'; -import './kfetch.test.mocks'; -import { readFileSync } from 'fs'; -import { join } from 'path'; -import { addInterceptor, kfetch, KFetchOptions } from '.'; -import { Interceptor, resetInterceptors, withDefaultOptions } from './kfetch'; -import { KFetchError } from './kfetch_error'; - -describe('kfetch', () => { - afterEach(() => { - fetchMock.restore(); - resetInterceptors(); - }); - - it('should use supplied request method', async () => { - fetchMock.post('*', {}); - await kfetch({ pathname: '/my/path', method: 'POST' }); - expect(fetchMock.lastOptions()!.method).toBe('POST'); - }); - - it('should use supplied Content-Type', async () => { - fetchMock.get('*', {}); - await kfetch({ pathname: '/my/path', headers: { 'Content-Type': 'CustomContentType' } }); - expect(fetchMock.lastOptions()!.headers).toMatchObject({ - 'content-type': 'CustomContentType', - }); - }); - - it('should use supplied pathname and querystring', async () => { - fetchMock.get('*', {}); - await kfetch({ pathname: '/my/path', query: { a: 'b' } }); - expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path?a=b'); - }); - - it('should use supplied headers', async () => { - fetchMock.get('*', {}); - await kfetch({ - pathname: '/my/path', - headers: { myHeader: 'foo' }, - }); - - expect(fetchMock.lastOptions()!.headers).toEqual({ - 'content-type': 'application/json', - 'kbn-version': 'kibanaVersion', - myheader: 'foo', - }); - }); - - it('should return response', async () => { - fetchMock.get('*', { foo: 'bar' }); - const res = await kfetch({ pathname: '/my/path' }); - expect(res).toEqual({ foo: 'bar' }); - }); - - it('should prepend url with basepath by default', async () => { - fetchMock.get('*', {}); - await kfetch({ pathname: '/my/path' }); - expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path'); - }); - - it('should not prepend url with basepath when disabled', async () => { - fetchMock.get('*', {}); - await kfetch({ pathname: '/my/path' }, { prependBasePath: false }); - expect(fetchMock.lastUrl()).toBe('/my/path'); - }); - - it('should make request with defaults', async () => { - fetchMock.get('*', {}); - await kfetch({ pathname: '/my/path' }); - - expect(fetchMock.lastCall()!.request.credentials).toBe('same-origin'); - expect(fetchMock.lastOptions()!).toMatchObject({ - method: 'GET', - headers: { - 'content-type': 'application/json', - 'kbn-version': 'kibanaVersion', - }, - }); - }); - - it('should make requests for NDJSON content', async () => { - const content = readFileSync(join(__dirname, '_import_objects.ndjson'), { encoding: 'utf-8' }); - - fetchMock.post('*', { - body: content, - headers: { 'Content-Type': 'application/ndjson' }, - }); - - const data = await kfetch({ - method: 'POST', - pathname: '/my/path', - body: content, - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); - - expect(data).toBeInstanceOf(Blob); - - const ndjson = await new Response(data).text(); - - expect(ndjson).toEqual(content); - }); - - it('should reject on network error', async () => { - expect.assertions(1); - fetchMock.get('*', { status: 500 }); - - try { - await kfetch({ pathname: '/my/path' }); - } catch (e) { - expect(e.message).toBe('Internal Server Error'); - } - }); - - describe('when throwing response error (KFetchError)', () => { - let error: KFetchError; - beforeEach(async () => { - fetchMock.get('*', { status: 404, body: { foo: 'bar' } }); - try { - await kfetch({ pathname: '/my/path' }); - } catch (e) { - error = e; - } - }); - - it('should contain error message', () => { - expect(error.message).toBe('Not Found'); - }); - - it('should return response body', () => { - expect(error.body).toEqual({ foo: 'bar' }); - }); - - it('should contain response properties', () => { - expect(error.res.status).toBe(404); - expect(error.res.url).toBe('http://localhost/myBase/my/path'); - }); - }); - - describe('when all interceptor resolves', () => { - let resp: any; - let interceptorCalls: string[]; - - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - - interceptorCalls = mockInterceptorCalls([{}, {}, {}]); - resp = await kfetch({ pathname: '/my/path' }); - }); - - it('should call interceptors in correct order', () => { - expect(interceptorCalls).toEqual([ - 'Request #3', - 'Request #2', - 'Request #1', - 'Response #1', - 'Response #2', - 'Response #3', - ]); - }); - - it('should make request', () => { - expect(fetchMock.called()).toBe(true); - }); - - it('should return response', () => { - expect(resp).toEqual({ foo: 'bar' }); - }); - }); - - describe('when a request interceptor throws; and the next requestError interceptor resolves', () => { - let resp: any; - let interceptorCalls: string[]; - - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - - interceptorCalls = mockInterceptorCalls([ - { requestError: () => ({ pathname: '/my/path' } as KFetchOptions) }, - { request: () => Promise.reject(new Error('Error in request')) }, - {}, - ]); - - resp = await kfetch({ pathname: '/my/path' }); - }); - - it('should call interceptors in correct order', () => { - expect(interceptorCalls).toEqual([ - 'Request #3', - 'Request #2', - 'RequestError #1', - 'Response #1', - 'Response #2', - 'Response #3', - ]); - }); - - it('should make request', () => { - expect(fetchMock.called()).toBe(true); - }); - - it('should return response', () => { - expect(resp).toEqual({ foo: 'bar' }); - }); - }); - - describe('when a request interceptor throws', () => { - let error: Error; - let interceptorCalls: string[]; - - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - - interceptorCalls = mockInterceptorCalls([ - {}, - { request: () => Promise.reject(new Error('Error in request')) }, - {}, - ]); - - try { - await kfetch({ pathname: '/my/path' }); - } catch (e) { - error = e; - } - }); - - it('should call interceptors in correct order', () => { - expect(interceptorCalls).toEqual([ - 'Request #3', - 'Request #2', - 'RequestError #1', - 'ResponseError #1', - 'ResponseError #2', - 'ResponseError #3', - ]); - }); - - it('should not make request', () => { - expect(fetchMock.called()).toBe(false); - }); - - it('should throw error', () => { - expect(error.message).toEqual('Error in request'); - }); - }); - - describe('when a response interceptor throws', () => { - let error: Error; - let interceptorCalls: string[]; - - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - - interceptorCalls = mockInterceptorCalls([ - { response: () => Promise.reject(new Error('Error in response')) }, - {}, - {}, - ]); - - try { - await kfetch({ pathname: '/my/path' }); - } catch (e) { - error = e; - } - }); - - it('should call in correct order', () => { - expect(interceptorCalls).toEqual([ - 'Request #3', - 'Request #2', - 'Request #1', - 'Response #1', - 'ResponseError #2', - 'ResponseError #3', - ]); - }); - - it('should make request', () => { - expect(fetchMock.called()).toBe(true); - }); - - it('should throw error', () => { - expect(error.message).toEqual('Error in response'); - }); - }); - - describe('when request interceptor throws; and a responseError interceptor resolves', () => { - let resp: any; - let interceptorCalls: string[]; - - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - - interceptorCalls = mockInterceptorCalls([ - {}, - { - request: () => { - throw new Error('My request error'); - }, - responseError: () => { - return { custom: 'response' }; - }, - }, - {}, - ]); - - resp = await kfetch({ pathname: '/my/path' }); - }); - - it('should call in correct order', () => { - expect(interceptorCalls).toEqual([ - 'Request #3', - 'Request #2', - 'RequestError #1', - 'ResponseError #1', - 'ResponseError #2', - 'Response #3', - ]); - }); - - it('should not make request', () => { - expect(fetchMock.called()).toBe(false); - }); - - it('should resolve', () => { - expect(resp).toEqual({ custom: 'response' }); - }); - }); - - describe('when interceptors return synchronously', () => { - let resp: any; - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - addInterceptor({ - request: (config) => ({ - ...config, - pathname: '/my/intercepted-route', - }), - response: (res) => ({ - ...res, - addedByResponseInterceptor: true, - }), - }); - - resp = await kfetch({ pathname: '/my/path' }); - }); - - it('should modify request', () => { - expect(fetchMock.lastUrl()).toContain('/my/intercepted-route'); - expect(fetchMock.lastOptions()!).toMatchObject({ - method: 'GET', - }); - }); - - it('should modify response', () => { - expect(resp).toEqual({ - addedByResponseInterceptor: true, - foo: 'bar', - }); - }); - }); - - describe('when interceptors return promise', () => { - let resp: any; - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - addInterceptor({ - request: (config) => - Promise.resolve({ - ...config, - pathname: '/my/intercepted-route', - }), - response: (res) => - Promise.resolve({ - ...res, - addedByResponseInterceptor: true, - }), - }); - - resp = await kfetch({ pathname: '/my/path' }); - }); - - it('should modify request', () => { - expect(fetchMock.lastUrl()).toContain('/my/intercepted-route'); - expect(fetchMock.lastOptions()!).toMatchObject({ - method: 'GET', - }); - }); - - it('should modify response', () => { - expect(resp).toEqual({ - addedByResponseInterceptor: true, - foo: 'bar', - }); - }); - }); -}); - -function mockInterceptorCalls(interceptors: Interceptor[]) { - const interceptorCalls: string[] = []; - interceptors.forEach((interceptor, i) => { - addInterceptor({ - request: (config) => { - interceptorCalls.push(`Request #${i + 1}`); - - if (interceptor.request) { - return interceptor.request(config); - } - - return config; - }, - requestError: (e) => { - interceptorCalls.push(`RequestError #${i + 1}`); - if (interceptor.requestError) { - return interceptor.requestError(e); - } - - throw e; - }, - response: (res) => { - interceptorCalls.push(`Response #${i + 1}`); - - if (interceptor.response) { - return interceptor.response(res); - } - - return res; - }, - responseError: (e) => { - interceptorCalls.push(`ResponseError #${i + 1}`); - - if (interceptor.responseError) { - return interceptor.responseError(e); - } - - throw e; - }, - }); - }); - - return interceptorCalls; -} - -describe('withDefaultOptions', () => { - it('should remove undefined query params', () => { - const { query } = withDefaultOptions({ - pathname: '/withDefaultOptions', - query: { - foo: 'bar', - param1: (undefined as any) as string, - param2: (null as any) as string, - param3: '', - }, - }); - expect(query).toEqual({ foo: 'bar', param2: null, param3: '' }); - }); - - it('should add default options', () => { - expect(withDefaultOptions({ pathname: '/addDefaultOptions' })).toEqual({ - pathname: '/addDefaultOptions', - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json' }, - method: 'GET', - }); - }); -}); diff --git a/src/legacy/ui/public/kfetch/kfetch.ts b/src/legacy/ui/public/kfetch/kfetch.ts deleted file mode 100644 index 4eb71499315757..00000000000000 --- a/src/legacy/ui/public/kfetch/kfetch.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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 { merge } from 'lodash'; -// @ts-ignore not really worth typing -import { KFetchError } from './kfetch_error'; - -import { HttpSetup } from '../../../../core/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { HttpRequestInit } from '../../../../core/public/http/types'; - -export interface KFetchQuery { - [key: string]: string | number | boolean | undefined; -} - -export interface KFetchOptions extends HttpRequestInit { - pathname: string; - query?: KFetchQuery; - asSystemRequest?: boolean; -} - -export interface KFetchKibanaOptions { - prependBasePath?: boolean; -} - -export interface Interceptor { - request?: (config: KFetchOptions) => Promise | KFetchOptions; - requestError?: (e: any) => Promise | KFetchOptions; - response?: (res: any) => any; - responseError?: (e: any) => any; -} - -const interceptors: Interceptor[] = []; -export const resetInterceptors = () => (interceptors.length = 0); -export const addInterceptor = (interceptor: Interceptor) => interceptors.push(interceptor); - -export function createKfetch(http: HttpSetup) { - return function kfetch( - options: KFetchOptions, - { prependBasePath = true }: KFetchKibanaOptions = {} - ) { - return responseInterceptors( - requestInterceptors(withDefaultOptions(options)) - .then(({ pathname, ...restOptions }) => - http.fetch(pathname, { ...restOptions, prependBasePath }) - ) - .catch((err) => { - throw new KFetchError(err.response || { statusText: err.message }, err.body); - }) - ); - }; -} - -// Request/response interceptors are called in opposite orders. -// Request hooks start from the newest interceptor and end with the oldest. -function requestInterceptors(config: KFetchOptions): Promise { - return interceptors.reduceRight((acc, interceptor) => { - return acc.then(interceptor.request, interceptor.requestError); - }, Promise.resolve(config)); -} - -// Response hooks start from the oldest interceptor and end with the newest. -function responseInterceptors(responsePromise: Promise) { - return interceptors.reduce((acc, interceptor) => { - return acc.then(interceptor.response, interceptor.responseError); - }, responsePromise); -} - -export function withDefaultOptions(options?: KFetchOptions): KFetchOptions { - const withDefaults = merge( - { - method: 'GET', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - }, - }, - options - ) as KFetchOptions; - - if ( - options && - options.headers && - 'Content-Type' in options.headers && - options.headers['Content-Type'] === undefined - ) { - // TS thinks headers could be undefined here, but that isn't possible because - // of the merge above. - // @ts-ignore - withDefaults.headers['Content-Type'] = undefined; - } - - return withDefaults; -} diff --git a/src/legacy/ui/public/kfetch/kfetch_error.ts b/src/legacy/ui/public/kfetch/kfetch_error.ts deleted file mode 100644 index f351959e624b85..00000000000000 --- a/src/legacy/ui/public/kfetch/kfetch_error.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 class KFetchError extends Error { - constructor(public readonly res: Response, public readonly body?: any) { - super(res.statusText); - - // captureStackTrace is only available in the V8 engine, so any browser using - // a different JS engine won't have access to this method. - if (Error.captureStackTrace) { - Error.captureStackTrace(this, KFetchError); - } - } -} diff --git a/src/legacy/ui/public/legacy_compat/__tests__/xsrf.js b/src/legacy/ui/public/legacy_compat/__tests__/xsrf.js deleted file mode 100644 index efcfb779972655..00000000000000 --- a/src/legacy/ui/public/legacy_compat/__tests__/xsrf.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * 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 $ from 'jquery'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import ngMock from 'ng_mock'; - -import { $setupXsrfRequestInterceptor } from '../../../../../plugins/kibana_legacy/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { version } from '../../../../../core/server/utils/package_json'; - -const xsrfHeader = 'kbn-version'; - -describe('chrome xsrf apis', function () { - const sandbox = sinon.createSandbox(); - - afterEach(function () { - sandbox.restore(); - }); - - describe('jQuery support', function () { - it('adds a global jQuery prefilter', function () { - sandbox.stub($, 'ajaxPrefilter'); - $setupXsrfRequestInterceptor(version); - expect($.ajaxPrefilter.callCount).to.be(1); - }); - - describe('jQuery prefilter', function () { - let prefilter; - - beforeEach(function () { - sandbox.stub($, 'ajaxPrefilter'); - $setupXsrfRequestInterceptor(version); - prefilter = $.ajaxPrefilter.args[0][0]; - }); - - it(`sets the ${xsrfHeader} header`, function () { - const setHeader = sinon.stub(); - prefilter({}, {}, { setRequestHeader: setHeader }); - - expect(setHeader.callCount).to.be(1); - expect(setHeader.args[0]).to.eql([xsrfHeader, version]); - }); - - it('can be canceled by setting the kbnXsrfToken option', function () { - const setHeader = sinon.stub(); - prefilter({ kbnXsrfToken: false }, {}, { setRequestHeader: setHeader }); - expect(setHeader.callCount).to.be(0); - }); - }); - - describe('Angular support', function () { - let $http; - let $httpBackend; - - beforeEach(function () { - sandbox.stub($, 'ajaxPrefilter'); - ngMock.module($setupXsrfRequestInterceptor(version)); - }); - - beforeEach( - ngMock.inject(function ($injector) { - $http = $injector.get('$http'); - $httpBackend = $injector.get('$httpBackend'); - - $httpBackend.when('POST', '/api/test').respond('ok'); - }) - ); - - afterEach(function () { - $httpBackend.verifyNoOutstandingExpectation(); - $httpBackend.verifyNoOutstandingRequest(); - }); - - it(`injects a ${xsrfHeader} header on every request`, function () { - $httpBackend - .expectPOST('/api/test', undefined, function (headers) { - return headers[xsrfHeader] === version; - }) - .respond(200, ''); - - $http.post('/api/test'); - $httpBackend.flush(); - }); - - it('skips requests with the kbnXsrfToken set falsy', function () { - $httpBackend - .expectPOST('/api/test', undefined, function (headers) { - return !(xsrfHeader in headers); - }) - .respond(200, ''); - - $http({ - method: 'POST', - url: '/api/test', - kbnXsrfToken: 0, - }); - - $http({ - method: 'POST', - url: '/api/test', - kbnXsrfToken: '', - }); - - $http({ - method: 'POST', - url: '/api/test', - kbnXsrfToken: false, - }); - - $httpBackend.flush(); - }); - - it('treats the kbnXsrfToken option as boolean-y', function () { - const customToken = `custom:${version}`; - $httpBackend - .expectPOST('/api/test', undefined, function (headers) { - return headers[xsrfHeader] === version; - }) - .respond(200, ''); - - $http({ - method: 'POST', - url: '/api/test', - kbnXsrfToken: customToken, - }); - - $httpBackend.flush(); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/legacy_compat/index.ts b/src/legacy/ui/public/legacy_compat/index.ts deleted file mode 100644 index 2067fa6489304f..00000000000000 --- a/src/legacy/ui/public/legacy_compat/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 { configureAppAngularModule } from '../../../../plugins/kibana_legacy/public'; diff --git a/src/legacy/ui/public/metadata.ts b/src/legacy/ui/public/metadata.ts deleted file mode 100644 index fade0f0d8629a0..00000000000000 --- a/src/legacy/ui/public/metadata.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 { npSetup } from 'ui/new_platform'; - -export const metadata: { - branch: string; - version: string; -} = npSetup.core.injectedMetadata.getLegacyMetadata(); diff --git a/src/legacy/ui/public/modules.js b/src/legacy/ui/public/modules.js deleted file mode 100644 index bb1c8aead1c34e..00000000000000 --- a/src/legacy/ui/public/modules.js +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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 angular from 'angular'; -import _ from 'lodash'; -/** - * This module is used by Kibana to create and reuse angular modules. Angular modules - * can only be created once and need to have their dependencies at creation. This is - * hard/impossible to do in require.js since all of the dependencies for a module are - * loaded before it is. - * - * Here is an example: - * - * In the scenario below, require.js would load directive.js first because it is a - * dependency of app.js. This would cause the call to `angular.module('app')` to - * execute before the module is actually created. This causes angular to throw an - * error. This effect is magnified when app.js links off to many different modules. - * - * This is normally solved by creating unique modules per file, listed as the 1st - * alternate solution below. Unfortunately this solution would have required that - * we replicate our require statements. - * - * app.js - * ``` - * angular.module('app', ['ui.bootstrap']) - * .controller('AppController', function () { ... }); - * - * require('./directive'); - * ``` - * - * directive.js - * ``` - * angular.module('app') - * .directive('someDirective', function () { ... }); - * ``` - * - * Before taking this approach we saw three possible solutions: - * 1. replicate our js modules in angular modules/use a different module per file - * 2. create a single module outside of our js modules and share it - * 3. use a helper lib to dynamically create modules as needed. - * - * We decided to go with #3 - * - * This ends up working by creating a list of modules that the code base creates by - * calling `modules.get(name)` with different names, and then before bootstrapping - * the application kibana uses `modules.link()` to set the dependencies of the "kibana" - * module to include every defined module. This guarantees that kibana can always find - * any angular dependency defined in the kibana code base. This **also** means that - * Private modules are able to find any dependency, since they are injected using the - * "kibana" module's injector. - * - */ -const existingModules = {}; -const links = []; - -/** - * Take an angular module and extends the dependencies for that module to include all of the modules - * created using `ui/modules` - * - * @param {AngularModule} module - the module to extend - * @return {undefined} - */ -export function link(module) { - // as modules are defined they will be set as requirements for this app - links.push(module); - - // merge in the existing modules - module.requires = _.union(module.requires, _.keys(existingModules)); -} - -/** - * The primary means of interacting with `ui/modules`. Returns an angular module. If the module already - * exists the existing version will be returned. `dependencies` are either set as or merged into the - * modules total dependencies. - * - * This is in contrast to the `angular.module(name, [dependencies])` function which will only - * create a module if the `dependencies` list is passed and get an existing module if no dependencies - * are passed. This requires knowing the order that your files will load, which we can't guarantee. - * - * @param {string} moduleName - the unique name for this module - * @param {array[string]} [requires=[]] - the other modules this module requires - * @return {AngularModule} - */ -export function get(moduleName, requires) { - let module = existingModules[moduleName]; - - if (module === void 0) { - // create the module - module = existingModules[moduleName] = angular.module(moduleName, []); - - module.close = _.partial(close, moduleName); - - // ensure that it is required by linked modules - _.each(links, function (app) { - if (!~app.requires.indexOf(moduleName)) app.requires.push(moduleName); - }); - } - - if (requires) { - // update requires list with possibly new requirements - module.requires = _.union(module.requires, requires); - } - - return module; -} - -export function close(moduleName) { - const module = existingModules[moduleName]; - - // already closed - if (!module) return; - - // if the module is currently linked, unlink it - const i = links.indexOf(module); - if (i > -1) links.splice(i, 1); - - // remove from linked modules list of required modules - _.each(links, function (app) { - _.pull(app.requires, moduleName); - }); - - // remove module from existingModules - delete existingModules[moduleName]; -} - -export const uiModules = { link, get, close }; diff --git a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts b/src/legacy/ui/public/new_platform/__mocks__/helpers.ts deleted file mode 100644 index 35aa8e48304289..00000000000000 --- a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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 { coreMock } from '../../../../../core/public/mocks'; -import { dataPluginMock } from '../../../../../plugins/data/public/mocks'; -import { embeddablePluginMock } from '../../../../../plugins/embeddable/public/mocks'; -import { navigationPluginMock } from '../../../../../plugins/navigation/public/mocks'; -import { expressionsPluginMock } from '../../../../../plugins/expressions/public/mocks'; -import { inspectorPluginMock } from '../../../../../plugins/inspector/public/mocks'; -import { uiActionsPluginMock } from '../../../../../plugins/ui_actions/public/mocks'; -import { managementPluginMock } from '../../../../../plugins/management/public/mocks'; -import { usageCollectionPluginMock } from '../../../../../plugins/usage_collection/public/mocks'; -import { kibanaLegacyPluginMock } from '../../../../../plugins/kibana_legacy/public/mocks'; -import { chartPluginMock } from '../../../../../plugins/charts/public/mocks'; -import { advancedSettingsMock } from '../../../../../plugins/advanced_settings/public/mocks'; -import { savedObjectsManagementPluginMock } from '../../../../../plugins/saved_objects_management/public/mocks'; -import { visualizationsPluginMock } from '../../../../../plugins/visualizations/public/mocks'; -import { discoverPluginMock } from '../../../../../plugins/discover/public/mocks'; - -export const pluginsMock = { - createSetup: () => ({ - data: dataPluginMock.createSetupContract(), - charts: chartPluginMock.createSetupContract(), - navigation: navigationPluginMock.createSetupContract(), - embeddable: embeddablePluginMock.createSetupContract(), - inspector: inspectorPluginMock.createSetupContract(), - expressions: expressionsPluginMock.createSetupContract(), - uiActions: uiActionsPluginMock.createSetupContract(), - usageCollection: usageCollectionPluginMock.createSetupContract(), - advancedSettings: advancedSettingsMock.createSetupContract(), - visualizations: visualizationsPluginMock.createSetupContract(), - kibanaLegacy: kibanaLegacyPluginMock.createSetupContract(), - savedObjectsManagement: savedObjectsManagementPluginMock.createSetupContract(), - discover: discoverPluginMock.createSetupContract(), - }), - createStart: () => ({ - data: dataPluginMock.createStartContract(), - charts: chartPluginMock.createStartContract(), - navigation: navigationPluginMock.createStartContract(), - embeddable: embeddablePluginMock.createStartContract(), - inspector: inspectorPluginMock.createStartContract(), - expressions: expressionsPluginMock.createStartContract(), - uiActions: uiActionsPluginMock.createStartContract(), - management: managementPluginMock.createStartContract(), - advancedSettings: advancedSettingsMock.createStartContract(), - visualizations: visualizationsPluginMock.createStartContract(), - kibanaLegacy: kibanaLegacyPluginMock.createStartContract(), - savedObjectsManagement: savedObjectsManagementPluginMock.createStartContract(), - discover: discoverPluginMock.createStartContract(), - }), -}; - -export const createUiNewPlatformMock = () => { - const mock = { - npSetup: { - core: coreMock.createSetup(), - plugins: pluginsMock.createSetup(), - }, - npStart: { - core: coreMock.createStart(), - plugins: pluginsMock.createStart(), - }, - }; - return mock; -}; diff --git a/src/legacy/ui/public/new_platform/__mocks__/index.ts b/src/legacy/ui/public/new_platform/__mocks__/index.ts deleted file mode 100644 index d469960ddd7b7a..00000000000000 --- a/src/legacy/ui/public/new_platform/__mocks__/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 { createUiNewPlatformMock } from './helpers'; - -const { npSetup, npStart } = createUiNewPlatformMock(); -export { npSetup, npStart }; diff --git a/src/legacy/ui/public/new_platform/index.ts b/src/legacy/ui/public/new_platform/index.ts deleted file mode 100644 index ca5890854f3aa1..00000000000000 --- a/src/legacy/ui/public/new_platform/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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 { __setup__, __start__, npSetup, npStart } from './new_platform'; 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 deleted file mode 100644 index b8d48b784dba71..00000000000000 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ /dev/null @@ -1,550 +0,0 @@ -/* - * 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 sinon from 'sinon'; -import { getFieldFormatsRegistry } from '../../../../test_utils/public/stub_field_formats'; -import { METRIC_TYPE } from '@kbn/analytics'; -import { setSetupServices, setStartServices } from './set_services'; -import { - AggTypesRegistry, - getAggTypes, - AggConfigs, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../src/plugins/data/common/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 { - subscribe: () => {}, - pipe: () => { - return { - subscribe: () => {}, - }; - }, - }; -}; - -const mockComponent = () => { - return null; -}; - -let refreshInterval = undefined; -let isTimeRangeSelectorEnabled = true; -let isAutoRefreshSelectorEnabled = true; - -export const mockUiSettings = { - 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(), -}; - -const mockCoreSetup = { - chrome: {}, - http: { - basePath: { - get: sinon.fake.returns(''), - }, - }, - injectedMetadata: {}, - uiSettings: mockUiSettings, -}; - -const mockCoreStart = { - application: { - capabilities: {}, - }, - chrome: { - overlays: { - openModal: sinon.fake(), - }, - }, - http: { - basePath: { - get: sinon.fake.returns(''), - }, - }, - notifications: { - toasts: {}, - }, - i18n: {}, - overlays: {}, - savedObjects: { - client: {}, - }, - uiSettings: mockUiSettings, -}; - -const querySetup = { - state$: mockObservable(), - filterManager: { - getFetches$: sinon.fake(), - getFilters: sinon.fake(), - getAppFilters: sinon.fake(), - getGlobalFilters: sinon.fake(), - removeFilter: sinon.fake(), - addFilters: sinon.fake(), - setFilters: sinon.fake(), - removeAll: sinon.fake(), - getUpdates$: mockObservable, - }, - timefilter: { - timefilter: { - getFetch$: mockObservable, - getAutoRefreshFetch$: mockObservable, - getEnabledUpdated$: mockObservable, - getTimeUpdate$: mockObservable, - getRefreshIntervalUpdate$: mockObservable, - isTimeRangeSelectorEnabled: () => { - return isTimeRangeSelectorEnabled; - }, - isAutoRefreshSelectorEnabled: () => { - return isAutoRefreshSelectorEnabled; - }, - disableAutoRefreshSelector: () => { - isAutoRefreshSelectorEnabled = false; - }, - enableAutoRefreshSelector: () => { - isAutoRefreshSelectorEnabled = true; - }, - getRefreshInterval: () => { - return refreshInterval; - }, - setRefreshInterval: (interval) => { - refreshInterval = interval; - }, - enableTimeRangeSelector: () => { - isTimeRangeSelectorEnabled = true; - }, - disableTimeRangeSelector: () => { - isTimeRangeSelectorEnabled = false; - }, - getTime: sinon.fake(), - setTime: sinon.fake(), - getActiveBounds: sinon.fake(), - getBounds: sinon.fake(), - calculateBounds: sinon.fake(), - createFilter: sinon.fake(), - }, - history: sinon.fake(), - }, - savedQueries: { - saveQuery: sinon.fake(), - getAllSavedQueries: sinon.fake(), - findSavedQueries: sinon.fake(), - getSavedQuery: sinon.fake(), - deleteSavedQuery: sinon.fake(), - getSavedQueryCount: sinon.fake(), - }, -}; - -const mockAggTypesRegistry = () => { - const registry = new AggTypesRegistry(); - const registrySetup = registry.setup(); - const aggTypes = getAggTypes({ - calculateBounds: sinon.fake(), - getConfig: sinon.fake(), - getFieldFormatsStart: () => ({ - deserialize: sinon.fake(), - getDefaultInstance: sinon.fake(), - }), - isDefaultTimezone: () => true, - }); - aggTypes.buckets.forEach((type) => registrySetup.registerBucket(type)); - aggTypes.metrics.forEach((type) => registrySetup.registerMetric(type)); - - return registry; -}; - -const aggTypesRegistry = mockAggTypesRegistry(); - -export const npSetup = { - core: mockCoreSetup, - plugins: { - advancedSettings: { - component: { - register: sinon.fake(), - componentType: ComponentRegistry.componentType, - }, - }, - usageCollection: { - allowTrackUserAgent: sinon.fake(), - reportUiStats: sinon.fake(), - METRIC_TYPE, - }, - embeddable: { - registerEmbeddableFactory: sinon.fake(), - }, - expressions: { - registerFunction: sinon.fake(), - registerRenderer: sinon.fake(), - registerType: sinon.fake(), - }, - data: { - autocomplete: { - addProvider: sinon.fake(), - getProvider: sinon.fake(), - }, - query: querySetup, - search: { - aggs: { - types: aggTypesRegistry.setup(), - }, - __LEGACY: { - esClient: { - search: sinon.fake(), - msearch: sinon.fake(), - }, - }, - }, - fieldFormats: getFieldFormatsRegistry(mockCoreSetup), - }, - share: { - register: () => {}, - urlGenerators: { - registerUrlGenerator: () => {}, - }, - }, - devTools: { - register: () => {}, - }, - kibanaLegacy: { - registerLegacyApp: () => {}, - forwardApp: () => {}, - config: { - defaultAppId: 'home', - }, - }, - inspector: { - registerView: () => undefined, - __LEGACY: { - views: { - register: () => undefined, - }, - }, - }, - uiActions: { - attachAction: sinon.fake(), - registerAction: sinon.fake(), - registerTrigger: sinon.fake(), - }, - home: { - featureCatalogue: { - register: sinon.fake(), - }, - environment: { - update: sinon.fake(), - }, - config: { - disableWelcomeScreen: false, - }, - tutorials: { - setVariable: sinon.fake(), - }, - }, - charts: { - theme: { - chartsTheme$: mockObservable, - useChartsTheme: sinon.fake(), - }, - colors: { - seedColors: ['white', 'black'], - }, - }, - management: { - sections: { - getSection: () => ({ - registerApp: sinon.fake(), - }), - }, - }, - indexPatternManagement: { - list: { addListConfig: sinon.fake() }, - creation: { addCreationConfig: sinon.fake() }, - }, - discover: { - docViews: { - addDocView: sinon.fake(), - setAngularInjectorGetter: sinon.fake(), - }, - }, - visTypeVega: { - config: sinon.fake(), - }, - visualizations: { - createBaseVisualization: sinon.fake(), - createReactVisualization: sinon.fake(), - registerAlias: sinon.fake(), - hideTypes: sinon.fake(), - }, - - mapsLegacy: { - serviceSettings: sinon.fake(), - getPrecision: sinon.fake(), - getZoomPrecision: sinon.fake(), - }, - }, -}; - -export const npStart = { - core: mockCoreStart, - plugins: { - management: { - legacy: { - getSection: () => ({ - register: sinon.fake(), - deregister: sinon.fake(), - hasItem: sinon.fake(), - }), - }, - sections: { - getSection: () => ({ - registerApp: sinon.fake(), - }), - }, - }, - indexPatternManagement: { - list: { - getType: sinon.fake(), - getIndexPatternCreationOptions: sinon.fake(), - }, - creation: { - getIndexPatternTags: sinon.fake(), - getFieldInfo: sinon.fake(), - areScriptedFieldsEnabled: sinon.fake(), - }, - }, - embeddable: { - getEmbeddableFactory: sinon.fake(), - getEmbeddableFactories: sinon.fake(), - registerEmbeddableFactory: sinon.fake(), - }, - expressions: { - registerFunction: sinon.fake(), - registerRenderer: sinon.fake(), - registerType: sinon.fake(), - }, - kibanaLegacy: { - getForwards: () => [], - loadFontAwesome: () => {}, - config: { - defaultAppId: 'home', - }, - dashboardConfig: { - turnHideWriteControlsOn: sinon.fake(), - getHideWriteControls: sinon.fake(), - }, - }, - dashboard: { - getSavedDashboardLoader: sinon.fake(), - }, - data: { - actions: { - createFiltersFromValueClickAction: Promise.resolve(['yes']), - createFiltersFromRangeSelectAction: sinon.fake(), - }, - autocomplete: { - getProvider: sinon.fake(), - }, - getSuggestions: sinon.fake(), - indexPatterns: { - get: sinon.spy((indexPatternId) => - Promise.resolve({ - id: indexPatternId, - isTimeNanosBased: () => false, - popularizeField: () => {}, - }) - ), - }, - ui: { - IndexPatternSelect: mockComponent, - SearchBar: mockComponent, - }, - query: { - filterManager: { - getFetches$: sinon.fake(), - getFilters: sinon.fake(), - getAppFilters: sinon.fake(), - getGlobalFilters: sinon.fake(), - removeFilter: sinon.fake(), - addFilters: sinon.fake(), - setFilters: sinon.fake(), - removeAll: sinon.fake(), - getUpdates$: mockObservable, - }, - timefilter: { - timefilter: { - getFetch$: mockObservable, - getAutoRefreshFetch$: mockObservable, - getEnabledUpdated$: mockObservable, - getTimeUpdate$: mockObservable, - getRefreshIntervalUpdate$: mockObservable, - isTimeRangeSelectorEnabled: () => { - return isTimeRangeSelectorEnabled; - }, - isAutoRefreshSelectorEnabled: () => { - return isAutoRefreshSelectorEnabled; - }, - disableAutoRefreshSelector: () => { - isAutoRefreshSelectorEnabled = false; - }, - enableAutoRefreshSelector: () => { - isAutoRefreshSelectorEnabled = true; - }, - getRefreshInterval: () => { - return refreshInterval; - }, - setRefreshInterval: (interval) => { - refreshInterval = interval; - }, - enableTimeRangeSelector: () => { - isTimeRangeSelectorEnabled = true; - }, - disableTimeRangeSelector: () => { - isTimeRangeSelectorEnabled = false; - }, - getTime: sinon.fake(), - setTime: sinon.fake(), - getActiveBounds: sinon.fake(), - getBounds: sinon.fake(), - calculateBounds: sinon.fake(), - createFilter: sinon.fake(), - }, - history: sinon.fake(), - }, - }, - search: { - aggs: { - calculateAutoTimeExpression: sinon.fake(), - createAggConfigs: (indexPattern, configStates = []) => { - return new AggConfigs(indexPattern, configStates, { - typesRegistry: aggTypesRegistry.start(), - fieldFormats: getFieldFormatsRegistry(mockCoreStart), - }); - }, - types: aggTypesRegistry.start(), - }, - __LEGACY: { - esClient: { - search: sinon.fake(), - msearch: sinon.fake(), - }, - }, - }, - fieldFormats: getFieldFormatsRegistry(mockCoreStart), - }, - share: { - toggleShareContextMenu: () => {}, - }, - inspector: { - isAvailable: () => false, - open: () => ({ - onClose: Promise.resolve(undefined), - close: () => Promise.resolve(undefined), - }), - }, - uiActions: { - attachAction: sinon.fake(), - registerAction: sinon.fake(), - registerTrigger: sinon.fake(), - detachAction: sinon.fake(), - executeTriggerActions: sinon.fake(), - getTrigger: sinon.fake(), - getTriggerActions: sinon.fake(), - getTriggerCompatibleActions: sinon.fake(), - }, - visualizations: { - get: sinon.fake(), - all: sinon.fake(), - getAliases: sinon.fake(), - savedVisualizationsLoader: {}, - showNewVisModal: sinon.fake(), - createVis: sinon.fake(), - convertFromSerializedVis: sinon.fake(), - convertToSerializedVis: sinon.fake(), - }, - navigation: { - ui: { - TopNavMenu: mockComponent, - }, - }, - charts: { - theme: { - chartsTheme$: mockObservable, - useChartsTheme: sinon.fake(), - }, - }, - discover: { - docViews: { - DocViewer: () => null, - }, - savedSearchLoader: {}, - }, - }, -}; - -export function __setup__(coreSetup) { - npSetup.core = coreSetup; - - // no-op application register calls (this is overwritten to - // 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); -} - -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/new_platform.test.mocks.ts b/src/legacy/ui/public/new_platform/new_platform.test.mocks.ts deleted file mode 100644 index f44efe17ef8eee..00000000000000 --- a/src/legacy/ui/public/new_platform/new_platform.test.mocks.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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 { scopedHistoryMock } from '../../../../core/public/mocks'; - -export const setRootControllerMock = jest.fn(); - -jest.doMock('ui/chrome', () => ({ - setRootController: setRootControllerMock, -})); - -export const historyMock = scopedHistoryMock.create(); -jest.doMock('../../../../core/public', () => ({ - ScopedHistory: jest.fn(() => historyMock), -})); diff --git a/src/legacy/ui/public/new_platform/new_platform.test.ts b/src/legacy/ui/public/new_platform/new_platform.test.ts deleted file mode 100644 index d515c348ca440b..00000000000000 --- a/src/legacy/ui/public/new_platform/new_platform.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* - * 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. - */ - -jest.mock('history'); - -import { setRootControllerMock, historyMock } from './new_platform.test.mocks'; -import { legacyAppRegister, __reset__, __setup__, __start__ } from './new_platform'; -import { coreMock } from '../../../../core/public/mocks'; -import { AppMount } from '../../../../core/public'; - -describe('ui/new_platform', () => { - describe('legacyAppRegister', () => { - beforeEach(() => { - setRootControllerMock.mockReset(); - __reset__(); - __setup__(coreMock.createSetup({ basePath: '/test/base/path' }) as any, {} as any); - }); - - const registerApp = () => { - const unmountMock = jest.fn(); - const mountMock = jest.fn, Parameters>(() => unmountMock); - legacyAppRegister({ - id: 'test', - title: 'Test', - mount: mountMock, - }); - return { mountMock, unmountMock }; - }; - - test('sets ui/chrome root controller', () => { - registerApp(); - expect(setRootControllerMock).toHaveBeenCalledWith('test', expect.any(Function)); - }); - - test('throws if called more than once', () => { - registerApp(); - expect(registerApp).toThrowErrorMatchingInlineSnapshot( - `"core.application.register may only be called once for legacy plugins."` - ); - }); - - test('controller calls app.mount when invoked', () => { - const { mountMock } = registerApp(); - const controller = setRootControllerMock.mock.calls[0][1]; - const scopeMock = { $on: jest.fn() }; - const elementMock = [document.createElement('div')]; - - controller(scopeMock, elementMock); - expect(mountMock).toHaveBeenCalledWith({ - element: expect.any(HTMLElement), - appBasePath: '/test/base/path/app/test', - onAppLeave: expect.any(Function), - history: historyMock, - }); - }); - - test('app is mounted in new div inside containing element', () => { - const { mountMock } = registerApp(); - const controller = setRootControllerMock.mock.calls[0][1]; - const scopeMock = { $on: jest.fn() }; - const elementMock = [document.createElement('div')]; - - controller(scopeMock, elementMock); - - const { element } = mountMock.mock.calls[0][0]; - expect(element.parentElement).toEqual(elementMock[0]); - }); - - test('controller calls deprecated context app.mount when invoked', () => { - const unmountMock = jest.fn(); - // Two arguments changes how this is called. - const mountMock = jest.fn((context, params) => unmountMock); - legacyAppRegister({ - id: 'test', - title: 'Test', - mount: mountMock, - }); - const controller = setRootControllerMock.mock.calls[0][1]; - const scopeMock = { $on: jest.fn() }; - const elementMock = [document.createElement('div')]; - - controller(scopeMock, elementMock); - expect(mountMock).toHaveBeenCalledWith(expect.any(Object), { - element: expect.any(HTMLElement), - appBasePath: '/test/base/path/app/test', - onAppLeave: expect.any(Function), - history: historyMock, - }); - }); - - test('controller calls unmount when $scope.$destroy', async () => { - const { unmountMock } = registerApp(); - const controller = setRootControllerMock.mock.calls[0][1]; - const scopeMock = { $on: jest.fn() }; - const elementMock = [document.createElement('div')]; - - controller(scopeMock, elementMock); - // Flush promise queue. Must be done this way because the controller cannot return a Promise without breaking - // angular. - await new Promise((resolve) => setTimeout(resolve, 1)); - - const [event, eventHandler] = scopeMock.$on.mock.calls[0]; - expect(event).toEqual('$destroy'); - eventHandler(); - expect(unmountMock).toHaveBeenCalled(); - }); - }); -}); diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts deleted file mode 100644 index 37787ffbde4fc4..00000000000000 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ /dev/null @@ -1,208 +0,0 @@ -/* - * 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 { IScope } from 'angular'; - -import { UiActionsStart, UiActionsSetup } from 'src/plugins/ui_actions/public'; -import { EmbeddableStart, EmbeddableSetup } from 'src/plugins/embeddable/public'; -import { createBrowserHistory } from 'history'; -import { VisTypeXyPluginSetup } from 'src/plugins/vis_type_xy/public'; -import { DashboardStart } from '../../../../plugins/dashboard/public'; -import { setSetupServices, setStartServices } from './set_services'; -import { - LegacyCoreSetup, - LegacyCoreStart, - App, - AppMountDeprecated, - ScopedHistory, -} from '../../../../core/public'; -import { Plugin as DataPlugin } from '../../../../plugins/data/public'; -import { Plugin as ExpressionsPlugin } from '../../../../plugins/expressions/public'; -import { - Setup as InspectorSetup, - Start as InspectorStart, -} from '../../../../plugins/inspector/public'; -import { ChartsPluginSetup, ChartsPluginStart } from '../../../../plugins/charts/public'; -import { DevToolsSetup } from '../../../../plugins/dev_tools/public'; -import { KibanaLegacySetup, KibanaLegacyStart } from '../../../../plugins/kibana_legacy/public'; -import { HomePublicPluginSetup } from '../../../../plugins/home/public'; -import { SharePluginSetup, SharePluginStart } from '../../../../plugins/share/public'; -import { - AdvancedSettingsSetup, - AdvancedSettingsStart, -} from '../../../../plugins/advanced_settings/public'; -import { ManagementSetup, ManagementStart } from '../../../../plugins/management/public'; -import { - IndexPatternManagementSetup, - IndexPatternManagementStart, -} from '../../../../plugins/index_pattern_management/public'; -import { BfetchPublicSetup, BfetchPublicStart } from '../../../../plugins/bfetch/public'; -import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public'; -import { TelemetryPluginSetup, TelemetryPluginStart } from '../../../../plugins/telemetry/public'; -import { - NavigationPublicPluginSetup, - NavigationPublicPluginStart, -} from '../../../../plugins/navigation/public'; -import { DiscoverSetup, DiscoverStart } from '../../../../plugins/discover/public'; -import { - SavedObjectsManagementPluginSetup, - SavedObjectsManagementPluginStart, -} from '../../../../plugins/saved_objects_management/public'; -import { - VisualizationsSetup, - VisualizationsStart, -} from '../../../../plugins/visualizations/public'; -import { VisTypeTimelionPluginStart } from '../../../../plugins/vis_type_timelion/public'; -import { MapsLegacyPluginSetup } from '../../../../plugins/maps_legacy/public'; - -export interface PluginsSetup { - bfetch: BfetchPublicSetup; - charts: ChartsPluginSetup; - data: ReturnType; - embeddable: EmbeddableSetup; - expressions: ReturnType; - home: HomePublicPluginSetup; - inspector: InspectorSetup; - uiActions: UiActionsSetup; - navigation: NavigationPublicPluginSetup; - devTools: DevToolsSetup; - kibanaLegacy: KibanaLegacySetup; - share: SharePluginSetup; - usageCollection: UsageCollectionSetup; - advancedSettings: AdvancedSettingsSetup; - management: ManagementSetup; - discover: DiscoverSetup; - visualizations: VisualizationsSetup; - telemetry?: TelemetryPluginSetup; - savedObjectsManagement: SavedObjectsManagementPluginSetup; - mapsLegacy: MapsLegacyPluginSetup; - indexPatternManagement: IndexPatternManagementSetup; - visTypeXy?: VisTypeXyPluginSetup; -} - -export interface PluginsStart { - bfetch: BfetchPublicStart; - charts: ChartsPluginStart; - data: ReturnType; - embeddable: EmbeddableStart; - expressions: ReturnType; - inspector: InspectorStart; - uiActions: UiActionsStart; - navigation: NavigationPublicPluginStart; - kibanaLegacy: KibanaLegacyStart; - share: SharePluginStart; - management: ManagementStart; - advancedSettings: AdvancedSettingsStart; - discover: DiscoverStart; - visualizations: VisualizationsStart; - telemetry?: TelemetryPluginStart; - dashboard: DashboardStart; - savedObjectsManagement: SavedObjectsManagementPluginStart; - visTypeTimelion: VisTypeTimelionPluginStart; - indexPatternManagement: IndexPatternManagementStart; -} - -export const npSetup = { - core: (null as unknown) as LegacyCoreSetup, - plugins: {} as PluginsSetup, -}; - -export const npStart = { - core: (null as unknown) as LegacyCoreStart, - plugins: {} as PluginsStart, -}; - -/** - * Only used by unit tests - * @internal - */ -export function __reset__() { - npSetup.core = (null as unknown) as LegacyCoreSetup; - npSetup.plugins = {} as any; - npStart.core = (null as unknown) as LegacyCoreStart; - npStart.plugins = {} as any; - legacyAppRegistered = false; -} - -export function __setup__(coreSetup: LegacyCoreSetup, plugins: PluginsSetup) { - npSetup.core = coreSetup; - npSetup.plugins = plugins; - - // Setup compatibility layer for AppService in legacy platform - npSetup.core.application.register = legacyAppRegister; - - // 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); -} - -export function __start__(coreStart: LegacyCoreStart, plugins: PluginsStart) { - npStart.core = coreStart; - npStart.plugins = plugins; - - // 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); -} - -/** Flag used to ensure `legacyAppRegister` is only called once. */ -let legacyAppRegistered = false; - -/** - * Exported for testing only. Use `npSetup.core.application.register` in legacy apps. - * @internal - */ -export const legacyAppRegister = (app: App) => { - if (legacyAppRegistered) { - throw new Error(`core.application.register may only be called once for legacy plugins.`); - } - legacyAppRegistered = true; - - // eslint-disable-next-line @typescript-eslint/no-var-requires - require('ui/chrome').setRootController(app.id, ($scope: IScope, $element: JQLite) => { - const element = document.createElement('div'); - $element[0].appendChild(element); - - // Root controller cannot return a Promise so use an internal async function and call it immediately - (async () => { - const appRoute = app.appRoute || `/app/${app.id}`; - const appBasePath = npSetup.core.http.basePath.prepend(appRoute); - const params = { - element, - appBasePath, - history: new ScopedHistory( - createBrowserHistory({ basename: npSetup.core.http.basePath.get() }), - appRoute - ), - onAppLeave: () => undefined, - }; - const unmount = isAppMountDeprecated(app.mount) - ? await app.mount({ core: npStart.core }, params) - : await app.mount(params); - $scope.$on('$destroy', () => { - unmount(); - }); - })(); - }); -}; - -function isAppMountDeprecated(mount: (...args: any[]) => any): mount is AppMountDeprecated { - // Mount functions with two arguments are assumed to expect deprecated `context` object. - return mount.length === 2; -} diff --git a/src/legacy/ui/public/new_platform/set_services.test.ts b/src/legacy/ui/public/new_platform/set_services.test.ts deleted file mode 100644 index 74e789ec220b1b..00000000000000 --- a/src/legacy/ui/public/new_platform/set_services.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 { __reset__, __setup__, __start__, PluginsSetup, PluginsStart } from './new_platform'; -import * as dataServices from '../../../../plugins/data/public/services'; -import * as visualizationsServices from '../../../../plugins/visualizations/public/services'; -import { LegacyCoreSetup, LegacyCoreStart } from '../../../../core/public'; -import { coreMock } from '../../../../core/public/mocks'; -import { npSetup, npStart } from './__mocks__'; - -describe('ui/new_platform', () => { - describe('set service getters', () => { - const testServiceGetters = (name: string, services: Record) => { - const getters = Object.keys(services).filter((k) => k.substring(0, 3) === 'get'); - getters.forEach((g) => { - it(`ui/new_platform sets a value for ${name} getter ${g}`, () => { - __reset__(); - __setup__( - (coreMock.createSetup() as unknown) as LegacyCoreSetup, - (npSetup.plugins as unknown) as PluginsSetup - ); - __start__( - (coreMock.createStart() as unknown) as LegacyCoreStart, - (npStart.plugins as unknown) as PluginsStart - ); - - expect(services[g]()).toBeDefined(); - }); - }); - }; - - testServiceGetters('data', dataServices); - testServiceGetters('visualizations', visualizationsServices); - }); -}); diff --git a/src/legacy/ui/public/new_platform/set_services.ts b/src/legacy/ui/public/new_platform/set_services.ts deleted file mode 100644 index 036157a9f3fbc4..00000000000000 --- a/src/legacy/ui/public/new_platform/set_services.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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 { pick } from 'lodash'; - -import { PluginsSetup, PluginsStart } from './new_platform'; -import { LegacyCoreSetup, LegacyCoreStart } from '../../../../core/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import * as dataServices from '../../../../plugins/data/public/services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import * as visualizationsServices from '../../../../plugins/visualizations/public/services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { createSavedVisLoader } from '../../../../plugins/visualizations/public/saved_visualizations/saved_visualizations'; - -interface NpSetup { - core: LegacyCoreSetup; - plugins: PluginsSetup; -} - -interface NpStart { - core: LegacyCoreStart; - plugins: PluginsStart; -} - -export function setSetupServices(npSetup: NpSetup) { - // Services that need to be set in the legacy platform since the legacy data plugin - // which previously provided them has been removed. - visualizationsServices.setUISettings(npSetup.core.uiSettings); - visualizationsServices.setUsageCollector(npSetup.plugins.usageCollection); -} - -export function setStartServices(npStart: NpStart) { - // Services that need to be set in the legacy platform since the legacy data plugin - // which previously provided them has been removed. - dataServices.setNotifications(npStart.core.notifications); - dataServices.setOverlays(npStart.core.overlays); - dataServices.setUiSettings(npStart.core.uiSettings); - dataServices.setFieldFormats(npStart.plugins.data.fieldFormats); - dataServices.setIndexPatterns(npStart.plugins.data.indexPatterns); - dataServices.setQueryService(npStart.plugins.data.query); - dataServices.setSearchService(npStart.plugins.data.search); - - visualizationsServices.setI18n(npStart.core.i18n); - visualizationsServices.setTypes( - pick(npStart.plugins.visualizations, ['get', 'all', 'getAliases']) - ); - 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); - visualizationsServices.setExpressions(npStart.plugins.expressions); - visualizationsServices.setUiActions(npStart.plugins.uiActions); - visualizationsServices.setTimeFilter(npStart.plugins.data.query.timefilter.timefilter); - visualizationsServices.setAggs(npStart.plugins.data.search.aggs); - visualizationsServices.setOverlays(npStart.core.overlays); - visualizationsServices.setChrome(npStart.core.chrome); - visualizationsServices.setSearch(npStart.plugins.data.search); - const savedVisualizationsLoader = createSavedVisLoader({ - savedObjectsClient: npStart.core.savedObjects.client, - indexPatterns: npStart.plugins.data.indexPatterns, - search: npStart.plugins.data.search, - chrome: npStart.core.chrome, - overlays: npStart.core.overlays, - visualizationTypes: visualizationsServices.getTypes(), - }); - visualizationsServices.setSavedVisualizationsLoader(savedVisualizationsLoader); - visualizationsServices.setSavedSearchLoader(npStart.plugins.discover.savedSearchLoader); -} diff --git a/src/legacy/ui/public/notify/banners/BANNERS.md b/src/legacy/ui/public/notify/banners/BANNERS.md deleted file mode 100644 index fc6bddc3aa4edd..00000000000000 --- a/src/legacy/ui/public/notify/banners/BANNERS.md +++ /dev/null @@ -1,360 +0,0 @@ -# Banners - -Use this service to surface banners at the top of the screen. The expectation is that the banner will used an -`` to render, but that is not a requirement. See [the EUI docs](https://elastic.github.io/eui/) for -more information on banners and their role within the UI. - -Banners should be considered with respect to their lifecycle. Most banners are best served by using the `add` and -`remove` functions. - -## Importing the module - -```js -import { banners } from 'ui/notify'; -``` - -## Interface - -There are three methods defined to manipulate the list of banners: `add`, `set`, and `remove`. A fourth method, -`onChange` exists to listen to changes made via `add`, `set`, and `remove`. - -### `add()` - -This is the preferred way to add banners because it implies the best usage of the banner: added once during a page's -lifecycle. For other usages, consider *not* using a banner. - -#### Syntax - -```js -const bannerId = banners.add({ - // required: - component, - // optional: - priority, -}); -``` - -##### Parameters - -| Field | Type | Description | -|-------|------|-------------| -| `component` | Any | The value displayed as the banner. | -| `priority` | Number | Optional priority, which defaults to `0` used to place the banner. | - -To add a banner, you only need to define the `component` field. - -The `priority` sorts in descending order. Items sharing the same priority are sorted from oldest to newest. For example: - -```js -const banner1 = banners.add({ component: }); -const banner2 = banners.add({ component: , priority: 0 }); -const banner3 = banners.add({ component: , priority: 1 }); -``` - -That would be displayed as: - -``` -[ fake3 ] -[ fake1 ] -[ fake2 ] -``` - -##### Returns - -| Type | Description | -|------|-------------| -| String | A newly generated ID. | - -#### Example - -This example includes buttons that allow the user to remove the banner. In some cases, you may not want any buttons -and in other cases you will want an action to proceed the banner's removal (e.g., apply an Advanced Setting). - -This makes the most sense to use when a banner is added at the beginning of the page life cycle and not expected to -be touched, except by its own buttons triggering an action or navigating away. - -```js -const bannerId = banners.add({ - component: ( - - - - banners.remove(bannerId)} - > - Dismiss - - - - window.alert('Do Something Else')} - > - Do Something Else - - - - - ), -}); -``` - -### `remove()` - -Unlike toast notifications, banners stick around until they are explicitly removed. Using the `add` example above,you can remove it by calling `remove`. - -Note: They will stick around as long as the scope is remembered by whatever set it; navigating away won't remove it -unless the scope is forgotten (e.g., when the "app" changes)! - -#### Syntax - -```js -const removed = banners.remove(bannerId); -``` - -##### Parameters - -| Field | Type | Description | -|-------|------|-------------| -| `id` | String | ID of a banner. | - -##### Returns - -| Type | Description | -|------|-------------| -| Boolean | `true` if the ID was recognized and the banner was removed. `false` otherwise. | - -#### Example - -To remove a banner, you need to pass the `id` of the banner. - -```js -if (banners.remove(bannerId)) { - // removed; otherwise it didn't exist (maybe it was already removed) -} -``` - -#### Scheduled removal - -Like toast notifications do automatically, you can have a banner automatically removed after a set of time, by -setting a timer: - -```js -setTimeout(() => banners.remove(bannerId), 15000); -``` - -Note: It is safe to remove a banner more than once as unknown IDs will be ignored. - -### `set()` - -Banners can be replaced once added by supplying their `id`. If one is supplied, then the ID will be used to replace -any banner with the same ID and a **new** `id` will be returned. - -You should only consider using `set` when the banner is manipulated frequently in the lifecycle of the page, where -maintaining the banner's `id` can be a burden. It is easier to allow `banners` to create the ID for you in most -situations where a banner is useful (e.g., set once), which safely avoids any chance to have an ID-based collision, -which happens automatically with `add`. - -Usage of `set` can imply that your use case is abusing the banner system. - -Note: `set` will only trigger the callback once for both the implicit remove and add operation. - -#### Syntax - -```js -const id = banners.set({ - // required: - component, - // optional: - id, - priority, -}); -``` - -##### Parameters - -| Field | Type | Description | -|-------|------|-------------| -| `component` | Any | The value displayed as the banner. | -| `id` | String | Optional ID used to remove an existing banner. | -| `priority` | Number | Optional priority, which defaults to `0` used to place the banner. | - -The `id` is optional because it follows the same semantics as the `remove` method: unknown IDs are ignored. This -is useful when first creating a banner so that you do not have to call `add` instead. - -##### Returns - -| Type | Description | -|------|-------------| -| String | A newly generated ID. | - -#### Example - -This example does not include any way for the user to clear the banner directly. Instead, it is cleared based on -time. Related to it being cleared by time, it can also reappear within the same page life cycle by navigating between -different paths that need it displayed. Instead of adding a new banner for every navigation, you should replace any -existing banner. - -```js -let bannerId; -let timeoutId; - -function displayBanner() { - clearTimeout(timeoutId); - - bannerId = banners.set({ - id: bannerId, // the first time it will be undefined, but reused as long as this is in the same lifecycle - component: ( - - ) - }); - - // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around - banner.timeoutId = setTimeout(() => { - banners.remove(bannerId); - timeoutId = undefined; - }, 6000); -} -``` - -### `onChange()` - -For React components that intend to display the banners, it is not enough to simply `render` the `banners.list` -values. Because they can change after being rendered, the React component that renders the list must be alerted -to changes to the list. - -#### Syntax - -```js -// inside your React component -banners.onChange(() => this.forceUpdate()); -``` - -##### Parameters - -| Field | Type | Description | -|-------|------|-------------| -| `callback` | Function | The function to invoke whenever the internal banner list is changed. | - -Every new `callback` replaces the previous callback. So calling this with `null` or `undefined` will unset the -callback. - -##### Returns - -Nothing. - -#### Example - -This can be used inside of a React component to trigger a re-`render` of the banners. - -```js -import { GlobalBannerList } from 'ui/notify'; - - -``` - -### `list` - -For React components that intend to display the banners, it is not enough to simply `render` the `banners.list` -values. Because they can change after being rendered, the React component that renders the list must be alerted -to changes to the list. - -#### Syntax - -```js - -``` - -##### Returns - -| Type | Description | -|------|-------------| -| Array | The array of banner objects. | - -Banner objects are sorted in descending order based on their `priority`, in the form: - -```js -{ - id: 'banner-123', - component: , - priority: 12, -} -``` - -| Field | Type | Description | -|-------|------|-------------| -| `component` | Any | The value displayed as the banner. | -| `id` | String | The ID of the banner, which can be used as a React "key". | -| `priority` | Number | The priority of the banner. | - -#### Example - -This can be used to supply the banners to the `GlobalBannerList` React component (which is done for you). - -```js -import { GlobalBannerList } from 'ui/notify'; - - -``` - -## Use in functional tests - -Functional tests are commonly used to verify that an action yielded a successful outcome. You can place a -`data-test-subj` attribute on the banner and use it to check if the banner exists inside of your functional test. -This acts as a proxy for verifying the successful outcome. Any unrecognized field will be added as a property of the -containing element. - -```js -banners.add({ - component: ( - - ), - data-test-subj: 'my-tested-banner', -}); -``` - -This will apply the `data-test-subj` to the element containing the `component`, so the inner HTML of that element -will exclusively be the specified `component`. - -Given that `component` is expected to be a React component, you could also add the `data-test-subj` directly to it: - -```js -banners.add({ - component: ( - - ), -}); -``` \ No newline at end of file diff --git a/src/legacy/ui/public/notify/banners/banners.tsx b/src/legacy/ui/public/notify/banners/banners.tsx deleted file mode 100644 index 777e408b8dfc40..00000000000000 --- a/src/legacy/ui/public/notify/banners/banners.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 React from 'react'; -import ReactDOM from 'react-dom'; -import { npStart } from 'ui/new_platform'; -import { I18nProvider } from '@kbn/i18n/react'; - -const npBanners = npStart.core.overlays.banners; - -/** compatibility layer for new platform */ -const mountForComponent = (component: React.ReactElement) => (element: HTMLElement) => { - ReactDOM.render({component}, element); - return () => ReactDOM.unmountComponentAtNode(element); -}; - -/** - * Banners represents a prioritized list of displayed components. - */ -export class Banners { - /** - * Add a new banner. - * - * @param {Object} component The React component to display. - * @param {Number} priority The optional priority order to display this banner. Higher priority values are shown first. - * @return {String} A newly generated ID. This value can be used to remove/replace the banner. - */ - add = ({ component, priority }: { component: React.ReactElement; priority?: number }) => { - return npBanners.add(mountForComponent(component), priority); - }; - - /** - * Remove an existing banner. - * - * @param {String} id The ID of the banner to remove. - * @return {Boolean} {@code true} if the ID is recognized and the banner is removed. {@code false} otherwise. - */ - remove = (id: string): boolean => { - return npBanners.remove(id); - }; - - /** - * Replace an existing banner by removing it, if it exists, and adding a new one in its place. - * - * This is similar to calling banners.remove, followed by banners.add, except that it only notifies the listener - * after adding. - * - * @param {Object} component The React component to display. - * @param {String} id The ID of the Banner to remove. - * @param {Number} priority The optional priority order to display this banner. Higher priority values are shown first. - * @return {String} A newly generated ID. This value can be used to remove/replace the banner. - */ - set = ({ - component, - id, - priority = 0, - }: { - component: React.ReactElement; - id: string; - priority?: number; - }): string => { - return npBanners.replace(id, mountForComponent(component), priority); - }; -} - -/** - * A singleton instance meant to represent all Kibana banners. - */ -export const banners = new Banners(); diff --git a/src/legacy/ui/public/notify/banners/index.ts b/src/legacy/ui/public/notify/banners/index.ts deleted file mode 100644 index 9221f95074cd96..00000000000000 --- a/src/legacy/ui/public/notify/banners/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 { banners } from './banners'; diff --git a/src/legacy/ui/public/notify/fatal_error.ts b/src/legacy/ui/public/notify/fatal_error.ts deleted file mode 100644 index 5614ffea7913e7..00000000000000 --- a/src/legacy/ui/public/notify/fatal_error.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 { npSetup } from 'ui/new_platform'; -import { AngularHttpError, addFatalError } from '../../../../plugins/kibana_legacy/public'; - -export function fatalError(error: AngularHttpError | Error | string, location?: string) { - addFatalError(npSetup.core.fatalErrors, error, location); -} diff --git a/src/legacy/ui/public/notify/index.d.ts b/src/legacy/ui/public/notify/index.d.ts deleted file mode 100644 index 3c1a7ba02db72b..00000000000000 --- a/src/legacy/ui/public/notify/index.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 { toastNotifications } from './toasts'; -export { fatalError } from './fatal_error'; diff --git a/src/legacy/ui/public/notify/index.js b/src/legacy/ui/public/notify/index.js deleted file mode 100644 index 51394033e4d2e0..00000000000000 --- a/src/legacy/ui/public/notify/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 { fatalError } from './fatal_error'; -export { toastNotifications } from './toasts'; -export { banners } from './banners'; diff --git a/src/legacy/ui/public/notify/toasts/index.ts b/src/legacy/ui/public/notify/toasts/index.ts deleted file mode 100644 index c5e8e3b7ca7a3a..00000000000000 --- a/src/legacy/ui/public/notify/toasts/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 { ToastNotifications } from './toast_notifications'; -export { toastNotifications } from './toasts'; diff --git a/src/legacy/ui/public/notify/toasts/toast_notifications.ts b/src/legacy/ui/public/notify/toasts/toast_notifications.ts deleted file mode 100644 index d3ec8edb5d73a0..00000000000000 --- a/src/legacy/ui/public/notify/toasts/toast_notifications.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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. - */ -/** - * ToastNotifications is deprecated! Please use npSetup.core.notifications.toasts instead - */ -export { ToastNotifications } from '../../../../../plugins/kibana_legacy/public'; diff --git a/src/legacy/ui/public/notify/toasts/toasts.ts b/src/legacy/ui/public/notify/toasts/toasts.ts deleted file mode 100644 index 15be7a7891553a..00000000000000 --- a/src/legacy/ui/public/notify/toasts/toasts.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 { npSetup } from 'ui/new_platform'; -import { ToastNotifications } from './toast_notifications'; -export const toastNotifications = new ToastNotifications(npSetup.core.notifications.toasts); diff --git a/src/legacy/ui/public/private/__tests__/private.js b/src/legacy/ui/public/private/__tests__/private.js deleted file mode 100644 index 1f9d696bb440f3..00000000000000 --- a/src/legacy/ui/public/private/__tests__/private.js +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; - -describe('Private module loader', function () { - let Private; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function ($injector) { - Private = $injector.get('Private'); - }) - ); - - it('accepts a provider that will be called to init a module', function () { - const football = {}; - function Provider() { - return football; - } - - const instance = Private(Provider); - expect(instance).to.be(football); - }); - - it('injects angular dependencies into the Provider', function () { - function Provider(Private) { - return Private; - } - - const instance = Private(Provider); - expect(instance).to.be(Private); - }); - - it('detects circular dependencies', function () { - expect(function () { - function Provider1() { - Private(Provider2); - } - - function Provider2() { - Private(Provider1); - } - - Private(Provider1); - }).to.throwException(/circular/i); - }); - - it('always provides the same instance form the Provider', function () { - function Provider() { - return {}; - } - - expect(Private(Provider)).to.be(Private(Provider)); - }); - - describe('#stub', function () { - it('accepts a replacement instance for a Provider', function () { - const replaced = {}; - const replacement = {}; - - function Provider() { - return replaced; - } - - const instance = Private(Provider); - expect(instance).to.be(replaced); - - Private.stub(Provider, replacement); - - const instance2 = Private(Provider); - expect(instance2).to.be(replacement); - - Private.stub(Provider, replaced); - - const instance3 = Private(Provider); - expect(instance3).to.be(replaced); - }); - }); - - describe('#swap', function () { - it('accepts a new Provider that should replace an existing Provider', function () { - function Provider1() { - return {}; - } - - function Provider2() { - return {}; - } - - const instance1 = Private(Provider1); - expect(instance1).to.be.an('object'); - - Private.swap(Provider1, Provider2); - - const instance2 = Private(Provider1); - expect(instance2).to.be.an('object'); - expect(instance2).to.not.be(instance1); - - Private.swap(Provider1, Provider1); - - const instance3 = Private(Provider1); - expect(instance3).to.be(instance1); - }); - - it('gives the new Provider access to the Provider it replaced via an injectable dependency called $decorate', function () { - function Provider1() { - return {}; - } - - function Provider2($decorate) { - return { - instance1: $decorate(), - }; - } - - const instance1 = Private(Provider1); - expect(instance1).to.be.an('object'); - - Private.swap(Provider1, Provider2); - - const instance2 = Private(Provider1); - expect(instance2).to.have.property('instance1'); - expect(instance2.instance1).to.be(instance1); - }); - }); -}); diff --git a/src/legacy/ui/public/private/index.d.ts b/src/legacy/ui/public/private/index.d.ts deleted file mode 100644 index 3b692ba58cbe1f..00000000000000 --- a/src/legacy/ui/public/private/index.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 { IPrivate } from '../../../../plugins/kibana_legacy/public/'; diff --git a/src/legacy/ui/public/private/index.js b/src/legacy/ui/public/private/index.js deleted file mode 100644 index 3815f230df4664..00000000000000 --- a/src/legacy/ui/public/private/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 './private'; diff --git a/src/legacy/ui/public/private/private.js b/src/legacy/ui/public/private/private.js deleted file mode 100644 index 7a0751959417ee..00000000000000 --- a/src/legacy/ui/public/private/private.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 { uiModules } from '../modules'; -import { PrivateProvider } from '../../../../plugins/kibana_legacy/public'; - -uiModules.get('kibana/private').provider('Private', PrivateProvider); diff --git a/src/legacy/ui/public/promises/__tests__/promises.js b/src/legacy/ui/public/promises/__tests__/promises.js deleted file mode 100644 index 7041aa2993376e..00000000000000 --- a/src/legacy/ui/public/promises/__tests__/promises.js +++ /dev/null @@ -1,277 +0,0 @@ -/* - * 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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import sinon from 'sinon'; - -describe('Promise service', () => { - let Promise; - let $rootScope; - - const sandbox = sinon.createSandbox(); - function tick(ms = 0) { - sandbox.clock.tick(ms); - - // Ugly, but necessary for promises to resolve: https://github.com/angular/angular.js/issues/12555 - $rootScope.$apply(); - } - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(($injector) => { - sandbox.useFakeTimers(); - - Promise = $injector.get('Promise'); - $rootScope = $injector.get('$rootScope'); - }) - ); - - afterEach(() => sandbox.restore()); - - describe('Constructor', () => { - it('provides resolve and reject function', () => { - const executor = sinon.stub(); - new Promise(executor); - - sinon.assert.calledOnce(executor); - sinon.assert.calledWithExactly(executor, sinon.match.func, sinon.match.func); - }); - }); - - it('Promise.resolve', () => { - const onResolve = sinon.stub(); - Promise.resolve(true).then(onResolve); - - tick(); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, true); - }); - - describe('Promise.fromNode', () => { - it('creates a callback that controls a promise', () => { - const callback = sinon.stub(); - Promise.fromNode(callback); - - tick(); - - sinon.assert.calledOnce(callback); - sinon.assert.calledWithExactly(callback, sinon.match.func); - }); - - it('rejects if the callback receives an error', () => { - const err = new Error(); - const onReject = sinon.stub(); - Promise.fromNode(sinon.stub().yields(err)).catch(onReject); - - tick(); - - sinon.assert.calledOnce(onReject); - sinon.assert.calledWithExactly(onReject, sinon.match.same(err)); - }); - - it('resolves with the second argument', () => { - const result = {}; - const onResolve = sinon.stub(); - Promise.fromNode(sinon.stub().yields(null, result)).then(onResolve); - - tick(); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, sinon.match.same(result)); - }); - - it('resolves with an array if multiple arguments are received', () => { - const result1 = {}; - const result2 = {}; - const onResolve = sinon.stub(); - Promise.fromNode(sinon.stub().yields(null, result1, result2)).then(onResolve); - - tick(); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, [ - sinon.match.same(result1), - sinon.match.same(result2), - ]); - }); - - it('resolves with an array if multiple undefined are received', () => { - const onResolve = sinon.stub(); - Promise.fromNode(sinon.stub().yields(null, undefined, undefined)).then(onResolve); - - tick(); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, [undefined, undefined]); - }); - }); - - describe('Promise.race()', () => { - it(`resolves with the first resolved promise's value`, () => { - const p1 = new Promise((resolve) => setTimeout(resolve, 100, 1)); - const p2 = new Promise((resolve) => setTimeout(resolve, 200, 2)); - const onResolve = sinon.stub(); - Promise.race([p1, p2]).then(onResolve); - - tick(200); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, 1); - }); - - it(`rejects with the first rejected promise's rejection reason`, () => { - const p1Error = new Error('1'); - const p1 = new Promise((r, reject) => setTimeout(reject, 200, p1Error)); - - const p2Error = new Error('2'); - const p2 = new Promise((r, reject) => setTimeout(reject, 100, p2Error)); - - const onReject = sinon.stub(); - Promise.race([p1, p2]).catch(onReject); - - tick(200); - - sinon.assert.calledOnce(onReject); - sinon.assert.calledWithExactly(onReject, sinon.match.same(p2Error)); - }); - - it('does not wait for subsequent promises to resolve/reject', () => { - const onP1Resolve = sinon.stub(); - const p1 = new Promise((resolve) => setTimeout(resolve, 100)).then(onP1Resolve); - - const onP2Resolve = sinon.stub(); - const p2 = new Promise((resolve) => setTimeout(resolve, 101)).then(onP2Resolve); - - const onResolve = sinon.stub(); - Promise.race([p1, p2]).then(onResolve); - - tick(100); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledOnce(onP1Resolve); - sinon.assert.callOrder(onP1Resolve, onResolve); - sinon.assert.notCalled(onP2Resolve); - }); - - it('allows non-promises in the array', () => { - const onResolve = sinon.stub(); - Promise.race([1, 2, 3]).then(onResolve); - - tick(); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, 1); - }); - - describe('argument is undefined', () => { - it('rejects the promise', () => { - const football = {}; - const onReject = sinon.stub(); - Promise.race() - .catch(() => football) - .then(onReject); - - tick(); - - sinon.assert.calledOnce(onReject); - sinon.assert.calledWithExactly(onReject, sinon.match.same(football)); - }); - }); - - describe('argument is a string', () => { - it(`resolves with the first character`, () => { - const onResolve = sinon.stub(); - Promise.race('abc').then(onResolve); - - tick(); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, 'a'); - }); - }); - - describe('argument is a non-iterable object', () => { - it('reject the promise', () => { - const football = {}; - const onReject = sinon.stub(); - Promise.race({}) - .catch(() => football) - .then(onReject); - - tick(); - - sinon.assert.calledOnce(onReject); - sinon.assert.calledWithExactly(onReject, sinon.match.same(football)); - }); - }); - - describe('argument is a generator', () => { - it('resolves with the first resolved value', () => { - function* gen() { - yield new Promise((resolve) => setTimeout(resolve, 100, 1)); - yield new Promise((resolve) => setTimeout(resolve, 200, 2)); - } - - const onResolve = sinon.stub(); - Promise.race(gen()).then(onResolve); - - tick(200); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, 1); - }); - - it('resolves with the first non-promise value', () => { - function* gen() { - yield 1; - yield new Promise((resolve) => setTimeout(resolve, 200, 2)); - } - - const onResolve = sinon.stub(); - Promise.race(gen()).then(onResolve); - - tick(200); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, 1); - }); - - it('iterates all values from the generator, even if one is already "resolved"', () => { - let yieldCount = 0; - function* gen() { - yieldCount += 1; - yield 1; - yieldCount += 1; - yield new Promise((resolve) => setTimeout(resolve, 200, 2)); - } - - const onResolve = sinon.stub(); - Promise.race(gen()).then(onResolve); - - tick(200); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, 1); - expect(yieldCount).to.be(2); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/promises/defer.ts b/src/legacy/ui/public/promises/defer.ts deleted file mode 100644 index 3d435f2ba8dfdf..00000000000000 --- a/src/legacy/ui/public/promises/defer.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export interface Defer { - promise: Promise; - resolve(value: T): void; - reject(reason: Error): void; -} - -export function createDefer(Class: typeof Promise): Defer { - const defer: Partial> = {}; - defer.promise = new Class((resolve, reject) => { - defer.resolve = resolve; - defer.reject = reject; - }); - return defer as Defer; -} diff --git a/src/legacy/ui/public/promises/index.ts b/src/legacy/ui/public/promises/index.ts deleted file mode 100644 index 07bb2554c5eda6..00000000000000 --- a/src/legacy/ui/public/promises/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { PromiseServiceCreator } from '../../../../plugins/kibana_legacy/public'; -export { createDefer } from './defer'; -// @ts-ignore -import { uiModules } from '../modules'; - -const module = uiModules.get('kibana'); -// Provides a tiny subset of the excellent API from -// bluebird, reimplemented using the $q service -module.service('Promise', PromiseServiceCreator); diff --git a/src/legacy/ui/public/react_components.js b/src/legacy/ui/public/react_components.js deleted file mode 100644 index 21fb53d6407fa9..00000000000000 --- a/src/legacy/ui/public/react_components.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 'ngreact'; - -import { EuiIcon, EuiIconTip } from '@elastic/eui'; - -import { uiModules } from './modules'; - -const app = uiModules.get('app/kibana', ['react']); - -app.directive('icon', (reactDirective) => reactDirective(EuiIcon)); - -app.directive('iconTip', (reactDirective) => - reactDirective(EuiIconTip, ['content', 'type', 'position', 'title', 'color']) -); diff --git a/src/legacy/ui/public/registry/__tests__/registry.js b/src/legacy/ui/public/registry/__tests__/registry.js deleted file mode 100644 index 10b2423abc0e3b..00000000000000 --- a/src/legacy/ui/public/registry/__tests__/registry.js +++ /dev/null @@ -1,168 +0,0 @@ -/* - * 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 { uiRegistry } from '../_registry'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; - -describe('Registry', function () { - let Private; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function ($injector) { - Private = $injector.get('Private'); - }) - ); - - it('is technically a function', function () { - const reg = uiRegistry(); - expect(reg).to.be.a('function'); - }); - - describe('#register', function () { - it('accepts a Private module', function () { - const reg = uiRegistry(); - const mod = function SomePrivateModule() {}; - - reg.register(mod); - // modules are not exposed, so this is the most that we can test - }); - - it('applies the filter function if one is specified', function () { - const reg = uiRegistry({ - filter: (item) => item.value % 2 === 0, // register only even numbers - }); - - reg.register(() => ({ value: 17 })); - reg.register(() => ({ value: 18 })); // only this one should get registered - reg.register(() => ({ value: 19 })); - - const modules = Private(reg); - expect(modules).to.have.length(1); - expect(modules[0].value).to.be(18); - }); - }); - - describe('as a module', function () { - it('exposes the list of registered modules', function () { - const reg = uiRegistry(); - const mod = function SomePrivateModule(Private) { - this.PrivateModuleLoader = Private; - }; - - reg.register(mod); - const modules = Private(reg); - expect(modules).to.have.length(1); - expect(modules[0]).to.have.property('PrivateModuleLoader', Private); - }); - }); - - describe('spec', function () { - it('executes with the module list as "this", and can override it', function () { - let self; - - const reg = uiRegistry({ - constructor: function () { - return { mods: (self = this) }; - }, - }); - - const modules = Private(reg); - expect(modules).to.be.an('object'); - expect(modules).to.have.property('mods', self); - }); - }); - - describe('spec.name', function () { - it('sets the displayName of the registry and the name param on the final instance', function () { - const reg = uiRegistry({ - name: 'visTypes', - }); - - expect(reg).to.have.property('displayName', '[registry visTypes]'); - expect(Private(reg)).to.have.property('name', 'visTypes'); - }); - }); - - describe('spec.constructor', function () { - it('executes before the modules are returned', function () { - let i = 0; - - const reg = uiRegistry({ - constructor: function () { - i = i + 1; - }, - }); - - Private(reg); - expect(i).to.be(1); - }); - - it('executes with the module list as "this", and can override it', function () { - let self; - - const reg = uiRegistry({ - constructor: function () { - return { mods: (self = this) }; - }, - }); - - const modules = Private(reg); - expect(modules).to.be.an('object'); - expect(modules).to.have.property('mods', self); - }); - }); - - describe('spec.invokeProviders', () => { - it('is called with the registered providers and defines the initial set of values in the registry', () => { - const reg = uiRegistry({ - invokeProviders(providers) { - return providers.map((i) => i * 1000); - }, - }); - - reg.register(1); - reg.register(2); - reg.register(3); - expect(Private(reg).toJSON()).to.eql([1000, 2000, 3000]); - }); - it('does not get assigned as a property of the registry', () => { - expect( - uiRegistry({ - invokeProviders() {}, - }) - ).to.not.have.property('invokeProviders'); - }); - }); - - describe('spec[any]', function () { - it('mixes the extra properties into the module list', function () { - const reg = uiRegistry({ - someMethod: function () { - return this; - }, - }); - - const modules = Private(reg); - expect(modules).to.have.property('someMethod'); - expect(modules.someMethod()).to.be(modules); - }); - }); -}); diff --git a/src/legacy/ui/public/registry/_registry.d.ts b/src/legacy/ui/public/registry/_registry.d.ts deleted file mode 100644 index 42f1bb521763ca..00000000000000 --- a/src/legacy/ui/public/registry/_registry.d.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 { IndexedArray, IndexedArrayConfig } from '../indexed_array'; - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface UIRegistry extends IndexedArray {} - -interface UIRegistrySpec extends IndexedArrayConfig { - name: string; - filter?(item: T): boolean; -} - -/** - * Creates a new UiRegistry (See js method for detailed documentation) - * The generic type T is the type of objects which are stored in the registry. - * The generic type A is an interface of accessors which depend on the - * fields of the objects stored in the registry. - * Example: if there is a string field "name" in type T, then A should be - * `{ byName: { [typeName: string]: T }; }` - */ -declare function uiRegistry( - spec: UIRegistrySpec -): { (): UIRegistry & A; register(privateModule: T): UIRegistry & A }; diff --git a/src/legacy/ui/public/registry/_registry.js b/src/legacy/ui/public/registry/_registry.js deleted file mode 100644 index 85aa1d9f2eca8f..00000000000000 --- a/src/legacy/ui/public/registry/_registry.js +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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 _ from 'lodash'; -import { IndexedArray } from '../indexed_array'; - -const notPropsOptNames = IndexedArray.OPT_NAMES.concat('constructor', 'invokeProviders'); - -/** - * Create a registry, which is just a Private module provider. - * - * The registry allows modifying the values it will provide - * using the #register method. - * - * To access these modules, pass the registry to the Private - * module loader. - * - * # Examples - * - * + register a module - * ```js - * let registry = require('ui/registry/vis_types'); - * registry.add(function InjectablePrivateModule($http, Promise) { - * ... - * }) - * ``` - * - * + get all registered modules - * ```js - * let visTypes = Private(RegistryVisTypesProvider); - * ``` - * - * - * @param {object} [spec] - an object describing the properties of - * the registry to create. Any property specified - * that is not listed below will be mixed into the - * final IndexedArray object. - * - * # init - * @param {Function} [spec.constructor] - an injectable function that is called when - * the registry is first instantiated by the app. - * @param {boolean} [spec.filter] - function that will be used to filter items before - * registering them. Function will called on each item and - * should return true to keep the item (register it) or - * skip it (don't register it) - * - * # IndexedArray params - * @param {array[String]} [spec.index] - passed to the IndexedArray constructor - * @param {array[String]} [spec.group] - passed to the IndexedArray constructor - * @param {array[String]} [spec.order] - passed to the IndexedArray constructor - * @param {array[String]} [spec.initialSet] - passed to the IndexedArray constructor - * @param {array[String]} [spec.immutable] - passed to the IndexedArray constructor - * - * @return {[type]} [description] - */ -export function uiRegistry(spec) { - spec = spec || {}; - - const constructor = _.has(spec, 'constructor') && spec.constructor; - const filter = _.has(spec, 'filter') && spec.filter; - const invokeProviders = _.has(spec, 'invokeProviders') && spec.invokeProviders; - const iaOpts = _.defaults(_.pick(spec, IndexedArray.OPT_NAMES), { index: ['name'] }); - const props = _.omit(spec, notPropsOptNames); - const providers = []; - - let isInstantiated = false; - let getInvokedProviders; - let modules; - - /** - * This is the Private module that will be instantiated by - * - * @tag:PrivateModule - * @return {IndexedArray} - an indexed array containing the values - * that were registered, the registry spec - * defines how things will be indexed. - */ - const registry = function (Private, $injector) { - getInvokedProviders = function (newProviders) { - let set = invokeProviders - ? $injector.invoke(invokeProviders, undefined, { providers: newProviders }) - : newProviders.map(Private); - - if (filter && _.isFunction(filter)) { - set = set.filter((item) => filter(item)); - } - - return set; - }; - - iaOpts.initialSet = getInvokedProviders(providers); - - modules = new IndexedArray(iaOpts); - - // mixin other props - _.assign(modules, props); - - // construct - if (constructor) { - modules = $injector.invoke(constructor, modules) || modules; - } - - isInstantiated = true; - - return modules; - }; - - registry.displayName = '[registry ' + props.name + ']'; - - registry.register = function (privateModule) { - providers.push(privateModule); - - if (isInstantiated) { - const [provider] = getInvokedProviders([privateModule]); - - if (provider) { - modules.push(provider); - } - } - - return registry; - }; - - return registry; -} diff --git a/src/legacy/ui/public/registry/chrome_header_nav_controls.ts b/src/legacy/ui/public/registry/chrome_header_nav_controls.ts deleted file mode 100644 index 7e6c80692cd634..00000000000000 --- a/src/legacy/ui/public/registry/chrome_header_nav_controls.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 { IndexedArray } from '../indexed_array'; -import { uiRegistry, UIRegistry } from './_registry'; - -interface ChromeHeaderNavControlsRegistryAccessors { - bySide: { [typeName: string]: IndexedArray }; -} - -export enum NavControlSide { - Left = 'left', - Right = 'right', -} - -export interface NavControl { - name: string; - order: number; - side: NavControlSide; - render: (targetDomElement: HTMLDivElement) => () => void; -} - -export type ChromeHeaderNavControlsRegistry = UIRegistry & - ChromeHeaderNavControlsRegistryAccessors; - -export const chromeHeaderNavControlsRegistry = uiRegistry< - NavControl, - ChromeHeaderNavControlsRegistryAccessors ->({ - name: 'chromeHeaderNavControls', - order: ['order'], - group: ['side'], -}); diff --git a/src/legacy/ui/public/registry/field_format_editors.ts b/src/legacy/ui/public/registry/field_format_editors.ts deleted file mode 100644 index 5489ce9250e043..00000000000000 --- a/src/legacy/ui/public/registry/field_format_editors.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 { uiRegistry } from './_registry'; - -export const RegistryFieldFormatEditorsProvider = uiRegistry({ - name: 'fieldFormatEditors', - index: ['formatId'], -}); diff --git a/src/legacy/ui/public/routes/__tests__/_route_manager.js b/src/legacy/ui/public/routes/__tests__/_route_manager.js deleted file mode 100644 index eb47a3e9ace709..00000000000000 --- a/src/legacy/ui/public/routes/__tests__/_route_manager.js +++ /dev/null @@ -1,218 +0,0 @@ -/* - * 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 _ from 'lodash'; -import ngMock from 'ng_mock'; -import sinon from 'sinon'; -import RouteManager from '../route_manager'; -import expect from '@kbn/expect'; - -let routes; // will contain an new instance of RouteManager for each test -const chainableMethods = [ - { name: 'when', args: ['', {}] }, - { name: 'otherwise', args: [{}] }, - { name: 'defaults', args: [/regexp/, {}] }, -]; - -let $rp; -describe('routes/route_manager', function () { - beforeEach( - ngMock.module('kibana', function ($routeProvider) { - $rp = $routeProvider; - sinon.stub($rp, 'otherwise'); - sinon.stub($rp, 'when'); - }) - ); - - beforeEach( - ngMock.inject(function () { - routes = new RouteManager(); - }) - ); - - it('should have chainable methods: ' + _.map(chainableMethods, 'name').join(', '), function () { - chainableMethods.forEach(function (meth) { - expect(routes[meth.name].apply(routes, _.clone(meth.args))).to.be(routes); - }); - }); - - describe('#otherwise', function () { - it('should forward the last otherwise route', function () { - const otherRoute = {}; - routes.otherwise({}); - routes.otherwise(otherRoute); - - routes.config($rp); - - expect($rp.otherwise.callCount).to.be(1); - expect($rp.otherwise.getCall(0).args[0]).to.be(otherRoute); - }); - }); - - describe('#when', function () { - it('should merge the additions into the when() defined routes', function () { - routes.when('/some/route'); - routes.when('/some/other/route'); - - // add the addition resolve to every route - routes.defaults(/.*/, { - resolve: { - addition: function () {}, - }, - }); - - routes.config($rp); - - // should have run once for each when route - expect($rp.when.callCount).to.be(2); - expect($rp.otherwise.callCount).to.be(0); - - // every route should have the "addition" resolve - expect($rp.when.getCall(0).args[1].resolve.addition).to.be.a('function'); - expect($rp.when.getCall(1).args[1].resolve.addition).to.be.a('function'); - }); - }); - - describe('#config', function () { - it('should add defined routes to the global $routeProvider service in order', function () { - const args = [ - ['/one', {}], - ['/two', {}], - ]; - - args.forEach(function (a) { - routes.when(a[0], a[1]); - }); - - routes.config($rp); - - expect($rp.when.callCount).to.be(args.length); - _.times(args.length, function (i) { - const call = $rp.when.getCall(i); - const a = args.shift(); - - expect(call.args[0]).to.be(a[0]); - expect(call.args[1]).to.be(a[1]); - }); - }); - - it('sets route.reloadOnSearch to false by default', function () { - routes.when('/nothing-set'); - routes.when('/no-reload', { reloadOnSearch: false }); - routes.when('/always-reload', { reloadOnSearch: true }); - routes.config($rp); - - expect($rp.when.callCount).to.be(3); - expect($rp.when.firstCall.args[1]).to.have.property('reloadOnSearch', false); - expect($rp.when.secondCall.args[1]).to.have.property('reloadOnSearch', false); - expect($rp.when.lastCall.args[1]).to.have.property('reloadOnSearch', true); - }); - }); - - describe('#defaults()', () => { - it('adds defaults to routes with matching paths', () => { - routes.when('/foo', { name: 'foo' }); - routes.when('/bar', { name: 'bar' }); - routes.when('/baz', { name: 'baz' }); - routes.defaults(/^\/ba/, { - withDefaults: true, - }); - routes.config($rp); - - sinon.assert.calledWithExactly( - $rp.when, - '/foo', - sinon.match({ name: 'foo', withDefaults: undefined }) - ); - sinon.assert.calledWithExactly( - $rp.when, - '/bar', - sinon.match({ name: 'bar', withDefaults: true }) - ); - sinon.assert.calledWithExactly( - $rp.when, - '/baz', - sinon.match({ name: 'baz', withDefaults: true }) - ); - }); - - it('does not override values specified in the route', () => { - routes.when('/foo', { name: 'foo' }); - routes.defaults(/./, { name: 'bar' }); - routes.config($rp); - - sinon.assert.calledWithExactly($rp.when, '/foo', sinon.match({ name: 'foo' })); - }); - - // See https://github.com/elastic/kibana/issues/13294 - it('does not assign defaults by reference, to prevent accidentally merging unrelated defaults together', () => { - routes.when('/foo', { name: 'foo' }); - routes.when('/bar', { name: 'bar' }); - routes.when('/baz', { name: 'baz', funcs: { bazFunc() {} } }); - - // multiple defaults must be defined that, when applied correctly, will - // create a new object property on all routes that is unique to all of them - routes.defaults(/./, { funcs: { all() {} } }); - routes.defaults(/^\/foo/, { funcs: { fooFunc() {} } }); - routes.defaults(/^\/bar/, { funcs: { barFunc() {} } }); - routes.config($rp); - - sinon.assert.calledThrice($rp.when); - sinon.assert.calledWithExactly( - $rp.when, - '/foo', - sinon.match({ - name: 'foo', - funcs: sinon.match({ - all: sinon.match.func, - fooFunc: sinon.match.func, - barFunc: undefined, - bazFunc: undefined, - }), - }) - ); - sinon.assert.calledWithExactly( - $rp.when, - '/bar', - sinon.match({ - name: 'bar', - funcs: sinon.match({ - all: sinon.match.func, - fooFunc: undefined, - barFunc: sinon.match.func, - bazFunc: undefined, - }), - }) - ); - sinon.assert.calledWithExactly( - $rp.when, - '/baz', - sinon.match({ - name: 'baz', - funcs: sinon.match({ - all: sinon.match.func, - fooFunc: undefined, - barFunc: undefined, - bazFunc: sinon.match.func, - }), - }) - ); - }); - }); -}); diff --git a/src/legacy/ui/public/routes/__tests__/_work_queue.js b/src/legacy/ui/public/routes/__tests__/_work_queue.js deleted file mode 100644 index 72891f7321fbd7..00000000000000 --- a/src/legacy/ui/public/routes/__tests__/_work_queue.js +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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 _ from 'lodash'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { WorkQueue } from '../work_queue'; -import sinon from 'sinon'; -import { createDefer } from 'ui/promises'; - -describe('work queue', function () { - let queue; - let Promise; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function (_Promise_) { - Promise = _Promise_; - }) - ); - beforeEach(function () { - queue = new WorkQueue(); - }); - afterEach(function () { - queue.empty(); - }); - - describe('#push', function () { - it('adds to the interval queue', function () { - queue.push(createDefer(Promise)); - expect(queue).to.have.length(1); - }); - }); - - describe('#resolveWhenFull', function () { - it('resolves requests waiting for the queue to fill when appropriate', function () { - const size = _.random(5, 50); - queue.limit = size; - - const whenFull = createDefer(Promise); - sinon.stub(whenFull, 'resolve'); - queue.resolveWhenFull(whenFull); - - // push all but one into the queue - _.times(size - 1, function () { - queue.push(createDefer(Promise)); - }); - - expect(whenFull.resolve.callCount).to.be(0); - queue.push(createDefer(Promise)); - expect(whenFull.resolve.callCount).to.be(1); - - queue.empty(); - }); - }); - - /** - * Fills the queue with a random number of work defers, but stubs all defer methods - * with the same stub and passed it back. - * - * @param {function} then - called with then(size, stub) so that the test - * can manipulate the filled queue - */ - function fillWithStubs(then) { - const size = _.random(5, 50); - const stub = sinon.stub(); - - _.times(size, function () { - const d = createDefer(Promise); - // overwrite the defer methods with the stub - d.resolve = stub; - d.reject = stub; - queue.push(d); - }); - - then(size, stub); - } - - describe('#doWork', function () { - it('flushes the queue and resolves all promises', function () { - fillWithStubs(function (size, stub) { - expect(queue).to.have.length(size); - queue.doWork(); - expect(queue).to.have.length(0); - expect(stub.callCount).to.be(size); - }); - }); - }); - - describe('#empty()', function () { - it('empties the internal queue WITHOUT resolving any promises', function () { - fillWithStubs(function (size, stub) { - expect(queue).to.have.length(size); - queue.empty(); - expect(queue).to.have.length(0); - expect(stub.callCount).to.be(0); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/routes/__tests__/_wrap_route_with_prep.js b/src/legacy/ui/public/routes/__tests__/_wrap_route_with_prep.js deleted file mode 100644 index 8ae85fce591a11..00000000000000 --- a/src/legacy/ui/public/routes/__tests__/_wrap_route_with_prep.js +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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 RouteManager from '../route_manager'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; - -import _ from 'lodash'; -import '../../private'; - -let routes; - -describe('wrapRouteWithPrep fn', function () { - require('test_utils/no_digest_promises').activateForSuite(); - - beforeEach(function () { - routes = new RouteManager(); - }); - - const SchedulingTest = function (opts) { - opts = opts || {}; - - const delaySetup = opts.delayUserWork ? 0 : 50; - const delayUserWork = opts.delayUserWork ? 50 : 0; - - return function () { - ngMock.module('kibana'); - let setupComplete = false; - let userWorkComplete = false; - let route; - let Promise; - let $injector; - - ngMock.inject(function (_Promise_, _$injector_) { - Promise = _Promise_; - $injector = _$injector_; - }); - - routes.addSetupWork(function () { - return new Promise(function (resolve) { - setTimeout(function () { - setupComplete = true; - resolve(); - }, delaySetup); - }); - }); - - routes - .when('/', { - resolve: { - test: function () { - expect(setupComplete).to.be(true); - userWorkComplete = true; - }, - }, - }) - .config({ - when: function (p, _r) { - route = _r; - }, - }); - - return new Promise(function (resolve, reject) { - setTimeout(function () { - Promise.all( - _.map(route.resolve, function (fn) { - return $injector.invoke(fn); - }) - ) - .then(function () { - expect(setupComplete).to.be(true); - expect(userWorkComplete).to.be(true); - }) - .then(resolve, reject); - }, delayUserWork); - }); - }; - }; - - it('always waits for setup to complete before calling user work', new SchedulingTest()); - - it('does not call user work when setup fails', new SchedulingTest({ failSetup: true })); - - it( - 'calls all user work even if it is not initialized until after setup is complete', - new SchedulingTest({ - delayUserWork: false, - }) - ); -}); diff --git a/src/legacy/ui/public/routes/__tests__/index.js b/src/legacy/ui/public/routes/__tests__/index.js deleted file mode 100644 index 30cedaaca3d19f..00000000000000 --- a/src/legacy/ui/public/routes/__tests__/index.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 './_route_manager'; -import './_work_queue'; -import './_wrap_route_with_prep'; -describe('Custom Route Management', function () {}); diff --git a/src/legacy/ui/public/routes/breadcrumbs.js b/src/legacy/ui/public/routes/breadcrumbs.js deleted file mode 100644 index 7917ffbd7c6e61..00000000000000 --- a/src/legacy/ui/public/routes/breadcrumbs.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 { trim, startCase } from 'lodash'; - -/** - * Take a path (from $location.path() usually) and parse - * it's segments into a list of breadcrumbs - * - * @param {string} path - * @return {Array} - */ -export function parsePathToBreadcrumbs(path) { - return trim(path, '/') - .split('/') - .reduce( - (acc, id, i, parts) => [ - ...acc, - { - id, - display: startCase(id), - href: i === 0 ? `#/${id}` : `${acc[i - 1].href}/${id}`, - current: i === parts.length - 1, - }, - ], - [] - ); -} diff --git a/src/legacy/ui/public/routes/index.d.ts b/src/legacy/ui/public/routes/index.d.ts deleted file mode 100644 index 84e17b0a69452d..00000000000000 --- a/src/legacy/ui/public/routes/index.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 { uiRoutes, UIRoutes } from 'ui/routes/routes'; - -// eslint-disable-next-line import/no-default-export -export default uiRoutes; -export { UIRoutes }; diff --git a/src/legacy/ui/public/routes/index.js b/src/legacy/ui/public/routes/index.js deleted file mode 100644 index 9c826ebee12302..00000000000000 --- a/src/legacy/ui/public/routes/index.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 { uiRoutes } from './routes'; - -// eslint-disable-next-line import/no-default-export -export default uiRoutes; diff --git a/src/legacy/ui/public/routes/route_manager.d.ts b/src/legacy/ui/public/routes/route_manager.d.ts deleted file mode 100644 index a5261a7c8ee3a8..00000000000000 --- a/src/legacy/ui/public/routes/route_manager.d.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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. - */ - -/** - * WARNING: these types are incomplete - */ - -import { ChromeBreadcrumb } from '../../../../core/public'; - -export interface RouteConfiguration { - controller?: string | ((...args: any[]) => void); - redirectTo?: string; - resolveRedirectTo?: (...args: any[]) => void; - reloadOnSearch?: boolean; - reloadOnUrl?: boolean; - outerAngularWrapperRoute?: boolean; - resolve?: object; - template?: string; - k7Breadcrumbs?: (...args: any[]) => ChromeBreadcrumb[]; - requireUICapability?: string; -} - -interface RouteManager { - addSetupWork(cb: (...args: any[]) => void): void; - when(path: string, routeConfiguration: RouteConfiguration): RouteManager; - otherwise(routeConfiguration: RouteConfiguration): RouteManager; - defaults(path: string | RegExp, defaults: RouteConfiguration): RouteManager; -} - -// eslint-disable-next-line import/no-default-export -export default RouteManager; diff --git a/src/legacy/ui/public/routes/route_manager.js b/src/legacy/ui/public/routes/route_manager.js deleted file mode 100644 index de8a541d1c50a8..00000000000000 --- a/src/legacy/ui/public/routes/route_manager.js +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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 { cloneDeep, defaultsDeep, wrap } from 'lodash'; - -import { wrapRouteWithPrep } from './wrap_route_with_prep'; -import { RouteSetupManager } from './route_setup_manager'; -import { parsePathToBreadcrumbs } from './breadcrumbs'; - -// eslint-disable-next-line import/no-default-export -export default function RouteManager() { - const self = this; - const setup = new RouteSetupManager(); - const when = []; - const defaults = []; - let otherwise; - - self.config = function ($routeProvider) { - when.forEach(function (args) { - const path = args[0]; - const route = args[1] || {}; - - defaults.forEach((def) => { - if (def.regex.test(path)) { - defaultsDeep(route, cloneDeep(def.value)); - } - }); - - if (route.reloadOnSearch == null) { - route.reloadOnSearch = false; - } - - wrapRouteWithPrep(route, setup); - $routeProvider.when(path, route); - }); - - if (otherwise) { - wrapRouteWithPrep(otherwise, setup); - $routeProvider.otherwise(otherwise); - } - }; - - self.run = function ($location, $route, $injector, $rootScope) { - if (window.elasticApm && typeof window.elasticApm.startTransaction === 'function') { - /** - * capture route-change events as transactions which happens after - * the browser's on load event. - * - * In Kibana app, this logic would run after the boostrap js files gets - * downloaded and get associated with the page-load transaction - */ - $rootScope.$on('$routeChangeStart', (_, nextRoute) => { - if (nextRoute.$$route) { - const name = nextRoute.$$route.originalPath; - window.elasticApm.startTransaction(name, 'route-change'); - } - }); - } - - self.getBreadcrumbs = () => { - const breadcrumbs = parsePathToBreadcrumbs($location.path()); - const map = $route.current.mapBreadcrumbs; - return map ? $injector.invoke(map, null, { breadcrumbs }) : breadcrumbs; - }; - }; - - const wrapSetupAndChain = (fn, ...args) => { - fn.apply(setup, args); - return this; - }; - - this.addSetupWork = wrap(setup.addSetupWork, wrapSetupAndChain); - this.afterSetupWork = wrap(setup.afterSetupWork, wrapSetupAndChain); - this.afterWork = wrap(setup.afterWork, wrapSetupAndChain); - - self.when = function (path, route) { - when.push([path, route]); - return self; - }; - - // before attaching the routes to the routeProvider, test the RE - // against the .when() path and add/override the resolves if there is a match - self.defaults = function (regex, value) { - defaults.push({ regex, value }); - return self; - }; - - self.otherwise = function (route) { - otherwise = route; - return self; - }; - - self.getBreadcrumbs = function () { - // overwritten in self.run(); - return []; - }; - - self.RouteManager = RouteManager; -} diff --git a/src/legacy/ui/public/routes/route_setup_manager.js b/src/legacy/ui/public/routes/route_setup_manager.js deleted file mode 100644 index a7a2f078f40fb7..00000000000000 --- a/src/legacy/ui/public/routes/route_setup_manager.js +++ /dev/null @@ -1,112 +0,0 @@ -/* - * 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 _ from 'lodash'; -import { createDefer } from 'ui/promises'; - -// Throw this inside of an Angular route resolver after calling `kbnUrl.change` -// so that the $router can observe the $location update. Otherwise, the location -// route setup work will resolve before the route update occurs. -export const WAIT_FOR_URL_CHANGE_TOKEN = new Error('WAIT_FOR_URL_CHANGE_TOKEN'); - -export class RouteSetupManager { - constructor() { - this.setupWork = []; - this.onSetupComplete = []; - this.onSetupError = []; - this.onWorkComplete = []; - this.onWorkError = []; - } - - addSetupWork(fn) { - this.setupWork.push(fn); - } - - afterSetupWork(onComplete, onError) { - this.onSetupComplete.push(onComplete); - this.onSetupError.push(onError); - } - - afterWork(onComplete, onError) { - this.onWorkComplete.push(onComplete); - this.onWorkError.push(onError); - } - - /** - * Do each setupWork function by injecting it with angular dependencies - * and accepting promises from it. - * @return {[type]} [description] - */ - doWork(Promise, $injector, userWork) { - const invokeEach = (arr, locals) => { - return Promise.map(arr, (fn) => { - if (!fn) return; - return $injector.invoke(fn, null, locals); - }); - }; - - // call each error handler in order, until one of them resolves - // or we run out of handlers - const callErrorHandlers = (handlers, origError) => { - if (!_.size(handlers)) throw origError; - - // clone so we don't discard handlers or loose them - handlers = handlers.slice(0); - - const next = (err) => { - if (!handlers.length) throw err; - - const handler = handlers.shift(); - if (!handler) return next(err); - - return Promise.try(function () { - return $injector.invoke(handler, null, { err }); - }).catch(next); - }; - - return next(origError); - }; - - return invokeEach(this.setupWork) - .then( - () => invokeEach(this.onSetupComplete), - (err) => callErrorHandlers(this.onSetupError, err) - ) - .then(() => { - // wait for the queue to fill up, then do all the work - const defer = createDefer(Promise); - userWork.resolveWhenFull(defer); - - return defer.promise.then(() => Promise.all(userWork.doWork())); - }) - .catch((error) => { - if (error === WAIT_FOR_URL_CHANGE_TOKEN) { - // prevent moving forward, return a promise that never resolves - // so that the $router can observe the $location update - return Promise.halt(); - } - - throw error; - }) - .then( - () => invokeEach(this.onWorkComplete), - (err) => callErrorHandlers(this.onWorkError, err) - ); - } -} diff --git a/src/legacy/ui/public/routes/routes.d.ts b/src/legacy/ui/public/routes/routes.d.ts deleted file mode 100644 index d48230e9d56f92..00000000000000 --- a/src/legacy/ui/public/routes/routes.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 RouteManager from 'ui/routes/route_manager'; - -interface DefaultRouteManager extends RouteManager { - WAIT_FOR_URL_CHANGE_TOKEN: string; - enable(): void; -} - -export const uiRoutes: DefaultRouteManager; -export type UIRoutes = DefaultRouteManager; diff --git a/src/legacy/ui/public/routes/routes.js b/src/legacy/ui/public/routes/routes.js deleted file mode 100644 index 2cfb7a608e853b..00000000000000 --- a/src/legacy/ui/public/routes/routes.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 RouteManager from './route_manager'; -import 'angular-route/angular-route'; -import { uiModules } from '../modules'; -import { WAIT_FOR_URL_CHANGE_TOKEN } from './route_setup_manager'; -const defaultRouteManager = new RouteManager(); - -export const uiRoutes = Object.create(defaultRouteManager, { - WAIT_FOR_URL_CHANGE_TOKEN: { - value: WAIT_FOR_URL_CHANGE_TOKEN, - }, - - enable: { - value() { - uiModules - .get('kibana', ['ngRoute']) - .config(defaultRouteManager.config) - .run(defaultRouteManager.run); - }, - }, -}); diff --git a/src/legacy/ui/public/routes/work_queue.js b/src/legacy/ui/public/routes/work_queue.js deleted file mode 100644 index 5c4c83663c5902..00000000000000 --- a/src/legacy/ui/public/routes/work_queue.js +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 function WorkQueue() { - const q = this; - - const work = []; - const fullDefers = []; - - q.limit = 0; - Object.defineProperty(q, 'length', { - get: function () { - return work.length; - }, - }); - - const resolve = function (defers) { - return defers.splice(0).map(function (defer) { - return defer.resolve(); - }); - }; - - const checkIfFull = function () { - if (work.length >= q.limit && fullDefers.length) { - resolve(fullDefers); - } - }; - - q.resolveWhenFull = function (defer) { - fullDefers.push(defer); - checkIfFull(); - }; - - q.doWork = function () { - const resps = resolve(work); - checkIfFull(); - return resps; - }; - - q.empty = function () { - work.splice(0); - checkIfFull(); - }; - - q.push = function (defer) { - work.push(defer); - checkIfFull(); - }; -} diff --git a/src/legacy/ui/public/routes/wrap_route_with_prep.js b/src/legacy/ui/public/routes/wrap_route_with_prep.js deleted file mode 100644 index e9ed33148d9acf..00000000000000 --- a/src/legacy/ui/public/routes/wrap_route_with_prep.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 angular from 'angular'; -import _ from 'lodash'; -import { createDefer } from 'ui/promises'; -import { WorkQueue } from './work_queue'; - -export function wrapRouteWithPrep(route, setup) { - if (!route.resolve && route.redirectTo) return; - - const userWork = new WorkQueue(); - // the point at which we will consider the queue "full" - userWork.limit = _.keys(route.resolve).length; - - const resolve = { - __prep__: function ($injector) { - return $injector.invoke(setup.doWork, setup, { userWork }); - }, - }; - - // send each user resolve to the userWork queue, which will prevent it from running before the - // prep is complete - _.forOwn(route.resolve || {}, function (expr, name) { - resolve[name] = function ($injector, Promise) { - const defer = createDefer(Promise); - userWork.push(defer); - return defer.promise.then(function () { - return $injector[angular.isString(expr) ? 'get' : 'invoke'](expr); - }); - }; - }); - - // we're copied everything over so now overwrite - route.resolve = resolve; -} diff --git a/src/legacy/ui/public/state_management/__tests__/app_state.js b/src/legacy/ui/public/state_management/__tests__/app_state.js deleted file mode 100644 index c47fa3bb0417f4..00000000000000 --- a/src/legacy/ui/public/state_management/__tests__/app_state.js +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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 sinon from 'sinon'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { AppStateProvider } from '../app_state'; - -describe('State Management', function () { - let $rootScope; - let AppState; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function (_$rootScope_, _$location_, Private) { - $rootScope = _$rootScope_; - AppState = Private(AppStateProvider); - }) - ); - - describe('App State', function () { - let appState; - - beforeEach(function () { - appState = new AppState(); - }); - - it('should have _urlParam of _a', function () { - expect(appState).to.have.property('_urlParam'); - expect(appState._urlParam).to.equal('_a'); - }); - - it('should use passed in params', function () { - const params = { - test: true, - mock: false, - }; - - appState = new AppState(params); - expect(appState).to.have.property('_defaults'); - - Object.keys(params).forEach(function (key) { - expect(appState._defaults).to.have.property(key); - expect(appState._defaults[key]).to.equal(params[key]); - }); - }); - - it('should have a destroy method', function () { - expect(appState).to.have.property('destroy'); - }); - - it('should be destroyed on $routeChangeStart', function () { - const destroySpy = sinon.spy(appState, 'destroy'); - - $rootScope.$emit('$routeChangeStart'); - - expect(destroySpy.callCount).to.be(1); - }); - }); -}); diff --git a/src/legacy/ui/public/state_management/__tests__/config_provider.js b/src/legacy/ui/public/state_management/__tests__/config_provider.js deleted file mode 100644 index 9f756bc51dcb0b..00000000000000 --- a/src/legacy/ui/public/state_management/__tests__/config_provider.js +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import '../config_provider'; - -describe('State Management Config', function () { - let stateManagementConfig; - - describe('is enabled', () => { - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function (_stateManagementConfig_) { - stateManagementConfig = _stateManagementConfig_; - }) - ); - - it('should be enabled by default', () => { - expect(stateManagementConfig.enabled).to.be(true); - }); - }); - - describe('can be disabled', () => { - beforeEach( - ngMock.module('kibana', function (stateManagementConfigProvider) { - stateManagementConfigProvider.disable(); - }) - ); - - beforeEach( - ngMock.inject(function (_stateManagementConfig_) { - stateManagementConfig = _stateManagementConfig_; - }) - ); - - it('is disabled by config', () => { - expect(stateManagementConfig.enabled).to.be(false); - }); - }); -}); diff --git a/src/legacy/ui/public/state_management/__tests__/global_state.js b/src/legacy/ui/public/state_management/__tests__/global_state.js deleted file mode 100644 index e9dae5880a8d13..00000000000000 --- a/src/legacy/ui/public/state_management/__tests__/global_state.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import '../global_state'; - -describe('State Management', function () { - let $location; - let state; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function (_$location_, globalState) { - $location = _$location_; - state = globalState; - }) - ); - - describe('Global State', function () { - it('should use previous state when not in URL', function () { - // set satte via URL - $location.search({ _g: '(foo:(bar:baz))' }); - state.fetch(); - expect(state.toObject()).to.eql({ foo: { bar: 'baz' } }); - - $location.search({ _g: '(fizz:buzz)' }); - state.fetch(); - expect(state.toObject()).to.eql({ fizz: 'buzz' }); - - $location.search({}); - state.fetch(); - expect(state.toObject()).to.eql({ fizz: 'buzz' }); - }); - }); -}); diff --git a/src/legacy/ui/public/state_management/__tests__/state.js b/src/legacy/ui/public/state_management/__tests__/state.js deleted file mode 100644 index b6c705e8145094..00000000000000 --- a/src/legacy/ui/public/state_management/__tests__/state.js +++ /dev/null @@ -1,393 +0,0 @@ -/* - * 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 sinon from 'sinon'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { encode as encodeRison } from 'rison-node'; -import uiRoutes from 'ui/routes'; -import '../../private'; -import { toastNotifications } from '../../notify'; -import * as FatalErrorNS from '../../notify/fatal_error'; -import { StateProvider } from '../state'; -import { - unhashQuery, - createStateHash, - isStateHash, - HashedItemStore, -} from '../../../../../plugins/kibana_utils/public'; -import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; -import { EventsProvider } from '../../events'; - -describe('State Management', () => { - const sandbox = sinon.createSandbox(); - afterEach(() => sandbox.restore()); - - uiRoutes.enable(); - - describe('Enabled', () => { - let $rootScope; - let $location; - let Events; - let setup; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function (_$rootScope_, _$location_, Private, config) { - const State = Private(StateProvider); - $location = _$location_; - $rootScope = _$rootScope_; - Events = Private(EventsProvider); - - setup = (opts) => { - const { param, initial, storeInHash } = opts || {}; - sinon.stub(config, 'get').withArgs('state:storeInSessionStorage').returns(!!storeInHash); - const store = new StubBrowserStorage(); - const hashedItemStore = new HashedItemStore(store); - const state = new State(param, initial, hashedItemStore); - - const getUnhashedSearch = () => unhashQuery($location.search()); - - return { store, hashedItemStore, state, getUnhashedSearch }; - }; - }) - ); - - describe('Provider', () => { - it('should reset the state to the defaults', () => { - const { state, getUnhashedSearch } = setup({ initial: { message: ['test'] } }); - state.reset(); - const search = getUnhashedSearch(state); - expect(search).to.have.property('_s'); - expect(search._s).to.equal('(message:!(test))'); - expect(state.message).to.eql(['test']); - }); - - it('should apply the defaults upon initialization', () => { - const { state } = setup({ initial: { message: 'test' } }); - expect(state).to.have.property('message', 'test'); - }); - - it('should inherit from Events', () => { - const { state } = setup(); - expect(state).to.be.an(Events); - }); - - it('should emit an event if reset with changes', (done) => { - const { state } = setup({ initial: { message: ['test'] } }); - state.on('reset_with_changes', (keys) => { - expect(keys).to.eql(['message']); - done(); - }); - state.save(); - state.message = 'foo'; - state.reset(); - $rootScope.$apply(); - }); - - it('should not emit an event if reset without changes', () => { - const { state } = setup({ initial: { message: 'test' } }); - state.on('reset_with_changes', () => { - expect().fail(); - }); - state.save(); - state.message = 'test'; - state.reset(); - $rootScope.$apply(); - }); - }); - - describe('Search', () => { - it('should save to $location.search()', () => { - const { state, getUnhashedSearch } = setup({ initial: { test: 'foo' } }); - state.save(); - const search = getUnhashedSearch(state); - expect(search).to.have.property('_s'); - expect(search._s).to.equal('(test:foo)'); - }); - - it('should emit an event if changes are saved', (done) => { - const { state, getUnhashedSearch } = setup(); - state.on('save_with_changes', (keys) => { - expect(keys).to.eql(['test']); - done(); - }); - state.test = 'foo'; - state.save(); - getUnhashedSearch(state); - $rootScope.$apply(); - }); - }); - - describe('Fetch', () => { - it('should emit an event if changes are fetched', (done) => { - const { state } = setup(); - state.on('fetch_with_changes', (keys) => { - expect(keys).to.eql(['foo']); - done(); - }); - $location.search({ _s: '(foo:bar)' }); - state.fetch(); - expect(state).to.have.property('foo', 'bar'); - $rootScope.$apply(); - }); - - it('should have events that attach to scope', (done) => { - const { state } = setup(); - state.on('test', (message) => { - expect(message).to.equal('foo'); - done(); - }); - state.emit('test', 'foo'); - $rootScope.$apply(); - }); - - it('should fire listeners for #onUpdate() on #fetch()', (done) => { - const { state } = setup(); - state.on('fetch_with_changes', (keys) => { - expect(keys).to.eql(['foo']); - done(); - }); - $location.search({ _s: '(foo:bar)' }); - state.fetch(); - expect(state).to.have.property('foo', 'bar'); - $rootScope.$apply(); - }); - - it('should apply defaults to fetches', () => { - const { state } = setup({ initial: { message: 'test' } }); - $location.search({ _s: '(foo:bar)' }); - state.fetch(); - expect(state).to.have.property('foo', 'bar'); - expect(state).to.have.property('message', 'test'); - }); - - it('should call fetch when $routeUpdate is fired on $rootScope', () => { - const { state } = setup(); - const spy = sinon.spy(state, 'fetch'); - $rootScope.$emit('$routeUpdate', 'test'); - sinon.assert.calledOnce(spy); - }); - - it('should clear state when missing form URL', () => { - let stateObj; - const { state } = setup(); - - // set state via URL - $location.search({ _s: '(foo:(bar:baz))' }); - state.fetch(); - stateObj = state.toObject(); - expect(stateObj).to.eql({ foo: { bar: 'baz' } }); - - // ensure changing URL changes state - $location.search({ _s: '(one:two)' }); - state.fetch(); - stateObj = state.toObject(); - expect(stateObj).to.eql({ one: 'two' }); - - // remove search, state should be empty - $location.search({}); - state.fetch(); - stateObj = state.toObject(); - expect(stateObj).to.eql({}); - }); - - it('should clear state when it is invalid', () => { - let stateObj; - const { state } = setup(); - - $location.search({ _s: '' }); - state.fetch(); - stateObj = state.toObject(); - expect(stateObj).to.eql({}); - - $location.search({ _s: '!n' }); - state.fetch(); - stateObj = state.toObject(); - expect(stateObj).to.eql({}); - - $location.search({ _s: 'alert(1)' }); - state.fetch(); - stateObj = state.toObject(); - expect(stateObj).to.eql({}); - }); - - it('does not replace the state value on read', () => { - const { state } = setup(); - sinon.stub($location, 'search').callsFake((newSearch) => { - if (newSearch) { - return $location; - } else { - return { - [state.getQueryParamName()]: '(a:1)', - }; - } - }); - const replaceStub = sinon.stub($location, 'replace').returns($location); - - state.fetch(); - sinon.assert.notCalled(replaceStub); - }); - }); - - describe('Hashing', () => { - it('stores state values in a hashedItemStore, writing the hash to the url', () => { - const { state, hashedItemStore } = setup({ storeInHash: true }); - state.foo = 'bar'; - state.save(); - const urlVal = $location.search()[state.getQueryParamName()]; - - expect(isStateHash(urlVal)).to.be(true); - expect(hashedItemStore.getItem(urlVal)).to.eql(JSON.stringify({ foo: 'bar' })); - }); - - it('should replace rison in the URL with a hash', () => { - const { state, hashedItemStore } = setup({ storeInHash: true }); - const obj = { foo: { bar: 'baz' } }; - const rison = encodeRison(obj); - - $location.search({ _s: rison }); - state.fetch(); - - const urlVal = $location.search()._s; - expect(urlVal).to.not.be(rison); - expect(isStateHash(urlVal)).to.be(true); - expect(hashedItemStore.getItem(urlVal)).to.eql(JSON.stringify(obj)); - }); - - describe('error handling', () => { - it('notifies the user when a hash value does not map to a stored value', () => { - // Ideally, state.js shouldn't be tightly coupled to toastNotifications. Instead, it - // should notify its consumer of this error state and the consumer should be responsible - // for notifying the user of the error. This test verifies the side effect of the error - // until we can remove this coupling. - - // Clear existing toasts. - toastNotifications.list.splice(0); - - const { state } = setup({ storeInHash: true }); - const search = $location.search(); - const badHash = createStateHash('{"a": "b"}', () => null); - - search[state.getQueryParamName()] = badHash; - $location.search(search); - - expect(toastNotifications.list).to.have.length(0); - state.fetch(); - expect(toastNotifications.list).to.have.length(1); - expect(toastNotifications.list[0].title).to.match(/use the share functionality/i); - }); - - it.skip('triggers fatal error linking to github when setting item fails', () => { - // NOTE: this test needs to be written in jest and removed from the browser ones - // More info could be read in the opened issue: - // https://github.com/elastic/kibana/issues/22751 - const { state, hashedItemStore } = setup({ storeInHash: true }); - const fatalErrorStub = sandbox.stub(); - Object.defineProperty(FatalErrorNS, 'fatalError', { - writable: true, - value: fatalErrorStub, - }); - - sandbox.stub(hashedItemStore, 'setItem').returns(false); - state.toQueryParam(); - sinon.assert.calledOnce(fatalErrorStub); - sinon.assert.calledWith( - fatalErrorStub, - sinon.match((error) => error instanceof Error && error.message.includes('github.com')) - ); - }); - - it('translateHashToRison should gracefully fallback if parameter can not be parsed', () => { - const { state, hashedItemStore } = setup({ storeInHash: false }); - - expect(state.translateHashToRison('(a:b)')).to.be('(a:b)'); - expect(state.translateHashToRison('')).to.be(''); - - const existingHash = createStateHash('{"a": "b"}', () => null); - hashedItemStore.setItem(existingHash, '{"a": "b"}'); - - const nonExistingHash = createStateHash('{"b": "c"}', () => null); - - expect(state.translateHashToRison(existingHash)).to.be('(a:b)'); - expect(state.translateHashToRison(nonExistingHash)).to.be('!n'); - }); - }); - }); - }); - - describe('Disabled with persisted state', () => { - let state; - let $location; - let $rootScope; - const stateParam = '_config_test'; - - const getLocationState = () => { - const search = $location.search(); - return search[stateParam]; - }; - - beforeEach( - ngMock.module('kibana', function (stateManagementConfigProvider) { - stateManagementConfigProvider.disable(); - }) - ); - beforeEach( - ngMock.inject(function (_$rootScope_, _$location_, Private, config) { - const State = Private(StateProvider); - $location = _$location_; - $rootScope = _$rootScope_; - - sinon.stub(config, 'get').withArgs('state:storeInSessionStorage').returns(false); - - class MockPersistedState extends State { - _persistAcrossApps = true; - } - - MockPersistedState.prototype._persistAcrossApps = true; - - state = new MockPersistedState(stateParam); - }) - ); - - describe('changing state', () => { - const methods = ['save', 'replace', 'reset']; - - methods.forEach((method) => { - it(`${method} should not change the URL`, () => { - $location.search({ _s: '(foo:bar)' }); - state[method](); - $rootScope.$apply(); - expect(getLocationState()).to.be(undefined); - }); - }); - }); - - describe('reading state', () => { - it('should not change the URL', () => { - const saveSpy = sinon.spy(state, 'save'); - $location.search({ _s: '(foo:bar)' }); - state.fetch(); - $rootScope.$apply(); - sinon.assert.notCalled(saveSpy); - expect(getLocationState()).to.be(undefined); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/state_management/__tests__/state_monitor_factory.js b/src/legacy/ui/public/state_management/__tests__/state_monitor_factory.js deleted file mode 100644 index dc00d4e05e82f4..00000000000000 --- a/src/legacy/ui/public/state_management/__tests__/state_monitor_factory.js +++ /dev/null @@ -1,265 +0,0 @@ -/* - * 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 expect from '@kbn/expect'; -import sinon from 'sinon'; -import { EventEmitter } from 'events'; -import { cloneDeep } from 'lodash'; -import { stateMonitorFactory } from '../state_monitor_factory'; - -describe('stateMonitorFactory', function () { - const noop = () => {}; - const eventTypes = ['save_with_changes', 'reset_with_changes', 'fetch_with_changes']; - - let mockState; - - function setState(mockState, obj, emit = true) { - mockState.toJSON = () => cloneDeep(obj); - if (emit) mockState.emit(eventTypes[0]); - } - - function createMockState(state = {}) { - const mockState = new EventEmitter(); - setState(mockState, state, false); - return mockState; - } - - beforeEach(() => { - mockState = createMockState({}); - }); - - it('should have a create method', function () { - expect(stateMonitorFactory).to.have.property('create'); - expect(stateMonitorFactory.create).to.be.a('function'); - }); - - describe('factory creation', function () { - it('should not call onChange with only the state', function () { - const monitor = stateMonitorFactory.create(mockState); - const changeStub = sinon.stub(); - monitor.onChange(changeStub); - sinon.assert.notCalled(changeStub); - }); - - it('should not call onChange with matching defaultState', function () { - const monitor = stateMonitorFactory.create(mockState, {}); - const changeStub = sinon.stub(); - monitor.onChange(changeStub); - sinon.assert.notCalled(changeStub); - }); - - it('should call onChange with differing defaultState', function () { - const monitor = stateMonitorFactory.create(mockState, { test: true }); - const changeStub = sinon.stub(); - monitor.onChange(changeStub); - sinon.assert.calledOnce(changeStub); - }); - }); - - describe('instance', function () { - let monitor; - - beforeEach(() => { - monitor = stateMonitorFactory.create(mockState); - }); - - describe('onChange', function () { - it('should throw if not given a handler function', function () { - const fn = () => monitor.onChange('not a function'); - expect(fn).to.throwException(/must be a function/); - }); - - eventTypes.forEach((eventType) => { - describe(`when ${eventType} is emitted`, function () { - let handlerFn; - - beforeEach(() => { - handlerFn = sinon.stub(); - monitor.onChange(handlerFn); - sinon.assert.notCalled(handlerFn); - }); - - it('should get called', function () { - mockState.emit(eventType); - sinon.assert.calledOnce(handlerFn); - }); - - it('should be given the state status', function () { - mockState.emit(eventType); - const args = handlerFn.firstCall.args; - expect(args[0]).to.be.an('object'); - }); - - it('should be given the event type', function () { - mockState.emit(eventType); - const args = handlerFn.firstCall.args; - expect(args[1]).to.equal(eventType); - }); - - it('should be given the changed keys', function () { - const keys = ['one', 'two', 'three']; - mockState.emit(eventType, keys); - const args = handlerFn.firstCall.args; - expect(args[2]).to.equal(keys); - }); - }); - }); - }); - - describe('ignoreProps', function () { - it('should not set status to dirty when ignored properties change', function () { - let status; - const mockState = createMockState({ messages: { world: 'hello', foo: 'bar' } }); - const monitor = stateMonitorFactory.create(mockState); - const changeStub = sinon.stub(); - monitor.ignoreProps('messages.world'); - monitor.onChange(changeStub); - sinon.assert.notCalled(changeStub); - - // update the ignored state prop - setState(mockState, { messages: { world: 'howdy', foo: 'bar' } }); - sinon.assert.calledOnce(changeStub); - status = changeStub.firstCall.args[0]; - expect(status).to.have.property('clean', true); - expect(status).to.have.property('dirty', false); - - // update a prop that is not ignored - setState(mockState, { messages: { world: 'howdy', foo: 'baz' } }); - sinon.assert.calledTwice(changeStub); - status = changeStub.secondCall.args[0]; - expect(status).to.have.property('clean', false); - expect(status).to.have.property('dirty', true); - }); - }); - - describe('setInitialState', function () { - let changeStub; - - beforeEach(() => { - changeStub = sinon.stub(); - monitor.onChange(changeStub); - sinon.assert.notCalled(changeStub); - }); - - it('should throw if no state is provided', function () { - const fn = () => monitor.setInitialState(); - expect(fn).to.throwException(/must be an object/); - }); - - it('should throw if given the wrong type', function () { - const fn = () => monitor.setInitialState([]); - expect(fn).to.throwException(/must be an object/); - }); - - it('should trigger the onChange handler', function () { - monitor.setInitialState({ new: 'state' }); - sinon.assert.calledOnce(changeStub); - }); - - it('should change the status with differing state', function () { - monitor.setInitialState({ new: 'state' }); - sinon.assert.calledOnce(changeStub); - - const status = changeStub.firstCall.args[0]; - expect(status).to.have.property('clean', false); - expect(status).to.have.property('dirty', true); - }); - - it('should not trigger the onChange handler without state change', function () { - monitor.setInitialState(cloneDeep(mockState.toJSON())); - sinon.assert.notCalled(changeStub); - }); - }); - - describe('status object', function () { - let handlerFn; - - beforeEach(() => { - handlerFn = sinon.stub(); - monitor.onChange(handlerFn); - }); - - it('should be clean by default', function () { - mockState.emit(eventTypes[0]); - const status = handlerFn.firstCall.args[0]; - expect(status).to.have.property('clean', true); - expect(status).to.have.property('dirty', false); - }); - - it('should be dirty when state changes', function () { - setState(mockState, { message: 'i am dirty now' }); - const status = handlerFn.firstCall.args[0]; - expect(status).to.have.property('clean', false); - expect(status).to.have.property('dirty', true); - }); - - it('should be clean when state is reset', function () { - const defaultState = { message: 'i am the original state' }; - const handlerFn = sinon.stub(); - - let status; - - // initial state and monitor setup - const mockState = createMockState(defaultState); - const monitor = stateMonitorFactory.create(mockState); - monitor.onChange(handlerFn); - sinon.assert.notCalled(handlerFn); - - // change the state and emit an event - setState(mockState, { message: 'i am dirty now' }); - sinon.assert.calledOnce(handlerFn); - status = handlerFn.firstCall.args[0]; - expect(status).to.have.property('clean', false); - expect(status).to.have.property('dirty', true); - - // reset the state and emit an event - setState(mockState, defaultState); - sinon.assert.calledTwice(handlerFn); - status = handlerFn.secondCall.args[0]; - expect(status).to.have.property('clean', true); - expect(status).to.have.property('dirty', false); - }); - }); - - describe('destroy', function () { - let stateSpy; - - beforeEach(() => { - stateSpy = sinon.spy(mockState, 'off'); - sinon.assert.notCalled(stateSpy); - }); - - it('should remove the listeners', function () { - monitor.onChange(noop); - monitor.destroy(); - sinon.assert.callCount(stateSpy, eventTypes.length); - eventTypes.forEach((eventType) => { - sinon.assert.calledWith(stateSpy, eventType); - }); - }); - - it('should stop the instance from being used any more', function () { - monitor.onChange(noop); - monitor.destroy(); - const fn = () => monitor.onChange(noop); - expect(fn).to.throwException(/has been destroyed/); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/state_management/app_state.d.ts b/src/legacy/ui/public/state_management/app_state.d.ts deleted file mode 100644 index ddd0b907107665..00000000000000 --- a/src/legacy/ui/public/state_management/app_state.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 { State } from './state'; - -export class AppState extends State {} - -export type AppStateClass< - TAppState extends AppState = AppState, - TDefaults = { [key: string]: any } -> = new (defaults: TDefaults) => TAppState; diff --git a/src/legacy/ui/public/state_management/app_state.js b/src/legacy/ui/public/state_management/app_state.js deleted file mode 100644 index ec680d163b9da6..00000000000000 --- a/src/legacy/ui/public/state_management/app_state.js +++ /dev/null @@ -1,137 +0,0 @@ -/* - * 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. - */ - -/** - * @name AppState - * - * @extends State - * - * @description Inherits State, which inherits Events. This class seems to be - * concerned with mapping "props" to PersistedState instances, and surfacing the - * ability to destroy those mappings. - */ - -import { uiModules } from '../modules'; -import { StateProvider } from './state'; -import { PersistedState } from '../../../../plugins/visualizations/public'; -import { createLegacyClass } from '../utils/legacy_class'; - -const urlParam = '_a'; - -export function AppStateProvider(Private, $location) { - const State = Private(StateProvider); - - let persistedStates; - let eventUnsubscribers; - - createLegacyClass(AppState).inherits(State); - function AppState(defaults) { - // Initialize persistedStates. This object maps "prop" names to - // PersistedState instances. These are used to make properties "stateful". - persistedStates = {}; - - // Initialize eventUnsubscribers. These will be called in `destroy`, to - // remove handlers for the 'change' and 'fetch_with_changes' events which - // are dispatched via the rootScope. - eventUnsubscribers = []; - - AppState.Super.call(this, urlParam, defaults); - AppState.getAppState._set(this); - } - - // if the url param is missing, write it back - AppState.prototype._persistAcrossApps = false; - - AppState.prototype.destroy = function () { - AppState.Super.prototype.destroy.call(this); - AppState.getAppState._set(null); - - eventUnsubscribers.forEach((listener) => listener()); - }; - - /** - * @returns PersistedState instance. - */ - AppState.prototype.makeStateful = function (prop) { - if (persistedStates[prop]) return persistedStates[prop]; - const self = this; - - // set up the ui state - persistedStates[prop] = new PersistedState(); - - // update the app state when the stateful instance changes - const updateOnChange = function () { - const replaceState = false; // TODO: debouncing logic - self[prop] = persistedStates[prop].getChanges(); - // Save state to the URL. - self.save(replaceState); - }; - const handlerOnChange = (method) => persistedStates[prop][method]('change', updateOnChange); - handlerOnChange('on'); - eventUnsubscribers.push(() => handlerOnChange('off')); - - // update the stateful object when the app state changes - const persistOnChange = function (changes) { - if (!changes) return; - - if (changes.indexOf(prop) !== -1) { - persistedStates[prop].set(self[prop]); - } - }; - const handlePersist = (method) => this[method]('fetch_with_changes', persistOnChange); - handlePersist('on'); - eventUnsubscribers.push(() => handlePersist('off')); - - // if the thing we're making stateful has an appState value, write to persisted state - if (self[prop]) persistedStates[prop].setSilent(self[prop]); - - return persistedStates[prop]; - }; - - AppState.getAppState = (function () { - let currentAppState; - - function get() { - return currentAppState; - } - - // Checks to see if the appState might already exist, even if it hasn't been newed up - get.previouslyStored = function () { - const search = $location.search(); - return search[urlParam] ? true : false; - }; - - get._set = function (current) { - currentAppState = current; - }; - - return get; - })(); - - return AppState; -} - -uiModules - .get('kibana/global_state') - .factory('AppState', function (Private) { - return Private(AppStateProvider); - }) - .service('getAppState', function (Private) { - return Private(AppStateProvider).getAppState; - }); diff --git a/src/legacy/ui/public/state_management/config_provider.js b/src/legacy/ui/public/state_management/config_provider.js deleted file mode 100644 index 68de449a57a56e..00000000000000 --- a/src/legacy/ui/public/state_management/config_provider.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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. - */ - -/** - * @name stateManagementConfig - * - * @description Allows apps to configure state management - */ - -import { uiModules } from '../modules'; - -export class StateManagementConfigProvider { - _enabled = true; - - $get(/* inject stuff */) { - return { - enabled: this._enabled, - }; - } - - disable() { - this._enabled = false; - } - - enable() { - this._enabled = true; - } -} - -uiModules - .get('kibana/state_management') - .provider('stateManagementConfig', StateManagementConfigProvider); diff --git a/src/legacy/ui/public/state_management/global_state.d.ts b/src/legacy/ui/public/state_management/global_state.d.ts deleted file mode 100644 index 66a85d88956c77..00000000000000 --- a/src/legacy/ui/public/state_management/global_state.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 { State } from './state'; - -export type GlobalState = State; diff --git a/src/legacy/ui/public/state_management/global_state.js b/src/legacy/ui/public/state_management/global_state.js deleted file mode 100644 index 0e8dfe40d5950b..00000000000000 --- a/src/legacy/ui/public/state_management/global_state.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 { StateProvider } from './state'; -import { uiModules } from '../modules'; -import { createLegacyClass } from '../utils/legacy_class'; - -const module = uiModules.get('kibana/global_state'); - -export function GlobalStateProvider(Private) { - const State = Private(StateProvider); - - createLegacyClass(GlobalState).inherits(State); - function GlobalState(defaults) { - GlobalState.Super.call(this, '_g', defaults); - } - - // if the url param is missing, write it back - GlobalState.prototype._persistAcrossApps = true; - - return new GlobalState(); -} - -module.service('globalState', function (Private) { - return Private(GlobalStateProvider); -}); diff --git a/src/legacy/ui/public/state_management/state.d.ts b/src/legacy/ui/public/state_management/state.d.ts deleted file mode 100644 index b31862b681f55a..00000000000000 --- a/src/legacy/ui/public/state_management/state.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 class State { - [key: string]: any; - translateHashToRison: (stateHashOrRison: string | string[]) => string | string[]; - getQueryParamName: () => string; -} diff --git a/src/legacy/ui/public/state_management/state.js b/src/legacy/ui/public/state_management/state.js deleted file mode 100644 index d91834adb4a79e..00000000000000 --- a/src/legacy/ui/public/state_management/state.js +++ /dev/null @@ -1,359 +0,0 @@ -/* - * 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. - */ - -/** - * @name State - * - * @extends Events - * - * @description Persists generic "state" to and reads it from the URL. - */ - -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import angular from 'angular'; -import rison from 'rison-node'; -import { EventsProvider } from '../events'; -import { fatalError, toastNotifications } from '../notify'; -import './config_provider'; -import { createLegacyClass } from '../utils/legacy_class'; -import { - hashedItemStore, - isStateHash, - createStateHash, - applyDiff, -} from '../../../../plugins/kibana_utils/public'; - -export function StateProvider( - Private, - $rootScope, - $location, - stateManagementConfig, - config, - kbnUrl, - $injector -) { - const Events = Private(EventsProvider); - - const isDummyRoute = () => - $injector.has('$route') && - $injector.get('$route').current && - $injector.get('$route').current.outerAngularWrapperRoute; - - createLegacyClass(State).inherits(Events); - function State(urlParam, defaults, _hashedItemStore = hashedItemStore) { - State.Super.call(this); - - this.setDefaults(defaults); - this._urlParam = urlParam || '_s'; - this._hashedItemStore = _hashedItemStore; - - // When the URL updates we need to fetch the values from the URL - this._cleanUpListeners = [ - // partial route update, no app reload - $rootScope.$on('$routeUpdate', () => { - this.fetch(); - }), - - // beginning of full route update, new app will be initialized before - // $routeChangeSuccess or $routeChangeError - $rootScope.$on('$routeChangeStart', () => { - if (!this._persistAcrossApps) { - this.destroy(); - } - }), - - $rootScope.$on('$routeChangeSuccess', () => { - if (this._persistAcrossApps) { - this.fetch(); - } - }), - ]; - - // Initialize the State with fetch - this.fetch(); - } - - State.prototype._readFromURL = function () { - const search = $location.search(); - const urlVal = search[this._urlParam]; - - if (!urlVal) { - return null; - } - - if (isStateHash(urlVal)) { - return this._parseStateHash(urlVal); - } - - let risonEncoded; - let unableToParse; - try { - risonEncoded = rison.decode(urlVal); - } catch (e) { - unableToParse = true; - } - - if (unableToParse) { - toastNotifications.addDanger( - i18n.translate('common.ui.stateManagement.unableToParseUrlErrorMessage', { - defaultMessage: 'Unable to parse URL', - }) - ); - search[this._urlParam] = this.toQueryParam(this._defaults); - $location.search(search).replace(); - } - - if (!risonEncoded) { - return null; - } - - if (this.isHashingEnabled()) { - // RISON can find its way into the URL any number of ways, including the navbar links or - // shared urls with the entire state embedded. These values need to be translated into - // hashes and replaced in the browser history when state-hashing is enabled - search[this._urlParam] = this.toQueryParam(risonEncoded); - $location.search(search).replace(); - } - - return risonEncoded; - }; - - /** - * Fetches the state from the url - * @returns {void} - */ - State.prototype.fetch = function () { - if (!stateManagementConfig.enabled) { - return; - } - - let stash = this._readFromURL(); - - // nothing to read from the url? save if ordered to persist, but only if it's not on a wrapper route - if (stash === null) { - if (this._persistAcrossApps) { - return this.save(); - } else { - stash = {}; - } - } - - _.defaults(stash, this._defaults); - // apply diff to state from stash, will change state in place via side effect - const diffResults = applyDiff(this, stash); - - if (!isDummyRoute() && diffResults.keys.length) { - this.emit('fetch_with_changes', diffResults.keys); - } - }; - - /** - * Saves the state to the url - * @returns {void} - */ - State.prototype.save = function (replace) { - if (!stateManagementConfig.enabled) { - return; - } - - if (isDummyRoute()) { - return; - } - - let stash = this._readFromURL(); - const state = this.toObject(); - replace = replace || false; - - if (!stash) { - replace = true; - stash = {}; - } - - // apply diff to state from stash, will change state in place via side effect - const diffResults = applyDiff(stash, _.defaults({}, state, this._defaults)); - - if (diffResults.keys.length) { - this.emit('save_with_changes', diffResults.keys); - } - - // persist the state in the URL - const search = $location.search(); - search[this._urlParam] = this.toQueryParam(state); - if (replace) { - $location.search(search).replace(); - } else { - $location.search(search); - } - }; - - /** - * Calls save with a forced replace - * @returns {void} - */ - State.prototype.replace = function () { - if (!stateManagementConfig.enabled) { - return; - } - - this.save(true); - }; - - /** - * Resets the state to the defaults - * - * @returns {void} - */ - State.prototype.reset = function () { - if (!stateManagementConfig.enabled) { - return; - } - - kbnUrl.removeParam(this.getQueryParamName()); - // apply diff to attributes from defaults, this is side effecting so - // it will change the state in place. - const diffResults = applyDiff(this, this._defaults); - if (diffResults.keys.length) { - this.emit('reset_with_changes', diffResults.keys); - } - this.save(); - }; - - /** - * Cleans up the state object - * @returns {void} - */ - State.prototype.destroy = function () { - this.off(); // removes all listeners - - // Removes the $routeUpdate listener - this._cleanUpListeners.forEach((listener) => listener(this)); - }; - - State.prototype.setDefaults = function (defaults) { - this._defaults = defaults || {}; - }; - - /** - * Parse the state hash to it's unserialized value. Hashes are restored - * to their pre-hashed state. - * - * @param {string} stateHash - state hash value from the query string. - * @return {any} - the stored value, or null if hash does not resolve. - */ - State.prototype._parseStateHash = function (stateHash) { - const json = this._hashedItemStore.getItem(stateHash); - if (json === null) { - toastNotifications.addDanger( - i18n.translate('common.ui.stateManagement.unableToRestoreUrlErrorMessage', { - defaultMessage: - 'Unable to completely restore the URL, be sure to use the share functionality.', - }) - ); - } - - return JSON.parse(json); - }; - - /** - * Lookup the value for a hash and return it's value in rison format or just - * return passed argument if it's not recognized as state hash. - * - * @param {string} stateHashOrRison - either state hash value or rison string. - * @return {string} rison - */ - State.prototype.translateHashToRison = function (stateHashOrRison) { - if (isStateHash(stateHashOrRison)) { - return rison.encode(this._parseStateHash(stateHashOrRison)); - } - - return stateHashOrRison; - }; - - State.prototype.isHashingEnabled = function () { - return !!config.get('state:storeInSessionStorage'); - }; - - /** - * Produce the hash version of the state in it's current position - * - * @return {string} - */ - State.prototype.toQueryParam = function (state = this.toObject()) { - if (!this.isHashingEnabled()) { - return rison.encode(state); - } - - // We need to strip out Angular-specific properties. - const json = angular.toJson(state); - const hash = createStateHash(json); - const isItemSet = this._hashedItemStore.setItem(hash, json); - - if (isItemSet) { - return hash; - } - - // If we ran out of space trying to persist the state, notify the user. - const message = i18n.translate( - 'common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage', - { - defaultMessage: - 'Kibana is unable to store history items in your session ' + - `because it is full and there don't seem to be items any items safe ` + - 'to delete.\n\n' + - 'This can usually be fixed by moving to a fresh tab, but could ' + - 'be caused by a larger issue. If you are seeing this message regularly, ' + - 'please file an issue at {gitHubIssuesUrl}.', - values: { gitHubIssuesUrl: 'https://github.com/elastic/kibana/issues' }, - } - ); - fatalError(new Error(message)); - }; - - /** - * Get the query string parameter name where this state writes and reads - * @return {string} - */ - State.prototype.getQueryParamName = function () { - return this._urlParam; - }; - - /** - * Returns an object with each property name and value corresponding to the entries in this collection - * excluding fields started from '$', '_' and all methods - * - * @return {object} - */ - State.prototype.toObject = function () { - return _.omitBy(this, (value, key) => { - return key.charAt(0) === '$' || key.charAt(0) === '_' || _.isFunction(value); - }); - }; - - /** Alias for method 'toObject' - * - * @obsolete Please use 'toObject' method instead - * @return {object} - */ - State.prototype.toJSON = function () { - return this.toObject(); - }; - - return State; -} diff --git a/src/legacy/ui/public/state_management/state_monitor_factory.ts b/src/legacy/ui/public/state_management/state_monitor_factory.ts deleted file mode 100644 index 968ececfe3be5d..00000000000000 --- a/src/legacy/ui/public/state_management/state_monitor_factory.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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 { set } from '@elastic/safer-lodash-set'; -import { cloneDeep, isEqual, isPlainObject } from 'lodash'; -import { State } from './state'; - -export const stateMonitorFactory = { - create: ( - state: State, - customInitialState: TStateDefault - ) => stateMonitor(state, customInitialState), -}; - -interface StateStatus { - clean: boolean; - dirty: boolean; -} - -export interface StateMonitor { - setInitialState: (innerCustomInitialState: TStateDefault) => void; - ignoreProps: (props: string[] | string) => void; - onChange: (callback: ChangeHandlerFn) => StateMonitor; - destroy: () => void; -} - -type ChangeHandlerFn = (status: StateStatus, type: string | null, keys: string[]) => void; - -function stateMonitor( - state: State, - customInitialState: TStateDefault -): StateMonitor { - let destroyed = false; - let ignoredProps: string[] = []; - let changeHandlers: ChangeHandlerFn[] | undefined = []; - let initialState: TStateDefault; - - setInitialState(customInitialState); - - function setInitialState(innerCustomInitialState: TStateDefault) { - // state.toJSON returns a reference, clone so we can mutate it safely - initialState = cloneDeep(innerCustomInitialState) || cloneDeep(state.toJSON()); - } - - function removeIgnoredProps(innerState: TStateDefault) { - ignoredProps.forEach((path) => { - set(innerState, path, true); - }); - return innerState; - } - - function getStatus(): StateStatus { - // state.toJSON returns a reference, clone so we can mutate it safely - const currentState = removeIgnoredProps(cloneDeep(state.toJSON())); - const isClean = isEqual(currentState, initialState); - - return { - clean: isClean, - dirty: !isClean, - }; - } - - function dispatchChange(type: string | null = null, keys: string[] = []) { - const status = getStatus(); - if (!changeHandlers) { - throw new Error('Change handlers is undefined, this object has been destroyed'); - } - changeHandlers.forEach((changeHandler) => { - changeHandler(status, type, keys); - }); - } - - function dispatchFetch(keys: string[]) { - dispatchChange('fetch_with_changes', keys); - } - - function dispatchSave(keys: string[]) { - dispatchChange('save_with_changes', keys); - } - - function dispatchReset(keys: string[]) { - dispatchChange('reset_with_changes', keys); - } - - return { - setInitialState(innerCustomInitialState: TStateDefault) { - if (!isPlainObject(innerCustomInitialState)) { - throw new TypeError('The default state must be an object'); - } - - // check the current status - const previousStatus = getStatus(); - - // update the initialState and apply ignoredProps - setInitialState(innerCustomInitialState); - removeIgnoredProps(initialState); - - // fire the change handler if the status has changed - if (!isEqual(previousStatus, getStatus())) { - dispatchChange(); - } - }, - - ignoreProps(props: string[] | string) { - ignoredProps = ignoredProps.concat(props); - removeIgnoredProps(initialState); - return this; - }, - - onChange(callback: ChangeHandlerFn) { - if (destroyed || !changeHandlers) { - throw new Error('Monitor has been destroyed'); - } - if (typeof callback !== 'function') { - throw new Error('onChange handler must be a function'); - } - - changeHandlers.push(callback); - - // Listen for state events. - state.on('fetch_with_changes', dispatchFetch); - state.on('save_with_changes', dispatchSave); - state.on('reset_with_changes', dispatchReset); - - // if the state is already dirty, fire the change handler immediately - const status = getStatus(); - if (status.dirty) { - dispatchChange(); - } - - return this; - }, - - destroy() { - destroyed = true; - changeHandlers = undefined; - state.off('fetch_with_changes', dispatchFetch); - state.off('save_with_changes', dispatchSave); - state.off('reset_with_changes', dispatchReset); - }, - }; -} diff --git a/src/legacy/ui/public/system_api/__tests__/system_api.js b/src/legacy/ui/public/system_api/__tests__/system_api.js deleted file mode 100644 index 816024f13f8b25..00000000000000 --- a/src/legacy/ui/public/system_api/__tests__/system_api.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 expect from '@kbn/expect'; -import { - addSystemApiHeader, - isSystemApiRequest, -} from '../../../../../plugins/kibana_legacy/public'; - -describe('system_api', () => { - describe('#addSystemApiHeader', () => { - it('adds the correct system API header', () => { - const headers = { - 'kbn-version': '4.6.0', - }; - const newHeaders = addSystemApiHeader(headers); - - expect(newHeaders).to.have.property('kbn-system-request'); - expect(newHeaders['kbn-system-request']).to.be(true); - - expect(newHeaders).to.have.property('kbn-version'); - expect(newHeaders['kbn-version']).to.be('4.6.0'); - }); - }); - - describe('#isSystemApiRequest', () => { - it('returns true for a system HTTP request', () => { - const mockRequest = { - headers: { - 'kbn-system-request': true, - }, - }; - expect(isSystemApiRequest(mockRequest)).to.be(true); - }); - - it('returns true for a legacy system API HTTP request', () => { - const mockRequest = { - headers: { - 'kbn-system-api': true, - }, - }; - expect(isSystemApiRequest(mockRequest)).to.be(true); - }); - - it('returns false for a non-system API HTTP request', () => { - const mockRequest = { - headers: {}, - }; - expect(isSystemApiRequest(mockRequest)).to.be(false); - }); - }); -}); diff --git a/src/legacy/ui/public/system_api/index.js b/src/legacy/ui/public/system_api/index.js deleted file mode 100644 index 6361c0ea6c69ad..00000000000000 --- a/src/legacy/ui/public/system_api/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 { addSystemApiHeader, isSystemApiRequest } from '../../../../plugins/kibana_legacy/public'; diff --git a/src/legacy/ui/public/timefilter/index.ts b/src/legacy/ui/public/timefilter/index.ts deleted file mode 100644 index 83795c73112be1..00000000000000 --- a/src/legacy/ui/public/timefilter/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 uiRoutes from 'ui/routes'; -import { npStart } from 'ui/new_platform'; - -import { TimefilterContract, TimeHistoryContract } from '../../../../plugins/data/public'; -import { registerTimefilterWithGlobalState } from './setup_router'; - -export { - getTime, - InputTimeRange, - TimeHistoryContract, - TimefilterContract, -} from '../../../../plugins/data/public'; -export type Timefilter = TimefilterContract; -export type TimeHistory = TimeHistoryContract; -export const timeHistory = npStart.plugins.data.query.timefilter.history; -export const timefilter = npStart.plugins.data.query.timefilter.timefilter; - -uiRoutes.addSetupWork((globalState, $rootScope) => { - return registerTimefilterWithGlobalState(timefilter, globalState, $rootScope); -}); diff --git a/src/legacy/ui/public/timefilter/setup_router.test.js b/src/legacy/ui/public/timefilter/setup_router.test.js deleted file mode 100644 index 2ae9a053ae2db1..00000000000000 --- a/src/legacy/ui/public/timefilter/setup_router.test.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 { registerTimefilterWithGlobalState } from './setup_router'; - -jest.mock('../../../../plugins/kibana_legacy/public', () => ({ - subscribeWithScope: jest.fn(), -})); - -describe('registerTimefilterWithGlobalState()', () => { - it('should always use iso8601 strings', async () => { - const setTime = jest.fn(); - const timefilter = { - setTime, - setRefreshInterval: jest.fn(), - getRefreshIntervalUpdate$: jest.fn(), - getTimeUpdate$: jest.fn(), - }; - - const globalState = { - time: { - from: '2017-09-07T20:12:04.011Z', - to: '2017-09-07T20:18:55.733Z', - }, - on: (eventName, callback) => { - callback(); - }, - }; - - const rootScope = { - $on: jest.fn(), - }; - - registerTimefilterWithGlobalState(timefilter, globalState, rootScope); - - expect(setTime.mock.calls.length).toBe(2); - expect(setTime.mock.calls[1][0]).toEqual(globalState.time); - }); -}); diff --git a/src/legacy/ui/public/timefilter/setup_router.ts b/src/legacy/ui/public/timefilter/setup_router.ts deleted file mode 100644 index 7c25c6aa3166e3..00000000000000 --- a/src/legacy/ui/public/timefilter/setup_router.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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 _ from 'lodash'; -import { IScope } from 'angular'; -import moment from 'moment'; -import chrome from 'ui/chrome'; -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 -// and listener can be registered without angular. -function convertISO8601(stringTime: string): string { - const obj = moment(stringTime, 'YYYY-MM-DDTHH:mm:ss.SSSZ', true); - return obj.isValid() ? obj.toISOString() : stringTime; -} - -export function getTimefilterConfig() { - const settings = chrome.getUiSettingsClient(); - return { - timeDefaults: settings.get('timepicker:timeDefaults'), - refreshIntervalDefaults: settings.get(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS), - }; -} - -export const registerTimefilterWithGlobalStateFactory = ( - timefilter: TimefilterContract, - globalState: any, - $rootScope: IScope -) => { - // settings have to be re-fetched here, to make sure that settings changed by overrideLocalDefault are taken into account. - const config = getTimefilterConfig(); - timefilter.setTime(_.defaults(globalState.time || {}, config.timeDefaults)); - timefilter.setRefreshInterval( - _.defaults(globalState.refreshInterval || {}, config.refreshIntervalDefaults) - ); - - globalState.on('fetch_with_changes', () => { - // clone and default to {} in one - const newTime: TimeRange = _.defaults({}, globalState.time, config.timeDefaults); - const newRefreshInterval: RefreshInterval = _.defaults( - {}, - globalState.refreshInterval, - config.refreshIntervalDefaults - ); - - if (newTime) { - if (newTime.to) newTime.to = convertISO8601(newTime.to); - if (newTime.from) newTime.from = convertISO8601(newTime.from); - } - - timefilter.setTime(newTime); - timefilter.setRefreshInterval(newRefreshInterval); - }); - - const updateGlobalStateWithTime = () => { - globalState.time = timefilter.getTime(); - globalState.refreshInterval = timefilter.getRefreshInterval(); - globalState.save(); - }; - - const subscriptions = new Subscription(); - subscriptions.add( - subscribeWithScope( - $rootScope, - timefilter.getRefreshIntervalUpdate$(), - { - next: updateGlobalStateWithTime, - }, - fatalError - ) - ); - - subscriptions.add( - subscribeWithScope( - $rootScope, - timefilter.getTimeUpdate$(), - { - next: updateGlobalStateWithTime, - }, - fatalError - ) - ); - - $rootScope.$on('$destroy', () => { - subscriptions.unsubscribe(); - }); -}; - -// Currently some parts of Kibana (index patterns, timefilter) rely on addSetupWork in the uiRouter -// and require it to be executed to properly function. -// This function is exposed for applications that do not use uiRoutes like APM -// Kibana issue https://github.com/elastic/kibana/issues/19110 tracks the removal of this dependency on uiRouter -export const registerTimefilterWithGlobalState = _.once(registerTimefilterWithGlobalStateFactory); diff --git a/src/legacy/ui/public/url/__tests__/extract_app_path_and_id.js b/src/legacy/ui/public/url/__tests__/extract_app_path_and_id.js deleted file mode 100644 index 965e8f4bc9f383..00000000000000 --- a/src/legacy/ui/public/url/__tests__/extract_app_path_and_id.js +++ /dev/null @@ -1,122 +0,0 @@ -/* - * 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 expect from '@kbn/expect'; - -import { extractAppPathAndId } from '../extract_app_path_and_id'; - -describe('extractAppPathAndId', function () { - describe('from an absolute url with a base path', function () { - describe('with a base path', function () { - const basePath = '/gza'; - const absoluteUrl = 'http://www.test.com:5601/gza/app/appId#appPathIsHere?query=here'; - it('extracts app path', function () { - expect(extractAppPathAndId(absoluteUrl, basePath).appPath).to.be( - 'appPathIsHere?query=here' - ); - }); - - it('extracts app id', function () { - expect(extractAppPathAndId(absoluteUrl, basePath).appId).to.be('appId'); - }); - - it('returns an empty object when there is no app path', function () { - const appPathAndId = extractAppPathAndId('http://www.test.com:5601/gza/noapppath'); - expect(appPathAndId.appId).to.be(undefined); - expect(appPathAndId.appPath).to.be(undefined); - }); - }); - - describe('without a base path', function () { - const absoluteUrl = 'http://www.test.com:5601/app/appId#appPathIsHere?query=here'; - it('extracts app path', function () { - expect(extractAppPathAndId(absoluteUrl).appPath).to.be('appPathIsHere?query=here'); - }); - - it('extracts app id', function () { - expect(extractAppPathAndId(absoluteUrl).appId).to.be('appId'); - }); - - it('returns an empty object when there is no app path', function () { - const appPathAndId = extractAppPathAndId('http://www.test.com:5601/noapppath'); - expect(appPathAndId.appId).to.be(undefined); - expect(appPathAndId.appPath).to.be(undefined); - }); - }); - - describe('when appPath is empty', function () { - const absoluteUrl = 'http://www.test.com:5601/app/appId'; - it('extracts app id', function () { - expect(extractAppPathAndId(absoluteUrl).appId).to.be('appId'); - }); - it('extracts empty appPath', function () { - expect(extractAppPathAndId(absoluteUrl).appPath).to.be(''); - }); - }); - }); - - describe('from a root relative url', function () { - describe('with a base path', function () { - const basePath = '/gza'; - const rootRelativePath = '/gza/app/appId#appPathIsHere?query=here'; - it('extracts app path', function () { - expect(extractAppPathAndId(rootRelativePath, basePath).appPath).to.be( - 'appPathIsHere?query=here' - ); - }); - - it('extracts app id', function () { - expect(extractAppPathAndId(rootRelativePath, basePath).appId).to.be('appId'); - }); - - it('returns an empty object when there is no app path', function () { - const appPathAndId = extractAppPathAndId('/gza/notformattedright'); - expect(appPathAndId.appId).to.be(undefined); - expect(appPathAndId.appPath).to.be(undefined); - }); - }); - - describe('without a base path', function () { - const rootRelativePath = '/app/appId#appPathIsHere?query=here'; - it('extracts app path', function () { - expect(extractAppPathAndId(rootRelativePath).appPath).to.be('appPathIsHere?query=here'); - }); - - it('extracts app id', function () { - expect(extractAppPathAndId(rootRelativePath).appId).to.be('appId'); - }); - - it('returns an empty object when there is no app path', function () { - const appPathAndId = extractAppPathAndId('/notformattedright'); - expect(appPathAndId.appId).to.be(undefined); - expect(appPathAndId.appPath).to.be(undefined); - }); - }); - - describe('when appPath is empty', function () { - const rootRelativePath = '/app/appId'; - it('extracts app id', function () { - expect(extractAppPathAndId(rootRelativePath).appId).to.be('appId'); - }); - it('extracts empty appPath', function () { - expect(extractAppPathAndId(rootRelativePath).appPath).to.be(''); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/url/__tests__/kibana_parsed_url.js b/src/legacy/ui/public/url/__tests__/kibana_parsed_url.js deleted file mode 100644 index 6ea199c3d22cc1..00000000000000 --- a/src/legacy/ui/public/url/__tests__/kibana_parsed_url.js +++ /dev/null @@ -1,217 +0,0 @@ -/* - * 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 expect from '@kbn/expect'; - -import { KibanaParsedUrl } from '../kibana_parsed_url'; - -describe('KibanaParsedUrl', function () { - it('getHashedAppPath', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - basePath: '/hi', - appId: 'bye', - appPath: 'visualize?hi=there&bye', - }); - expect(kibanaParsedUrl.getHashedAppPath()).to.be('#visualize?hi=there&bye'); - }); - - it('getAppRootPath', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - basePath: '/hi', - appId: 'appId', - appPath: 'dashboard?edit=123', - }); - expect(kibanaParsedUrl.getAppRootPath()).to.be('/app/appId#dashboard?edit=123'); - }); - - describe('when basePath is specified', function () { - it('getRootRelativePath', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - basePath: '/base', - appId: 'appId', - appPath: 'visualize?hi=there&bye', - }); - expect(kibanaParsedUrl.getRootRelativePath()).to.be('/base/app/appId#visualize?hi=there&bye'); - }); - - describe('getAbsolutePath', function () { - const protocol = 'http'; - const hostname = 'www.test.com'; - const port = '5601'; - - it('returns the absolute url when there is a port', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - basePath: '/base', - appId: 'appId', - appPath: 'visualize?hi=there&bye', - hostname, - protocol, - port, - }); - expect(kibanaParsedUrl.getAbsoluteUrl()).to.be( - 'http://www.test.com:5601/base/app/appId#visualize?hi=there&bye' - ); - }); - - it('returns the absolute url when there are no query parameters', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - basePath: '/base', - appId: 'appId', - appPath: 'visualize', - hostname, - protocol, - }); - expect(kibanaParsedUrl.getAbsoluteUrl()).to.be( - 'http://www.test.com/base/app/appId#visualize' - ); - }); - - it('returns the absolute url when the are query parameters', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - basePath: '/base', - appId: 'appId', - appPath: 'visualize?hi=bye&tata', - hostname, - protocol, - }); - expect(kibanaParsedUrl.getAbsoluteUrl()).to.be( - 'http://www.test.com/base/app/appId#visualize?hi=bye&tata' - ); - }); - }); - }); - - describe('when basePath is not specified', function () { - it('getRootRelativePath', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - appId: 'appId', - appPath: 'visualize?hi=there&bye', - }); - expect(kibanaParsedUrl.getRootRelativePath()).to.be('/app/appId#visualize?hi=there&bye'); - }); - - describe('getAbsolutePath', function () { - const protocol = 'http'; - const hostname = 'www.test.com'; - const port = '5601'; - - it('returns the absolute url when there is a port', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - appId: 'appId', - appPath: 'visualize?hi=there&bye', - hostname, - protocol, - port, - }); - expect(kibanaParsedUrl.getAbsoluteUrl()).to.be( - 'http://www.test.com:5601/app/appId#visualize?hi=there&bye' - ); - }); - - it('returns the absolute url when there are no query parameters', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - appId: 'appId', - appPath: 'visualize', - hostname, - protocol, - }); - expect(kibanaParsedUrl.getAbsoluteUrl()).to.be('http://www.test.com/app/appId#visualize'); - }); - - it('returns the absolute url when there are query parameters', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - appId: 'appId', - appPath: 'visualize?hi=bye&tata', - hostname, - protocol, - }); - expect(kibanaParsedUrl.getAbsoluteUrl()).to.be( - 'http://www.test.com/app/appId#visualize?hi=bye&tata' - ); - }); - }); - }); - - describe('getGlobalState', function () { - const basePath = '/xyz'; - const appId = 'myApp'; - - it('returns an empty string when the KibanaParsedUrl is in an invalid state', function () { - const url = new KibanaParsedUrl({ basePath }); - expect(url.getGlobalState()).to.be(''); - }); - - it('returns an empty string when there is no global state', function () { - const url = new KibanaParsedUrl({ basePath, appId, appPath: '/hi?notg=something' }); - expect(url.getGlobalState()).to.be(''); - }); - - it('returns the global state when it is the last parameter', function () { - const url = new KibanaParsedUrl({ - basePath, - appId, - appPath: '/hi?notg=something&_g=(thisismyglobalstate)', - }); - expect(url.getGlobalState()).to.be('(thisismyglobalstate)'); - }); - - it('returns the global state when it is the first parameter', function () { - const url = new KibanaParsedUrl({ - basePath, - appId, - appPath: '/hi?_g=(thisismyglobalstate)&hi=bye', - }); - expect(url.getGlobalState()).to.be('(thisismyglobalstate)'); - }); - }); - - describe('setGlobalState', function () { - const basePath = '/xyz'; - const appId = 'myApp'; - - it('does nothing when KibanaParsedUrl is in an invalid state', function () { - const url = new KibanaParsedUrl({ basePath }); - url.setGlobalState('newglobalstate'); - expect(url.getGlobalState()).to.be(''); - }); - - it('clears the global state when setting it to an empty string', function () { - const url = new KibanaParsedUrl({ basePath, appId, appPath: '/hi?_g=globalstate' }); - url.setGlobalState(''); - expect(url.getGlobalState()).to.be(''); - }); - - it('updates the global state when a string is passed in', function () { - const url = new KibanaParsedUrl({ - basePath, - appId, - appPath: '/hi?notg=something&_g=oldstate', - }); - url.setGlobalState('newstate'); - expect(url.getGlobalState()).to.be('newstate'); - }); - - it('adds the global state parameters if it did not exist before', function () { - const url = new KibanaParsedUrl({ basePath, appId, appPath: '/hi' }); - url.setGlobalState('newstate'); - expect(url.getGlobalState()).to.be('newstate'); - expect(url.appPath).to.be('/hi?_g=newstate'); - }); - }); -}); diff --git a/src/legacy/ui/public/url/__tests__/prepend_path.js b/src/legacy/ui/public/url/__tests__/prepend_path.js deleted file mode 100644 index 36991b77553e40..00000000000000 --- a/src/legacy/ui/public/url/__tests__/prepend_path.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 expect from '@kbn/expect'; - -import { prependPath } from '../prepend_path'; - -describe('url prependPath', function () { - describe('returns the relative path unchanged', function () { - it('if it is null', function () { - expect(prependPath(null, 'kittens')).to.be(null); - }); - - it('if it is undefined', function () { - expect(prependPath(undefined, 'kittens')).to.be(undefined); - }); - - it('if it is an absolute url', function () { - expect(prependPath('http://www.hithere.com/howareyou', 'welcome')).to.be( - 'http://www.hithere.com/howareyou' - ); - }); - - it('if it does not start with a /', function () { - expect(prependPath('are/so/cool', 'cats')).to.be('are/so/cool'); - }); - - it('when new path is empty', function () { - expect(prependPath('/are/so/cool', '')).to.be('/are/so/cool'); - }); - - it('when it is only a slash and new path is empty', function () { - expect(prependPath('/', '')).to.be('/'); - }); - }); - - describe('returns an updated relative path', function () { - it('when it starts with a slash', function () { - expect(prependPath('/are/so/cool', 'dinosaurs')).to.be('dinosaurs/are/so/cool'); - }); - - it('when new path starts with a slash', function () { - expect(prependPath('/are/so/cool', '/fish')).to.be('/fish/are/so/cool'); - }); - - it('with two slashes if new path is a slash', function () { - expect(prependPath('/are/so/cool', '/')).to.be('//are/so/cool'); - }); - - it('when there is a slash on the end', function () { - expect(prependPath('/are/delicious/', 'lollipops')).to.be('lollipops/are/delicious/'); - }); - - it('when pathname that ends with a file', function () { - expect(prependPath('/are/delicious/index.html', 'donuts')).to.be( - 'donuts/are/delicious/index.html' - ); - }); - - it('when it is only a slash', function () { - expect(prependPath('/', 'kittens')).to.be('kittens/'); - }); - }); -}); diff --git a/src/legacy/ui/public/url/__tests__/url.js b/src/legacy/ui/public/url/__tests__/url.js deleted file mode 100644 index 8b173482e1bb4d..00000000000000 --- a/src/legacy/ui/public/url/__tests__/url.js +++ /dev/null @@ -1,562 +0,0 @@ -/* - * 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 sinon from 'sinon'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import faker from 'faker'; -import _ from 'lodash'; -import { AppStateProvider } from '../../state_management/app_state'; -import '..'; - -// global vars, injected and mocked in init() -let kbnUrl; -let $route; -let $location; -let $rootScope; -let appState; - -class StubAppState { - constructor() { - this.getQueryParamName = () => '_a'; - this.toQueryParam = () => 'stateQueryParam'; - this.destroy = sinon.stub(); - } -} - -function init() { - ngMock.module('kibana/url', 'kibana', function ($provide, PrivateProvider) { - $provide.service('$route', function () { - return { - reload: _.noop, - }; - }); - - appState = new StubAppState(); - PrivateProvider.swap(AppStateProvider, ($decorate) => { - const AppState = $decorate(); - AppState.getAppState = () => appState; - return AppState; - }); - }); - - ngMock.inject(function ($injector) { - $route = $injector.get('$route'); - $location = $injector.get('$location'); - $rootScope = $injector.get('$rootScope'); - kbnUrl = $injector.get('kbnUrl'); - }); -} - -describe('kbnUrl', function () { - beforeEach(function () { - init(); - }); - - describe('forcing reload', function () { - it('schedules a listener for $locationChangeSuccess on the $rootScope', function () { - $location.url('/url'); - $route.current = { - $$route: { - regex: /.*/, - }, - }; - - sinon.stub($rootScope, '$on'); - - expect($rootScope.$on.callCount).to.be(0); - kbnUrl.change('/url'); - sinon.assert.calledOnce(appState.destroy); - expect($rootScope.$on.callCount).to.be(1); - expect($rootScope.$on.firstCall.args[0]).to.be('$locationChangeSuccess'); - }); - - it('handler unbinds the listener and calls reload', function () { - $location.url('/url'); - $route.current = { - $$route: { - regex: /.*/, - }, - }; - - const unbind = sinon.stub(); - sinon.stub($rootScope, '$on').returns(unbind); - $route.reload = sinon.stub(); - - expect($rootScope.$on.callCount).to.be(0); - kbnUrl.change('/url'); - expect($rootScope.$on.callCount).to.be(1); - - const handler = $rootScope.$on.firstCall.args[1]; - handler(); - expect(unbind.callCount).to.be(1); - expect($route.reload.callCount).to.be(1); - }); - - it('reloads requested before the first are ignored', function () { - $location.url('/url'); - $route.current = { - $$route: { - regex: /.*/, - }, - }; - $route.reload = sinon.stub(); - - sinon.stub($rootScope, '$on').returns(sinon.stub()); - - expect($rootScope.$on.callCount).to.be(0); - kbnUrl.change('/url'); - expect($rootScope.$on.callCount).to.be(1); - - // don't call the first handler - - kbnUrl.change('/url'); - expect($rootScope.$on.callCount).to.be(1); - }); - - it('one reload can happen once the first has completed', function () { - $location.url('/url'); - $route.current = { - $$route: { - regex: /.*/, - }, - }; - $route.reload = sinon.stub(); - - sinon.stub($rootScope, '$on').returns(sinon.stub()); - - expect($rootScope.$on.callCount).to.be(0); - kbnUrl.change('/url'); - expect($rootScope.$on.callCount).to.be(1); - - // call the first handler - $rootScope.$on.firstCall.args[1](); - expect($route.reload.callCount).to.be(1); - - expect($rootScope.$on.callCount).to.be(1); - kbnUrl.change('/url'); - expect($rootScope.$on.callCount).to.be(2); - }); - }); - - describe('remove', function () { - it('removes a parameter with a value from the url', function () { - $location.url('/myurl?exist&WithAParamToRemove=2&anothershouldexist=5'); - kbnUrl.removeParam('WithAParamToRemove'); - expect($location.url()).to.be('/myurl?exist&anothershouldexist=5'); - }); - - it('removes a parameter with no value from the url', function () { - $location.url('/myurl?removeme&hi=5'); - kbnUrl.removeParam('removeme'); - expect($location.url()).to.be('/myurl?hi=5'); - }); - - it('is noop if the given parameter doesn\t exist in the url', function () { - $location.url('/myurl?hi&bye'); - kbnUrl.removeParam('noexist'); - expect($location.url()).to.be('/myurl?hi&bye'); - }); - - it('is noop if given empty string param', function () { - $location.url('/myurl?hi&bye'); - kbnUrl.removeParam(''); - expect($location.url()).to.be('/myurl?hi&bye'); - }); - }); - - describe('change', function () { - it('should set $location.url', function () { - sinon.stub($location, 'url'); - - expect($location.url.callCount).to.be(0); - kbnUrl.change('/some-url'); - expect($location.url.callCount).to.be(1); - }); - - it('should uri encode replaced params', function () { - const url = '/some/path/'; - const params = { replace: faker.Lorem.words(3).join(' ') }; - const check = encodeURIComponent(params.replace); - sinon.stub($location, 'url'); - - kbnUrl.change(url + '{{replace}}', params); - - expect($location.url.firstCall.args[0]).to.be(url + check); - }); - - it('should parse angular expression in substitutions and uri encode the results', function () { - // build url by piecing together these parts - const urlParts = ['/', '/', '?', '&', '#']; - // make sure it can parse templates with weird spacing - const wrappers = [ - ['{{', '}}'], - ['{{ ', ' }}'], - ['{{', ' }}'], - ['{{ ', '}}'], - ['{{ ', ' }}'], - ]; - // make sure filters are evaluated via angular expressions - const objIndex = 4; // used to case one replace as an object - const filters = ['', 'uppercase', '', 'uppercase', '']; - - // the words (template keys) used must all be unique - const words = _.uniq(faker.Lorem.words(10)) - .slice(0, urlParts.length) - .map(function (word, i) { - if (filters[i].length) { - return word + '|' + filters[i]; - } - return word; - }); - - const replacements = faker.Lorem.words(urlParts.length).map(function (word, i) { - // make selected replacement into an object - if (i === objIndex) { - return { replace: word }; - } - - return word; - }); - - // build the url and test url - let url = ''; - let testUrl = ''; - urlParts.forEach(function (part, i) { - url += part + wrappers[i][0] + words[i] + wrappers[i][1]; - const locals = {}; - locals[words[i].split('|')[0]] = replacements[i]; - testUrl += part + encodeURIComponent($rootScope.$eval(words[i], locals)); - }); - - // create the locals replacement object - const params = {}; - replacements.forEach(function (replacement, i) { - const word = words[i].split('|')[0]; - params[word] = replacement; - }); - - sinon.stub($location, 'url'); - - kbnUrl.change(url, params); - - expect($location.url.firstCall.args[0]).to.not.be(url); - expect($location.url.firstCall.args[0]).to.be(testUrl); - }); - - it('should handle dot notation', function () { - const url = '/some/thing/{{that.is.substituted}}'; - - kbnUrl.change(url, { - that: { - is: { - substituted: 'test', - }, - }, - }); - - expect($location.url()).to.be('/some/thing/test'); - }); - - it('should throw when params are missing', function () { - const url = '/{{replace_me}}'; - const params = {}; - - try { - kbnUrl.change(url, params); - throw new Error('this should not run'); - } catch (err) { - expect(err).to.be.an(Error); - expect(err.message).to.match(/replace_me/); - } - }); - - it('should throw when filtered params are missing', function () { - const url = '/{{replace_me|number}}'; - const params = {}; - - try { - kbnUrl.change(url, params); - throw new Error('this should not run'); - } catch (err) { - expect(err).to.be.an(Error); - expect(err.message).to.match(/replace_me\|number/); - } - }); - - it('should change the entire url', function () { - const path = '/test/path'; - const search = { search: 'test' }; - const hash = 'hash'; - const newPath = '/new/location'; - - $location.path(path).search(search).hash(hash); - - // verify the starting state - expect($location.path()).to.be(path); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - - kbnUrl.change(newPath); - - // verify the ending state - expect($location.path()).to.be(newPath); - expect($location.search()).to.eql({}); - expect($location.hash()).to.be(''); - }); - - it('should allow setting app state on the target url', function () { - const path = '/test/path'; - const search = { search: 'test' }; - const hash = 'hash'; - const newPath = '/new/location'; - - $location.path(path).search(search).hash(hash); - - // verify the starting state - expect($location.path()).to.be(path); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - - kbnUrl.change(newPath, null, new StubAppState()); - - // verify the ending state - expect($location.path()).to.be(newPath); - expect($location.search()).to.eql({ _a: 'stateQueryParam' }); - expect($location.hash()).to.be(''); - }); - }); - - describe('changePath', function () { - it('should change just the path', function () { - const path = '/test/path'; - const search = { search: 'test' }; - const hash = 'hash'; - const newPath = '/new/location'; - - $location.path(path).search(search).hash(hash); - - // verify the starting state - expect($location.path()).to.be(path); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - - kbnUrl.changePath(newPath); - - // verify the ending state - expect($location.path()).to.be(newPath); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - }); - }); - - describe('redirect', function () { - it('should change the entire url', function () { - const path = '/test/path'; - const search = { search: 'test' }; - const hash = 'hash'; - const newPath = '/new/location'; - - $location.path(path).search(search).hash(hash); - - // verify the starting state - expect($location.path()).to.be(path); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - - kbnUrl.redirect(newPath); - - // verify the ending state - expect($location.path()).to.be(newPath); - expect($location.search()).to.eql({}); - expect($location.hash()).to.be(''); - }); - - it('should allow setting app state on the target url', function () { - const path = '/test/path'; - const search = { search: 'test' }; - const hash = 'hash'; - const newPath = '/new/location'; - - $location.path(path).search(search).hash(hash); - - // verify the starting state - expect($location.path()).to.be(path); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - - kbnUrl.redirect(newPath, null, new StubAppState()); - - // verify the ending state - expect($location.path()).to.be(newPath); - expect($location.search()).to.eql({ _a: 'stateQueryParam' }); - expect($location.hash()).to.be(''); - }); - - it('should replace the current history entry', function () { - sinon.stub($location, 'replace'); - $location.url('/some/path'); - - expect($location.replace.callCount).to.be(0); - kbnUrl.redirect('/new/path/'); - expect($location.replace.callCount).to.be(1); - }); - - it('should call replace on $location', function () { - sinon.stub(kbnUrl, '_shouldForceReload').returns(false); - sinon.stub($location, 'replace'); - - expect($location.replace.callCount).to.be(0); - kbnUrl.redirect('/poop'); - expect($location.replace.callCount).to.be(1); - }); - }); - - describe('redirectPath', function () { - it('should only change the path', function () { - const path = '/test/path'; - const search = { search: 'test' }; - const hash = 'hash'; - const newPath = '/new/location'; - - $location.path(path).search(search).hash(hash); - - // verify the starting state - expect($location.path()).to.be(path); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - - kbnUrl.redirectPath(newPath); - - // verify the ending state - expect($location.path()).to.be(newPath); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - }); - - it('should call replace on $location', function () { - sinon.stub(kbnUrl, '_shouldForceReload').returns(false); - sinon.stub($location, 'replace'); - - expect($location.replace.callCount).to.be(0); - kbnUrl.redirectPath('/poop'); - expect($location.replace.callCount).to.be(1); - }); - }); - - describe('_shouldForceReload', function () { - let next; - let prev; - - beforeEach(function () { - $route.current = { - $$route: { - regexp: /^\/is-current-route\/(\d+)/, - reloadOnSearch: true, - }, - }; - - prev = { path: '/is-current-route/1', search: {} }; - next = { path: '/is-current-route/1', search: {} }; - }); - - it("returns false if the passed url doesn't match the current route", function () { - next.path = '/not current'; - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(false); - }); - - describe('if the passed url does match the route', function () { - describe('and the route reloads on search', function () { - describe('and the path is the same', function () { - describe('and the search params are the same', function () { - it('returns true', function () { - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(true); - }); - }); - describe('but the search params are different', function () { - it('returns false', function () { - next.search = {}; - prev.search = { q: 'search term' }; - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(false); - }); - }); - }); - - describe('and the path is different', function () { - beforeEach(function () { - next.path = '/not-same'; - }); - - describe('and the search params are the same', function () { - it('returns false', function () { - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(false); - }); - }); - describe('but the search params are different', function () { - it('returns false', function () { - next.search = {}; - prev.search = { q: 'search term' }; - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(false); - }); - }); - }); - }); - - describe('but the route does not reload on search', function () { - beforeEach(function () { - $route.current.$$route.reloadOnSearch = false; - }); - - describe('and the path is the same', function () { - describe('and the search params are the same', function () { - it('returns true', function () { - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(true); - }); - }); - describe('but the search params are different', function () { - it('returns true', function () { - next.search = {}; - prev.search = { q: 'search term' }; - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(true); - }); - }); - }); - - describe('and the path is different', function () { - beforeEach(function () { - next.path = '/not-same'; - }); - - describe('and the search params are the same', function () { - it('returns false', function () { - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(false); - }); - }); - describe('but the search params are different', function () { - it('returns false', function () { - next.search = {}; - prev.search = { q: 'search term' }; - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(false); - }); - }); - }); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/url/absolute_to_parsed_url.ts b/src/legacy/ui/public/url/absolute_to_parsed_url.ts deleted file mode 100644 index 30f493c25776c2..00000000000000 --- a/src/legacy/ui/public/url/absolute_to_parsed_url.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 { parse } from 'url'; - -import { extractAppPathAndId } from './extract_app_path_and_id'; -import { KibanaParsedUrl } from './kibana_parsed_url'; - -/** - * - * @param absoluteUrl - an absolute url, e.g. https://localhost:5601/gra/app/visualize#/edit/viz_id?hi=bye - * @param basePath - An optional base path for kibana. If supplied, should start with a "/". - * e.g. in https://localhost:5601/gra/app/visualize#/edit/viz_id the basePath is - * "/gra". - * @return {KibanaParsedUrl} - */ -export function absoluteToParsedUrl(absoluteUrl: string, basePath = '') { - const { appPath, appId } = extractAppPathAndId(absoluteUrl, basePath); - const { hostname, port, protocol } = parse(absoluteUrl); - return new KibanaParsedUrl({ - basePath, - appId: appId!, - appPath, - hostname, - port, - protocol, - }); -} diff --git a/src/legacy/ui/public/url/extract_app_path_and_id.ts b/src/legacy/ui/public/url/extract_app_path_and_id.ts deleted file mode 100644 index 44bba272e0873a..00000000000000 --- a/src/legacy/ui/public/url/extract_app_path_and_id.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 { parse } from 'url'; - -/** - * If the url is determined to contain an appId and appPath, it returns those portions. If it is not in the right - * format and an appId and appPath can't be extracted, it returns an empty object. - * @param {string} url - a relative or absolute url which contains an appPath, an appId, and optionally, a basePath. - * @param {string} basePath - optional base path, if given should start with "/". - */ -export function extractAppPathAndId(url: string, basePath = '') { - const parsedUrl = parse(url); - if (!parsedUrl.path) { - return {}; - } - const pathWithoutBase = parsedUrl.path.slice(basePath.length); - - if (!pathWithoutBase.startsWith('/app/')) { - return {}; - } - - const appPath = parsedUrl.hash && parsedUrl.hash.length > 0 ? parsedUrl.hash.slice(1) : ''; - return { appId: pathWithoutBase.slice('/app/'.length), appPath }; -} diff --git a/src/legacy/ui/public/url/index.js b/src/legacy/ui/public/url/index.js deleted file mode 100644 index 8ef267de2890c2..00000000000000 --- a/src/legacy/ui/public/url/index.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 { KbnUrlProvider } from './url'; -export { RedirectWhenMissingProvider } from './redirect_when_missing'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { modifyUrl } from '../../../../core/utils'; diff --git a/src/legacy/ui/public/url/kbn_url.ts b/src/legacy/ui/public/url/kbn_url.ts deleted file mode 100644 index 42b6a8f19f9a9d..00000000000000 --- a/src/legacy/ui/public/url/kbn_url.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export interface KbnUrl { - change: (url: string) => void; - removeParam: (param: string) => void; -} diff --git a/src/legacy/ui/public/url/kibana_parsed_url.ts b/src/legacy/ui/public/url/kibana_parsed_url.ts deleted file mode 100644 index 1c60e8729e0ff6..00000000000000 --- a/src/legacy/ui/public/url/kibana_parsed_url.ts +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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 { parse } from 'url'; - -import { modifyUrl } from '../../../../core/public'; -import { prependPath } from './prepend_path'; - -interface Options { - /** - * An optional base path for kibana. If supplied, should start with a "/". - * e.g. in https://localhost:5601/gra/app/visualize#/edit/viz_id the - * basePath is "/gra" - */ - basePath?: string; - - /** - * The app id. - * e.g. in https://localhost:5601/gra/app/visualize#/edit/viz_id the app id is "kibana". - */ - appId: string; - - /** - * The path for a page in the the app. Should start with a "/". Don't include the hash sign. Can - * include all query parameters. - * e.g. in https://localhost:5601/gra/app/visualize#/edit/viz_id?g=state the appPath is - * "/edit/viz_id?g=state" - */ - appPath?: string; - - /** - * Optional hostname. Uses current window location's hostname if hostname, port, - * and protocol are undefined. - */ - hostname?: string; - - /** - * Optional port. Uses current window location's port if hostname, port, - * and protocol are undefined. - */ - port?: string; - - /** - * Optional protocol. Uses current window location's protocol if hostname, port, - * and protocol are undefined. - */ - protocol?: string; -} - -/** - * Represents the pieces that make up a url in Kibana, offering some helpful functionality - * for translating those pieces into absolute or relative urls. A Kibana url with a basePath - * looks like this: http://localhost:5601/basePath/app/appId#/an/appPath?with=query¶ms - * - * - basePath is "/basePath" - * - appId is "appId" - * - appPath is "/an/appPath?with=query¶ms" - * - * Almost all urls in Kibana should have this structure, including the "/app" portion in front of the appId - * (one exception is the login link). - */ -export class KibanaParsedUrl { - public appId: string; - public appPath: string; - public basePath: string; - public hostname?: string; - public protocol?: string; - public port?: string; - - constructor(options: Options) { - const { appId, basePath = '', appPath = '', hostname, protocol, port } = options; - - // We'll use window defaults - const hostOrProtocolSpecified = hostname || protocol || port; - - this.basePath = basePath; - this.appId = appId; - this.appPath = appPath; - this.hostname = hostOrProtocolSpecified ? hostname : window.location.hostname; - this.port = hostOrProtocolSpecified ? port : window.location.port; - this.protocol = hostOrProtocolSpecified ? protocol : window.location.protocol; - } - - public getGlobalState() { - if (!this.appPath) { - return ''; - } - const parsedUrl = parse(this.appPath, true); - const query = parsedUrl.query || {}; - return query._g || ''; - } - - public setGlobalState(newGlobalState: string | string[]) { - if (!this.appPath) { - return; - } - - this.appPath = modifyUrl(this.appPath, (parsed) => { - parsed.query._g = newGlobalState; - }); - } - - public addQueryParameter(name: string, val: string) { - this.appPath = modifyUrl(this.appPath, (parsed) => { - parsed.query[name] = val; - }); - } - - public getHashedAppPath() { - return `#${this.appPath}`; - } - - public getAppBasePath() { - return `/${this.appId}`; - } - - public getAppRootPath() { - return `/app${this.getAppBasePath()}${this.getHashedAppPath()}`; - } - - public getRootRelativePath() { - return prependPath(this.getAppRootPath(), this.basePath); - } - - public getAbsoluteUrl() { - return modifyUrl(this.getRootRelativePath(), (parsed) => { - parsed.protocol = this.protocol; - parsed.port = this.port; - parsed.hostname = this.hostname; - }); - } -} diff --git a/src/legacy/ui/public/url/prepend_path.ts b/src/legacy/ui/public/url/prepend_path.ts deleted file mode 100644 index b8a77d5c23beeb..00000000000000 --- a/src/legacy/ui/public/url/prepend_path.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 { isString } from 'lodash'; -import { format, parse } from 'url'; - -/** - * - * @param {string} relativePath - a relative path that must start with a "/". - * @param {string} newPath - the new path to prefix. ex: 'xyz' - * @return {string} the url with the basePath prepended. ex. '/xyz/app/kibana#/management'. If - * the relative path isn't in the right format (e.g. doesn't start with a "/") the relativePath is returned - * unchanged. - */ -export function prependPath(relativePath: string, newPath = '') { - if (!relativePath || !isString(relativePath)) { - return relativePath; - } - - const parsed = parse(relativePath, true, true); - if (!parsed.host && parsed.pathname) { - if (parsed.pathname[0] === '/') { - parsed.pathname = newPath + parsed.pathname; - } - } - - return format({ - protocol: parsed.protocol, - host: parsed.host, - pathname: parsed.pathname, - query: parsed.query, - hash: parsed.hash, - }); -} diff --git a/src/legacy/ui/public/url/redirect_when_missing.js b/src/legacy/ui/public/url/redirect_when_missing.js deleted file mode 100644 index 85c90a14d9fd78..00000000000000 --- a/src/legacy/ui/public/url/redirect_when_missing.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 React from 'react'; -import { i18n } from '@kbn/i18n'; -import { MarkdownSimple } from '../../../../plugins/kibana_react/public'; -import { toastNotifications } from 'ui/notify'; -import { SavedObjectNotFound } from '../../../../plugins/kibana_utils/public'; -import { uiModules } from '../modules'; - -uiModules.get('kibana/url').service('redirectWhenMissing', function (Private) { - return Private(RedirectWhenMissingProvider); -}); - -export function RedirectWhenMissingProvider(kbnUrl, Promise) { - /** - * Creates an error handler that will redirect to a url when a SavedObjectNotFound - * error is thrown - * - * @param {string|object} mapping - a mapping of url's to redirect to based on the saved object that - * couldn't be found, or just a string that will be used for all types - * @return {function} - the handler to pass to .catch() - */ - return function (mapping) { - if (typeof mapping === 'string') { - mapping = { '*': mapping }; - } - - return function (error) { - // if this error is not "404", rethrow - const savedObjectNotFound = error instanceof SavedObjectNotFound; - const unknownVisType = error.message.indexOf('Invalid type') === 0; - if (unknownVisType) { - error.savedObjectType = 'visualization'; - } else if (!savedObjectNotFound) { - throw error; - } - - let url = mapping[error.savedObjectType] || mapping['*']; - if (!url) url = '/'; - - url += (url.indexOf('?') >= 0 ? '&' : '?') + `notFound=${error.savedObjectType}`; - - toastNotifications.addWarning({ - title: i18n.translate('common.ui.url.savedObjectIsMissingNotificationMessage', { - defaultMessage: 'Saved object is missing', - }), - text: {error.message}, - }); - - kbnUrl.redirect(url); - return Promise.halt(); - }; - }; -} diff --git a/src/legacy/ui/public/url/relative_to_absolute.ts b/src/legacy/ui/public/url/relative_to_absolute.ts deleted file mode 100644 index 7d0737d145a1b2..00000000000000 --- a/src/legacy/ui/public/url/relative_to_absolute.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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. - */ - -/** - * - * @param {string} url - a relative or root relative url. If a relative path is given then the - * absolute url returned will depend on the current page where this function is called from. For example - * if you are on page "http://www.mysite.com/shopping/kids" and you pass this function "adults", you would get - * back "http://www.mysite.com/shopping/adults". If you passed this function a root relative path, or one that - * starts with a "/", for example "/account/cart", you would get back "http://www.mysite.com/account/cart". - * @return {string} the relative url transformed into an absolute url - */ -export function relativeToAbsolute(url: string) { - // convert all link urls to absolute urls - const a = document.createElement('a'); - a.setAttribute('href', url); - return a.href; -} diff --git a/src/legacy/ui/public/url/url.js b/src/legacy/ui/public/url/url.js deleted file mode 100644 index fb243b02e05c7c..00000000000000 --- a/src/legacy/ui/public/url/url.js +++ /dev/null @@ -1,247 +0,0 @@ -/* - * 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 _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { uiModules } from '../modules'; -import { AppStateProvider } from '../state_management/app_state'; - -uiModules.get('kibana/url').service('kbnUrl', function (Private, $injector) { - //config is not directly used but registers global event listeners to kbnUrl to function - $injector.get('config'); - return Private(KbnUrlProvider); -}); - -export function KbnUrlProvider($injector, $location, $rootScope, $parse, Private) { - /** - * the `kbnUrl` service was created to smooth over some of the - * inconsistent behavior that occurs when modifying the url via - * the `$location` api. In general it is recommended that you use - * the `kbnUrl` service any time you want to modify the url. - * - * "features" that `kbnUrl` does it's best to guarantee, which - * are not guaranteed with the `$location` service: - * - calling `kbnUrl.change()` with a url that resolves to the current - * route will force a full transition (rather than just updating the - * properties of the $route object) - * - * Additional features of `kbnUrl` - * - parameterized urls - * - easily include an app state with the url - * - * @type {KbnUrl} - */ - const self = this; - - /** - * Navigate to a url - * - * @param {String} url - the new url, can be a template. See #eval - * @param {Object} [paramObj] - optional set of parameters for the url template - * @return {undefined} - */ - self.change = function (url, paramObj, appState) { - self._changeLocation('url', url, paramObj, false, appState); - }; - - /** - * Same as #change except only changes the url's path, - * leaving the search string and such intact - * - * @param {String} path - the new path, can be a template. See #eval - * @param {Object} [paramObj] - optional set of parameters for the path template - * @return {undefined} - */ - self.changePath = function (path, paramObj) { - self._changeLocation('path', path, paramObj); - }; - - /** - * Same as #change except that it removes the current url from history - * - * @param {String} url - the new url, can be a template. See #eval - * @param {Object} [paramObj] - optional set of parameters for the url template - * @return {undefined} - */ - self.redirect = function (url, paramObj, appState) { - self._changeLocation('url', url, paramObj, true, appState); - }; - - /** - * Same as #redirect except only changes the url's path, - * leaving the search string and such intact - * - * @param {String} path - the new path, can be a template. See #eval - * @param {Object} [paramObj] - optional set of parameters for the path template - * @return {undefined} - */ - self.redirectPath = function (path, paramObj) { - self._changeLocation('path', path, paramObj, true); - }; - - /** - * Evaluate a url template. templates can contain double-curly wrapped - * expressions that are evaluated in the context of the paramObj - * - * @param {String} template - the url template to evaluate - * @param {Object} [paramObj] - the variables to expose to the template - * @return {String} - the evaluated result - * @throws {Error} If any of the expressions can't be parsed. - */ - self.eval = function (template, paramObj) { - paramObj = paramObj || {}; - - return template.replace(/\{\{([^\}]+)\}\}/g, function (match, expr) { - // remove filters - const key = expr.split('|')[0].trim(); - - // verify that the expression can be evaluated - const p = $parse(key)(paramObj); - - // if evaluation can't be made, throw - if (_.isUndefined(p)) { - throw new Error( - i18n.translate('common.ui.url.replacementFailedErrorMessage', { - defaultMessage: 'Replacement failed, unresolved expression: {expr}', - values: { expr }, - }) - ); - } - - return encodeURIComponent($parse(expr)(paramObj)); - }); - }; - - /** - * convert an object's route to an href, compatible with - * window.location.href= and - * - * @param {Object} obj - any object that list's it's routes at obj.routes{} - * @param {string} route - the route name - * @return {string} - the computed href - */ - self.getRouteHref = function (obj, route) { - return '#' + self.getRouteUrl(obj, route); - }; - - /** - * convert an object's route to a url, compatible with url.change() or $location.url() - * - * @param {Object} obj - any object that list's it's routes at obj.routes{} - * @param {string} route - the route name - * @return {string} - the computed url - */ - self.getRouteUrl = function (obj, route) { - const template = obj && obj.routes && obj.routes[route]; - if (template) return self.eval(template, obj); - }; - - /** - * Similar to getRouteUrl, supports objects which list their routes, - * and redirects to the named route. See #redirect - * - * @param {Object} obj - any object that list's it's routes at obj.routes{} - * @param {string} route - the route name - * @return {undefined} - */ - self.redirectToRoute = function (obj, route) { - self.redirect(self.getRouteUrl(obj, route)); - }; - - /** - * Similar to getRouteUrl, supports objects which list their routes, - * and changes the url to the named route. See #change - * - * @param {Object} obj - any object that list's it's routes at obj.routes{} - * @param {string} route - the route name - * @return {undefined} - */ - self.changeToRoute = function (obj, route) { - self.change(self.getRouteUrl(obj, route)); - }; - - /** - * Removes the given parameter from the url. Does so without modifying the browser - * history. - * @param param - */ - self.removeParam = function (param) { - $location.search(param, null).replace(); - }; - - ///// - // private api - ///// - let reloading; - - self._changeLocation = function (type, url, paramObj, replace, appState) { - const prev = { - path: $location.path(), - search: $location.search(), - }; - - url = self.eval(url, paramObj); - $location[type](url); - if (replace) $location.replace(); - - if (appState) { - $location.search(appState.getQueryParamName(), appState.toQueryParam()); - } - - const next = { - path: $location.path(), - search: $location.search(), - }; - - if ($injector.has('$route')) { - const $route = $injector.get('$route'); - - if (self._shouldForceReload(next, prev, $route)) { - const appState = Private(AppStateProvider).getAppState(); - if (appState) appState.destroy(); - - reloading = $rootScope.$on('$locationChangeSuccess', function () { - // call the "unlisten" function returned by $on - reloading(); - reloading = false; - - $route.reload(); - }); - } - } - }; - - // determine if the router will automatically reload the route - self._shouldForceReload = function (next, prev, $route) { - if (reloading) return false; - - const route = $route.current && $route.current.$$route; - if (!route) return false; - - // for the purposes of determining whether the router will - // automatically be reloading, '' and '/' are equal - const nextPath = next.path || '/'; - const prevPath = prev.path || '/'; - if (nextPath !== prevPath) return false; - - const reloadOnSearch = route.reloadOnSearch; - const searchSame = _.isEqual(next.search, prev.search); - return (reloadOnSearch && searchSame) || !reloadOnSearch; - }; -} diff --git a/src/legacy/ui/public/utils/collection.test.ts b/src/legacy/ui/public/utils/collection.test.ts deleted file mode 100644 index 0841e3554c0d09..00000000000000 --- a/src/legacy/ui/public/utils/collection.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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 { move } from './collection'; - -describe('collection', () => { - describe('move', () => { - test('accepts previous from->to syntax', () => { - const list = [1, 1, 1, 1, 1, 1, 1, 1, 8, 1, 1]; - - expect(list[3]).toBe(1); - expect(list[8]).toBe(8); - - move(list, 8, 3); - - expect(list[8]).toBe(1); - expect(list[3]).toBe(8); - }); - - test('moves an object up based on a function callback', () => { - const list = [1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1]; - - expect(list[4]).toBe(0); - expect(list[5]).toBe(1); - expect(list[6]).toBe(0); - - move(list, 5, false, (v: any) => v === 0); - - expect(list[4]).toBe(1); - expect(list[5]).toBe(0); - expect(list[6]).toBe(0); - }); - - test('moves an object down based on a function callback', () => { - const list = [1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1]; - - expect(list[4]).toBe(0); - expect(list[5]).toBe(1); - expect(list[6]).toBe(0); - - move(list, 5, true, (v: any) => v === 0); - - expect(list[4]).toBe(0); - expect(list[5]).toBe(0); - expect(list[6]).toBe(1); - }); - - test('moves an object up based on a where callback', () => { - const list = [ - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 0 }, - { v: 1 }, - { v: 0 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - ]; - - expect(list[4]).toHaveProperty('v', 0); - expect(list[5]).toHaveProperty('v', 1); - expect(list[6]).toHaveProperty('v', 0); - - move(list, 5, false, { v: 0 }); - - expect(list[4]).toHaveProperty('v', 1); - expect(list[5]).toHaveProperty('v', 0); - expect(list[6]).toHaveProperty('v', 0); - }); - - test('moves an object down based on a where callback', () => { - const list = [ - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 0 }, - { v: 1 }, - { v: 0 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - ]; - - expect(list[4]).toHaveProperty('v', 0); - expect(list[5]).toHaveProperty('v', 1); - expect(list[6]).toHaveProperty('v', 0); - - move(list, 5, true, { v: 0 }); - - expect(list[4]).toHaveProperty('v', 0); - expect(list[5]).toHaveProperty('v', 0); - expect(list[6]).toHaveProperty('v', 1); - }); - - test('moves an object down based on a pluck callback', () => { - const list = [ - { id: 0, normal: true }, - { id: 1, normal: true }, - { id: 2, normal: true }, - { id: 3, normal: true }, - { id: 4, normal: true }, - { id: 5, normal: false }, - { id: 6, normal: true }, - { id: 7, normal: true }, - { id: 8, normal: true }, - { id: 9, normal: true }, - ]; - - expect(list[4]).toHaveProperty('id', 4); - expect(list[5]).toHaveProperty('id', 5); - expect(list[6]).toHaveProperty('id', 6); - - move(list, 5, true, 'normal'); - - expect(list[4]).toHaveProperty('id', 4); - expect(list[5]).toHaveProperty('id', 6); - expect(list[6]).toHaveProperty('id', 5); - }); - }); -}); diff --git a/src/legacy/ui/public/utils/collection.ts b/src/legacy/ui/public/utils/collection.ts deleted file mode 100644 index b882a2bbe6e5b9..00000000000000 --- a/src/legacy/ui/public/utils/collection.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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 _ from 'lodash'; - -/** - * move an obj either up or down in the collection by - * injecting it either before/after the prev/next obj that - * satisfied the qualifier - * - * or, just from one index to another... - * - * @param {array} objs - the list to move the object within - * @param {number|any} obj - the object that should be moved, or the index that the object is currently at - * @param {number|boolean} below - the index to move the object to, or whether it should be moved up or down - * @param {function} qualifier - a lodash-y callback, object = _.where, string = _.pluck - * @return {array} - the objs argument - */ -export function move( - objs: any[], - obj: object | number, - below: number | boolean, - qualifier?: ((object: object, index: number) => any) | Record | string -): object[] { - const origI = _.isNumber(obj) ? obj : objs.indexOf(obj); - if (origI === -1) { - return objs; - } - - if (_.isNumber(below)) { - // move to a specific index - objs.splice(below, 0, objs.splice(origI, 1)[0]); - return objs; - } - - below = !!below; - qualifier = qualifier && _.iteratee(qualifier); - - const above = !below; - const finder = below ? _.findIndex : _.findLastIndex; - - // find the index of the next/previous obj that meets the qualifications - const targetI = finder(objs, (otherAgg, otherI) => { - if (below && otherI <= origI) { - return; - } - if (above && otherI >= origI) { - return; - } - return Boolean(_.isFunction(qualifier) && qualifier(otherAgg, otherI)); - }); - - if (targetI === -1) { - return objs; - } - - // place the obj at it's new index - objs.splice(targetI, 0, objs.splice(origI, 1)[0]); - return objs; -} diff --git a/src/legacy/ui/public/utils/legacy_class.js b/src/legacy/ui/public/utils/legacy_class.js deleted file mode 100644 index f47650a77bb6d4..00000000000000 --- a/src/legacy/ui/public/utils/legacy_class.js +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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. - */ - -// create a property descriptor for properties -// that won't change -function describeConst(val) { - return { - writable: false, - enumerable: false, - configurable: false, - value: val, - }; -} - -const props = { - inherits: describeConst(function (SuperClass) { - const prototype = Object.create(SuperClass.prototype, { - constructor: describeConst(this), - superConstructor: describeConst(SuperClass), - }); - - Object.defineProperties(this, { - prototype: describeConst(prototype), - Super: describeConst(SuperClass), - }); - - return this; - }), -}; - -/** - * Add class-related behavior to a function, currently this - * only attaches an .inherits() method. - * - * @param {Constructor} ClassConstructor - The function that should be extended - * @return {Constructor} - the constructor passed in; - */ -export function createLegacyClass(ClassConstructor) { - return Object.defineProperties(ClassConstructor, props); -} diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx index 6103041cf0a4c8..68a21c6a1587c4 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx @@ -32,10 +32,6 @@ import { AdvancedSettingsComponent } from './advanced_settings'; import { notificationServiceMock, docLinksServiceMock } from '../../../../core/public/mocks'; import { ComponentRegistry } from '../component_registry'; -jest.mock('ui/new_platform', () => ({ - npStart: mockConfig(), -})); - jest.mock('./components/field', () => ({ Field: () => { return 'field'; diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts index 51f4fc7ce94b90..5e0eeaee3c0d09 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts @@ -22,5 +22,3 @@ import { setup } from 'test_utils/http_test_setup'; export const { http } = setup((injectedMetadata) => { injectedMetadata.getBasePath.mockReturnValue('/hola/daro/'); }); - -jest.doMock('ui/new_platform', () => ({ npSetup: { core: { http } } })); diff --git a/src/plugins/discover/public/application/components/field_name/field_name.test.tsx b/src/plugins/discover/public/application/components/field_name/field_name.test.tsx index 46be1044c0d4d1..e6cf8a57686f14 100644 --- a/src/plugins/discover/public/application/components/field_name/field_name.test.tsx +++ b/src/plugins/discover/public/application/components/field_name/field_name.test.tsx @@ -20,8 +20,6 @@ import React from 'react'; import { render } from 'enzyme'; import { FieldName } from './field_name'; -jest.mock('ui/new_platform'); - // Note that it currently provides just 2 basic tests, there should be more, but // the components involved will soon change test('FieldName renders a string field by providing fieldType and fieldName', () => { diff --git a/src/plugins/es_ui_shared/public/indices/validate/validate_index.test.ts b/src/plugins/es_ui_shared/public/indices/validate/validate_index.test.ts index a6792543cd7267..b28cac1cb76f5f 100644 --- a/src/plugins/es_ui_shared/public/indices/validate/validate_index.test.ts +++ b/src/plugins/es_ui_shared/public/indices/validate/validate_index.test.ts @@ -17,8 +17,6 @@ * under the License. */ -jest.mock('ui/new_platform'); - import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from '../constants'; import { diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx index 80132167b7f58f..ed50317aed6a0e 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx @@ -46,14 +46,6 @@ jest.mock('./components/table', () => ({ }, })); -jest.mock('ui/documentation_links', () => ({ - documentationLinks: { - scriptedFields: { - painless: 'painlessDocs', - }, - }, -})); - const helpers = { redirectToRoute: () => {}, getRouteHref: () => '#', diff --git a/src/plugins/timelion/public/directives/_saved_object_finder.scss b/src/plugins/timelion/public/directives/_saved_object_finder.scss index b97dace5e9e000..e1a055a5f49e99 100644 --- a/src/plugins/timelion/public/directives/_saved_object_finder.scss +++ b/src/plugins/timelion/public/directives/_saved_object_finder.scss @@ -23,3 +23,74 @@ } } } + + +saved-object-finder { + + .list-sort-button { + border-top-left-radius: 0; + border-top-right-radius: 0; + border: none; + padding: $euiSizeS $euiSize; + font-weight: $euiFontWeightRegular; + background-color: $euiColorLightestShade; + } + + .li-striped { + li { + border: none; + } + + li:nth-child(even) { + background-color: $euiColorLightestShade; + } + + li:nth-child(odd) { + background-color: $euiColorEmptyShade; + } + + .paginate-heading { + font-weight: $euiFontWeightRegular; + color: $euiColorDarkestShade; + } + + .list-group-item { + padding: $euiSizeS $euiSize; + + ul { + padding: 0; + display: flex; + flex-direction: row; + + .finder-type { + margin-right: $euiSizeS; + } + } + + a { + display: block; + color: $euiColorPrimary; + + i { + color: shade($euiColorPrimary, 10%); + margin-right: $euiSizeS; + } + } + + &:first-child { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + &.list-group-no-results p { + margin-bottom: 0; + } + } + } + + paginate { + paginate-controls { + margin: $euiSize; + } + } +} diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.test.js index cfbd5ecb7bf655..068e3fea8b768a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.test.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.test.js @@ -19,8 +19,6 @@ import { getAxisLabelString } from './get_axis_label_string'; -jest.mock('ui/new_platform'); - describe('getAxisLabelString(interval)', () => { test('should return a valid label for 10 seconds', () => { expect(getAxisLabelString(10000)).toEqual('per 10 seconds'); diff --git a/src/test_utils/public/no_digest_promises.js b/src/test_utils/public/no_digest_promises.js deleted file mode 100644 index 396d6fb4892f31..00000000000000 --- a/src/test_utils/public/no_digest_promises.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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 Bluebird from 'bluebird'; -import 'ui/promises'; -import { uiModules } from 'ui/modules'; - -Bluebird.longStackTraces(); - -/** - * replace the Promise service with Bluebird so that tests - * can use promises without having to call $rootScope.apply() - * - * import noDigestPromises from 'test_utils/no_digest_promises'; - * - * describe('some module that does complex shit with promises', function () { - * beforeEach(noDigestPromises.activate); - * - * }); - */ - -let active = false; - -uiModules.get('kibana').config(function ($provide) { - $provide.decorator('Promise', function ($delegate) { - return active ? Bluebird : $delegate; - }); -}); - -function activate() { - active = true; -} -function deactivate() { - active = false; -} - -export default { - activate: activate, - deactivate: deactivate, - activateForSuite: function () { - before(activate); - after(deactivate); - }, -}; diff --git a/src/test_utils/public/stub_get_active_injector.js b/src/test_utils/public/stub_get_active_injector.js deleted file mode 100644 index fb72cc5dabb7ee..00000000000000 --- a/src/test_utils/public/stub_get_active_injector.js +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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. - */ - -/** - * This test file contains stubs for chrome.dangerouslyGetActiveInjector, that you will - * need to load if any part of the code you are testing relies on that method. - * You will need to call setupInjectorStub and teardownInjectorStub in specific - * places inside your test file to setup and teardown the stub. - * If you can call both of them at the same place you can also use the shortcut - * setupAndTeardownInjectorStub instead. - */ - -import ngMock from 'ng_mock'; - -import chrome from 'ui/chrome'; -import sinon from 'sinon'; - -/** - * This method setups the stub for chrome.dangerouslyGetActiveInjector. You must call it in - * a place where beforeEach is allowed to be called (read: inside your describe) - * method. You must call this AFTER you've called `ngMock.module` to setup the modules, - * but BEFORE you first execute code, that uses chrome.dangerouslyGetActiveInjector. - */ -export function setupInjectorStub() { - beforeEach( - ngMock.inject(($injector) => { - sinon.stub(chrome, 'dangerouslyGetActiveInjector').returns(Promise.resolve($injector)); - }) - ); -} - -/** - * This methods tears down the stub for chrome.dangerouslyGetActiveInjector. You must call it - * in a place where afterEach is allowed to be called. - */ -export function teardownInjectorStub() { - afterEach(() => { - chrome.dangerouslyGetActiveInjector.restore(); - }); -} - -/** - * This method combines setupInjectorStub and teardownInjectorStub in one method. - * It can be used if you can call the other two methods directly behind each other. - */ -export function setupAndTeardownInjectorStub() { - setupInjectorStub(); - teardownInjectorStub(); -} diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_vis.js b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_vis.js deleted file mode 100644 index 7aa12ea7a1130b..00000000000000 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_vis.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 { SelfChangingEditor } from './self_changing_editor'; -import { SelfChangingComponent } from './self_changing_components'; - -import { npSetup } from '../../../../../../src/legacy/ui/public/new_platform'; - -npSetup.plugins.visualizations.createReactVisualization({ - name: 'self_changing_vis', - title: 'Self Changing Vis', - icon: 'controlsHorizontal', - description: - 'This visualization is able to change its own settings, that you could also set in the editor.', - visConfig: { - component: SelfChangingComponent, - defaults: { - counter: 0, - }, - }, - editorConfig: { - optionTabs: [ - { - name: 'options', - title: 'Options', - editor: SelfChangingEditor, - }, - ], - }, - requestHandler: 'none', -}); diff --git a/tsconfig.json b/tsconfig.json index 66906fb18bb80a..819cb5dc4c1e3c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,9 +7,6 @@ "kibana/public": ["src/core/public"], "kibana/server": ["src/core/server"], "plugins/*": ["src/legacy/core_plugins/*/public/"], - "ui/*": [ - "src/legacy/ui/public/*" - ], "test_utils/*": [ "src/test_utils/public/*" ], diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index a0574dbdf36da6..a693e008db6ea0 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -13,7 +13,6 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector moduleNameMapper: { '@elastic/eui$': `${kibanaDirectory}/node_modules/@elastic/eui/test-env`, '@elastic/eui/lib/(.*)?': `${kibanaDirectory}/node_modules/@elastic/eui/test-env/$1`, - '^ui/(.*)': `${kibanaDirectory}/src/legacy/ui/public/$1`, '^fixtures/(.*)': `${kibanaDirectory}/src/fixtures/$1`, 'uiExports/(.*)': fileMockPath, '^src/core/(.*)': `${kibanaDirectory}/src/core/$1`, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts index 882d1e2ea58b9a..33260b5c9303fb 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); + import { savedLens } from './saved_lens'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts index 74e41a030de35d..b2de0b6526d1e7 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); + import { savedMap } from './saved_map'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts index 9bd32202b563a0..7d5952754aa908 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); + import { savedSearch } from './saved_search'; import { buildEmbeddableFilters } from '../../../public/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts index 8327c1433b9af4..a64fb167dd19f0 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); + import { savedVisualization } from './saved_visualization'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/common/lib/autocomplete.test.ts b/x-pack/plugins/canvas/common/lib/autocomplete.test.ts index f7a773f6ca36a2..777810cad05ba6 100644 --- a/x-pack/plugins/canvas/common/lib/autocomplete.test.ts +++ b/x-pack/plugins/canvas/common/lib/autocomplete.test.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); import { functionSpecs } from '../../__tests__/fixtures/function_specs'; import { diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.test.tsx b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.test.tsx index e8b772e0f2fbd8..b037d30ada12f6 100644 --- a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.test.tsx +++ b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.test.tsx @@ -14,7 +14,6 @@ import { import { METRIC_TYPE } from '../../../lib/ui_metric'; import { ResolvedArgType } from '../../../../types'; -jest.mock('ui/new_platform'); const trackMetric = jest.fn(); const Component = withUnconnectedElementsLoadedTelemetry(() =>

, trackMetric); diff --git a/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts b/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts index bf69a862d5c300..e451a39df06db4 100644 --- a/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts +++ b/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); // actions/elements has some dependencies on ui/new_platform. jest.mock('../../../lib/app_state'); import { workpadRefresh } from '../workpad_refresh'; diff --git a/x-pack/plugins/canvas/public/state/reducers/embeddables.test.ts b/x-pack/plugins/canvas/public/state/reducers/embeddables.test.ts index 5b1192630897a2..08b59e5fedc440 100644 --- a/x-pack/plugins/canvas/public/state/reducers/embeddables.test.ts +++ b/x-pack/plugins/canvas/public/state/reducers/embeddables.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); + import { State } from '../../../types'; import { updateEmbeddableExpression } from '../actions/embeddable'; import { embeddableReducer } from './embeddable'; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js index eda275ba50c1a6..cdb107016e4659 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js @@ -11,8 +11,6 @@ jest.mock('../services/auto_follow_pattern_validators', () => ({ validateLeaderIndexPattern: jest.fn(), })); -jest.mock('ui/new_platform'); - describe(' { describe('updateFormErrors()', () => { it('should merge errors with existing fieldsErrors', () => { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js index 93da20a8ed93c6..72c4832a631a7c 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js @@ -6,8 +6,6 @@ import { updateFields, updateFormErrors } from './follower_index_form'; -jest.mock('ui/new_platform'); - describe(' state transitions', () => { it('updateFormErrors() should merge errors with existing fieldsErrors', () => { const errors = { name: 'Some error' }; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js index 11ec125c17d59c..924bbe708c73a4 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js @@ -6,8 +6,6 @@ import { validateAutoFollowPattern } from './auto_follow_pattern_validators'; -jest.mock('ui/new_platform'); - describe('Auto-follow pattern validators', () => { describe('validateAutoFollowPattern()', () => { it('returns empty object when autoFollowPattern is undefined', () => { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js index 01dccf70a21d6e..a3655df1f89566 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js @@ -8,7 +8,6 @@ import { reducer, initialState } from './api'; import { API_STATUS } from '../../constants'; import { apiRequestStart, apiRequestEnd, setApiError } from '../actions'; -jest.mock('ui/new_platform'); jest.mock('../../constants', () => ({ API_STATUS: { IDLE: 'idle', diff --git a/x-pack/plugins/graph/public/components/search_bar.test.tsx b/x-pack/plugins/graph/public/components/search_bar.test.tsx index 100122af943e18..1b783df1b7d026 100644 --- a/x-pack/plugins/graph/public/components/search_bar.test.tsx +++ b/x-pack/plugins/graph/public/components/search_bar.test.tsx @@ -21,7 +21,6 @@ import { ReactWrapper } from 'enzyme'; import { createMockGraphStore } from '../state_management/mocks'; import { Provider } from 'react-redux'; -jest.mock('ui/new_platform'); jest.mock('../services/source_modal', () => ({ openSourceModal: jest.fn() })); const waitForIndexPatternFetch = () => new Promise((r) => setTimeout(r)); diff --git a/x-pack/plugins/graph/public/state_management/mocks.ts b/x-pack/plugins/graph/public/state_management/mocks.ts index d32bc9a175a47c..f28f51544a6ede 100644 --- a/x-pack/plugins/graph/public/state_management/mocks.ts +++ b/x-pack/plugins/graph/public/state_management/mocks.ts @@ -17,8 +17,6 @@ import { GraphStoreDependencies, createRootReducer, GraphStore, GraphState } fro import { Workspace, GraphWorkspaceSavedObject, IndexPatternSavedObject } from '../types'; import { IndexPattern } from '../../../../../src/plugins/data/public'; -jest.mock('ui/new_platform'); - export interface MockedGraphEnvironment { store: GraphStore; mockedDeps: jest.Mocked; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts index 7c7c0b70c0eedf..81eb1c8cad1358 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts @@ -14,7 +14,6 @@ import { } from '../constants/'; import { getUiMetricsForPhases } from './ui_metric'; -jest.mock('ui/new_platform'); describe('getUiMetricsForPhases', () => { test('gets cold phase', () => { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts index a112d73230b82b..2a0585d61d6f6d 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts @@ -12,11 +12,6 @@ import { ComponentTemplateDeserialized } from '../../shared_imports'; const { setup } = pageHelpers.componentTemplateDetails; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - const COMPONENT_TEMPLATE: ComponentTemplateDeserialized = { name: 'comp-1', template: { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts index bd6ac273758363..9bf7df263ec26c 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts @@ -14,11 +14,6 @@ import { API_BASE_PATH } from './helpers/constants'; const { setup } = pageHelpers.componentTemplateList; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: ComponentTemplateListTestBed; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts index 8d6a83a625651a..c0acc39ca35a1c 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts @@ -13,11 +13,6 @@ import { PipelineListTestBed } from './helpers/pipelines_list.helpers'; const { setup } = pageHelpers.pipelinesList; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: PipelineListTestBed; diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx index 52524d0c9a5fae..c5d6ced76b5c01 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx @@ -6,7 +6,6 @@ import { EMSFileSource } from './ems_file_source'; -jest.mock('ui/new_platform'); jest.mock('../../layers/vector_layer/vector_layer', () => {}); function makeEMSFileSource(tooltipProperties: string[]) { diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts index 87abbedfdf50e2..cf0170ab7f1bdd 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts @@ -11,8 +11,6 @@ import _ from 'lodash'; import { AGG_TYPE } from '../../../../common/constants'; import { AggDescriptor } from '../../../../common/descriptor_types'; -jest.mock('ui/new_platform'); - const sumFieldName = 'myFieldGettingSummed'; const metricExamples = [ { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts index 37193e148bdc7f..43bfb74bf54b68 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts @@ -6,7 +6,6 @@ import { MapExtent, MapFilters } from '../../../../common/descriptor_types'; jest.mock('../../../kibana_services'); -jest.mock('ui/new_platform'); import { getIndexPatternService, getSearchService } from '../../../kibana_services'; import { ESGeoGridSource } from './es_geo_grid_source'; diff --git a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js index f6779206868a59..060096157f5781 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js +++ b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js @@ -6,7 +6,6 @@ import { ESTermSource, extractPropertiesMap } from './es_term_source'; -jest.mock('ui/new_platform'); jest.mock('../../layers/vector_layer/vector_layer', () => {}); const indexPatternTitle = 'myIndex'; diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.tsx b/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.tsx index aa4dbc67e8e4de..f082e67512099a 100644 --- a/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.tsx @@ -9,8 +9,6 @@ import { shallow } from 'enzyme'; import { HeatmapStyleEditor } from './heatmap_style_editor'; -jest.mock('ui/new_platform'); - describe('HeatmapStyleEditor', () => { test('is rendered', () => { const component = shallow( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx index 62a6a59dd091ba..c3610cbc78e15f 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); jest.mock('../components/vector_style_editor', () => ({ VectorStyleEditor: () => { return
mockVectorStyleEditor
; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx index af93c8e0c9d6db..06987ab8bcc48f 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx @@ -6,7 +6,6 @@ import { shallow } from 'enzyme'; -jest.mock('ui/new_platform'); jest.mock('../components/vector_style_editor', () => ({ VectorStyleEditor: () => { return
mockVectorStyleEditor
; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx index db44ae0da562d4..c5298067f6cbe7 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); jest.mock('../components/vector_style_editor', () => ({ VectorStyleEditor: () => { return
mockVectorStyleEditor
; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js index a85cd0cc864075..28801a402ca14d 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js @@ -14,7 +14,6 @@ import { } from '../../../../common/constants'; jest.mock('../../../kibana_services'); -jest.mock('ui/new_platform'); class MockField { constructor({ fieldName }) { diff --git a/x-pack/plugins/security/server/authorization/authorization_service.ts b/x-pack/plugins/security/server/authorization/authorization_service.ts index 4190499cbd5f43..2dead301b298a4 100644 --- a/x-pack/plugins/security/server/authorization/authorization_service.ts +++ b/x-pack/plugins/security/server/authorization/authorization_service.ts @@ -5,7 +5,7 @@ */ import { Subscription, Observable } from 'rxjs'; -import { UICapabilities } from 'ui/capabilities'; +import type { Capabilities as UICapabilities } from '../../../../../src/core/types'; import { LoggerFactory, KibanaRequest, 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 c126be1b07f6e3..41d596d570fb97 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts @@ -5,7 +5,7 @@ */ import { flatten, isObject, mapValues } from 'lodash'; -import { UICapabilities } from 'ui/capabilities'; +import type { Capabilities as UICapabilities } from '../../../../../src/core/types'; import { KibanaRequest, Logger } from '../../../../../src/core/server'; import { Feature } from '../../../features/server'; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts index 0abc47686a6b4b..6021ab2a42c90e 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts @@ -20,13 +20,6 @@ import { REPOSITORY_NAME } from './helpers/constant'; const { setup } = pageHelpers.home; -jest.mock('ui/new_platform'); - -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - // Mocking FormattedDate and FormattedTime due to timezone differences on CI jest.mock('@kbn/i18n/react', () => { const original = jest.requireActual('@kbn/i18n/react'); diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts index f480e937f162a8..dc568161d4fb4f 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts @@ -17,11 +17,6 @@ import { DEFAULT_POLICY_SCHEDULE } from '../../public/application/constants'; const { setup } = pageHelpers.policyAdd; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - // mock for EuiSelectable's virtualization jest.mock('react-virtualized-auto-sizer', () => { return ({ diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts index 7eec80890ca86a..a5b2ec73b85cd1 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts @@ -18,11 +18,6 @@ const { setup: setupPolicyAdd } = pageHelpers.policyAdd; const EXPIRE_AFTER_VALUE = '5'; const EXPIRE_AFTER_UNIT = TIME_UNITS.MINUTE; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - describe('', () => { let testBed: PolicyFormTestBed; let testBedPolicyAdd: PolicyFormTestBed; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_add.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_add.test.ts index 2a9e17a1c9c4dd..e0c9aeffa09e8d 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_add.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_add.test.ts @@ -11,16 +11,9 @@ import { RepositoryType } from '../../common/types'; import { setupEnvironment, pageHelpers, nextTick } from './helpers'; import { RepositoryAddTestBed } from './helpers/repository_add.helpers'; -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.repositoryAdd; const repositoryTypes = ['fs', 'url', 'source', 'azure', 'gcs', 's3', 'hdfs']; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - describe('', () => { let testBed: RepositoryAddTestBed; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_edit.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_edit.test.ts index bab276584966bb..1606db07b57b47 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_edit.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_edit.test.ts @@ -12,16 +12,9 @@ import { RepositoryEditTestSubjects } from './helpers/repository_edit.helpers'; import { RepositoryAddTestSubjects } from './helpers/repository_add.helpers'; import { REPOSITORY_EDIT } from './helpers/constant'; -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.repositoryEdit; const { setup: setupRepositoryAdd } = pageHelpers.repositoryAdd; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - describe('', () => { let testBed: TestBed; let testBedRepositoryAdd: TestBed; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 83dac4912d4ac1..89a020bd044cc3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -253,31 +253,6 @@ "charts.controls.rangeErrorMessage": "値は{min}と{max}の間でなければなりません", "charts.controls.vislibBasicOptions.legendPositionLabel": "凡例位置", "charts.controls.vislibBasicOptions.showTooltipLabel": "ツールヒントを表示", - "common.ui.flotCharts.aprLabel": "4 月", - "common.ui.flotCharts.augLabel": "8 月", - "common.ui.flotCharts.decLabel": "12 月", - "common.ui.flotCharts.febLabel": "2 月", - "common.ui.flotCharts.friLabel": "金", - "common.ui.flotCharts.janLabel": "1 月", - "common.ui.flotCharts.julLabel": "7 月", - "common.ui.flotCharts.junLabel": "6 月", - "common.ui.flotCharts.marLabel": "3 月", - "common.ui.flotCharts.mayLabel": "5 月", - "common.ui.flotCharts.monLabel": "月", - "common.ui.flotCharts.novLabel": "11 月", - "common.ui.flotCharts.octLabel": "10 月", - "common.ui.flotCharts.pie.unableToDrawLabelsInsideCanvasErrorMessage": "キャンバス内のラベルではパイを作成できません", - "common.ui.flotCharts.satLabel": "土", - "common.ui.flotCharts.sepLabel": "9 月", - "common.ui.flotCharts.sunLabel": "日", - "common.ui.flotCharts.thuLabel": "木", - "common.ui.flotCharts.tueLabel": "火", - "common.ui.flotCharts.wedLabel": "水", - "common.ui.stateManagement.unableToParseUrlErrorMessage": "URL をパースできません", - "common.ui.stateManagement.unableToRestoreUrlErrorMessage": "URL を完全に復元できません。共有機能を使用していることを確認してください。", - "common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage": "セッションがいっぱいで安全に削除できるアイテムが見つからないため、Kibana は履歴アイテムを保存できません。\n\nこれは大抵新規タブに移動することで解決されますが、より大きな問題が原因である可能性もあります。このメッセージが定期的に表示される場合は、{gitHubIssuesUrl} で問題を報告してください。", - "common.ui.url.replacementFailedErrorMessage": "置換に失敗、未解決の表現式: {expr}", - "common.ui.url.savedObjectIsMissingNotificationMessage": "保存されたオブジェクトがありません", "console.autocomplete.addMethodMetaText": "メソド", "console.consoleDisplayName": "コンソール", "console.consoleMenu.copyAsCurlMessage": "リクエストが URL としてコピーされました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8b3a1941f450a4..68c1b5dab92955 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -253,31 +253,6 @@ "charts.controls.rangeErrorMessage": "值必须是在 {min} 到 {max} 的范围内", "charts.controls.vislibBasicOptions.legendPositionLabel": "图例位置", "charts.controls.vislibBasicOptions.showTooltipLabel": "显示工具提示", - "common.ui.flotCharts.aprLabel": "四月", - "common.ui.flotCharts.augLabel": "八月", - "common.ui.flotCharts.decLabel": "十二月", - "common.ui.flotCharts.febLabel": "二月", - "common.ui.flotCharts.friLabel": "周五", - "common.ui.flotCharts.janLabel": "一月", - "common.ui.flotCharts.julLabel": "七月", - "common.ui.flotCharts.junLabel": "六月", - "common.ui.flotCharts.marLabel": "三月", - "common.ui.flotCharts.mayLabel": "五月", - "common.ui.flotCharts.monLabel": "周一", - "common.ui.flotCharts.novLabel": "十一月", - "common.ui.flotCharts.octLabel": "十月", - "common.ui.flotCharts.pie.unableToDrawLabelsInsideCanvasErrorMessage": "无法用画布内包含的标签绘制饼图", - "common.ui.flotCharts.satLabel": "周六", - "common.ui.flotCharts.sepLabel": "九月", - "common.ui.flotCharts.sunLabel": "周日", - "common.ui.flotCharts.thuLabel": "周四", - "common.ui.flotCharts.tueLabel": "周二", - "common.ui.flotCharts.wedLabel": "周三", - "common.ui.stateManagement.unableToParseUrlErrorMessage": "无法解析 URL", - "common.ui.stateManagement.unableToRestoreUrlErrorMessage": "无法完整还原 URL,确保使用共享功能。", - "common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage": "Kibana 无法将历史记录项存储在您的会话中,因为其已满,并且似乎没有任何可安全删除的项。\n\n通常可通过移至新的标签页来解决此问题,但这会导致更大的问题。如果您有规律地看到此消息,请在 {gitHubIssuesUrl} 提交问题。", - "common.ui.url.replacementFailedErrorMessage": "替换失败,未解析的表达式:{expr}", - "common.ui.url.savedObjectIsMissingNotificationMessage": "已保存对象缺失", "console.autocomplete.addMethodMetaText": "方法", "console.consoleDisplayName": "控制台", "console.consoleMenu.copyAsCurlMessage": "请求已复制为 cURL", diff --git a/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts b/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts index 5f2e9d07c4bcba..9fb3a54d5a1825 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts @@ -10,8 +10,6 @@ import { HelloWorldEmbeddable } from '../../../../examples/embeddable_examples/p /** eslint-enable */ import { TimeRangeEmbeddable, TimeRangeContainer } from './test_helpers'; -jest.mock('ui/new_platform'); - test('canInheritTimeRange returns false if embeddable is inside container without a time range', () => { const embeddable = new TimeRangeEmbeddable( { id: '1234', timeRange: { from: 'noxw-15m', to: 'now' } }, diff --git a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts index 7919d2d2482ae6..9af79c9ac52597 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts @@ -21,8 +21,6 @@ import { import { nextTick } from 'test_utils/enzyme_helpers'; import { ReactElement } from 'react'; -jest.mock('ui/new_platform'); - const createOpenModalMock = () => { const mock = jest.fn(); mock.mockReturnValue({ close: jest.fn() }); diff --git a/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts b/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts index 7f831973aea5c1..e567828598b317 100644 --- a/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts +++ b/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import axios, { AxiosInstance } from 'axios'; -import { UICapabilities } from 'ui/capabilities'; +import type { Capabilities as UICapabilities } from 'src/core/types'; import { format as formatUrl } from 'url'; import util from 'util'; import { ToolingLog } from '@kbn/dev-utils'; diff --git a/x-pack/test_utils/jest/config.js b/x-pack/test_utils/jest/config.js index 7bb073023b7f8b..c94fe02d2f4bdb 100644 --- a/x-pack/test_utils/jest/config.js +++ b/x-pack/test_utils/jest/config.js @@ -17,7 +17,6 @@ export default { ], collectCoverageFrom: ['legacy/plugins/**/*.js', 'legacy/common/**/*.js', 'legacy/server/**/*.js'], moduleNameMapper: { - '^ui/(.*)': '**/public/$1', '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/src/dev/jest/mocks/file_mock.js', '\\.(css|less|scss)$': '/../src/dev/jest/mocks/style_mock.js', diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index e978702a356342..35e1800c6fbd19 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -22,9 +22,6 @@ "paths": { "kibana/public": ["src/core/public"], "kibana/server": ["src/core/server"], - "ui/*": [ - "src/legacy/ui/public/*" - ], "plugins/xpack_main/*": [ "x-pack/legacy/plugins/xpack_main/public/*" ], From 69461cc6184e661b0a2d6196b1082d63629ed6f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Tue, 1 Sep 2020 10:10:01 +0200 Subject: [PATCH 134/216] [APM UI] Rephrase BDD steps for RUM scenarios (#72492) * chore: group tests doing the same This will reduce the time it takes to execute the tests * chore: rephrase step * chore: extract common code to a function Co-authored-by: Elastic Machine --- .../rum_dashboard.feature.disabled | 14 ++----- .../rum/client_metrics_helper.ts | 37 +++++++++++++++++++ .../step_definitions/rum/page_load_dist.ts | 2 +- .../step_definitions/rum/rum_dashboard.ts | 15 ++------ .../rum/service_name_filter.ts | 13 ++----- 5 files changed, 48 insertions(+), 33 deletions(-) create mode 100644 x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/client_metrics_helper.ts diff --git a/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature.disabled b/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature.disabled index be1597c8340ebe..727898773904e1 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature.disabled +++ b/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature.disabled @@ -12,20 +12,14 @@ Feature: RUM Dashboard | os | | location | - Scenario: Page load distribution percentiles + Scenario: Display RUM Data components When a user browses the APM UI application for RUM Data Then should display percentile for page load chart - - Scenario: Page load distribution chart tooltip - When a user browses the APM UI application for RUM Data - Then should display tooltip on hover - - Scenario: Page load distribution chart legends - When a user browses the APM UI application for RUM Data - Then should display chart legend + And should display tooltip on hover + And should display chart legend Scenario: Breakdown filter - Given a user click page load breakdown filter + Given a user clicks the page load breakdown filter When the user selected the breakdown Then breakdown series should appear in chart diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/client_metrics_helper.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/client_metrics_helper.ts new file mode 100644 index 00000000000000..1cc36059b8ff8e --- /dev/null +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/client_metrics_helper.ts @@ -0,0 +1,37 @@ +/* + * 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 { DEFAULT_TIMEOUT } from './rum_dashboard'; + +/** + * Verifies the behavior of the client metrics component + * @param metrics array of three elements + * @param checkTitleStatus if it's needed to check title elements + */ +export function verifyClientMetrics( + metrics: string[], + checkTitleStatus: boolean +) { + const clientMetricsSelector = '[data-cy=client-metrics] .euiStat__title'; + + // wait for all loading to finish + cy.get('kbnLoadingIndicator').should('not.be.visible'); + + if (checkTitleStatus) { + cy.get('.euiStat__title', { timeout: DEFAULT_TIMEOUT }).should( + 'be.visible' + ); + cy.get('.euiSelect-isLoading').should('not.be.visible'); + } + + cy.get('.euiStat__title-isLoading').should('not.be.visible'); + + cy.get(clientMetricsSelector).eq(0).should('have.text', metrics[0]); + + cy.get(clientMetricsSelector).eq(1).should('have.text', metrics[1]); + + cy.get(clientMetricsSelector).eq(2).should('have.text', metrics[2]); +} diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts index f319f7ef986673..d671bdc0078ebf 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts @@ -9,7 +9,7 @@ import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'; /** The default time in ms to wait for a Cypress command to complete */ export const DEFAULT_TIMEOUT = 60 * 1000; -Given(`a user click page load breakdown filter`, () => { +Given(`a user clicks the page load breakdown filter`, () => { // wait for all loading to finish cy.get('kbnLoadingIndicator').should('not.be.visible'); cy.get('.euiStat__title-isLoading').should('not.be.visible'); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts index 8e010d5180f881..804974d8d437d7 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts @@ -6,6 +6,7 @@ import { Given, Then } from 'cypress-cucumber-preprocessor/steps'; import { loginAndWaitForPage } from '../../../integration/helpers'; +import { verifyClientMetrics } from './client_metrics_helper'; /** The default time in ms to wait for a Cypress command to complete */ export const DEFAULT_TIMEOUT = 60 * 1000; @@ -21,19 +22,9 @@ Given(`a user browses the APM UI application for RUM Data`, () => { }); Then(`should have correct client metrics`, () => { - const clientMetrics = '[data-cy=client-metrics] .euiStat__title'; + const metrics = ['0.01 sec', '0.08 sec', '55 ']; - // wait for all loading to finish - cy.get('kbnLoadingIndicator').should('not.be.visible'); - cy.get('.euiStat__title', { timeout: DEFAULT_TIMEOUT }).should('be.visible'); - cy.get('.euiSelect-isLoading').should('not.be.visible'); - cy.get('.euiStat__title-isLoading').should('not.be.visible'); - - cy.get(clientMetrics).eq(2).should('have.text', '55 '); - - cy.get(clientMetrics).eq(1).should('have.text', '0.08 sec'); - - cy.get(clientMetrics).eq(0).should('have.text', '0.01 sec'); + verifyClientMetrics(metrics, true); }); Then(`should display percentile for page load chart`, () => { diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/service_name_filter.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/service_name_filter.ts index b0694c902085ad..68fc4d528543ac 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/service_name_filter.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/service_name_filter.ts @@ -6,6 +6,7 @@ import { When, Then } from 'cypress-cucumber-preprocessor/steps'; import { DEFAULT_TIMEOUT } from '../apm'; +import { verifyClientMetrics } from './client_metrics_helper'; When('a user changes the selected service name', (filterName) => { // wait for all loading to finish @@ -16,15 +17,7 @@ When('a user changes the selected service name', (filterName) => { }); Then(`it displays relevant client metrics`, () => { - const clientMetrics = '[data-cy=client-metrics] .euiStat__title'; + const metrics = ['0.01 sec', '0.07 sec', '7 ']; - // wait for all loading to finish - cy.get('kbnLoadingIndicator').should('not.be.visible'); - cy.get('.euiStat__title-isLoading').should('not.be.visible'); - - cy.get(clientMetrics).eq(2).should('have.text', '7 '); - - cy.get(clientMetrics).eq(1).should('have.text', '0.07 sec'); - - cy.get(clientMetrics).eq(0).should('have.text', '0.01 sec'); + verifyClientMetrics(metrics, false); }); From ec1c158e09a7a45b79ef8c17f10351e3e71229b6 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Tue, 1 Sep 2020 12:01:04 +0200 Subject: [PATCH 135/216] restore dummy documentation_links.ts file to fix doc build (#76354) --- .../documentation_links.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/legacy/ui/public/documentation_links/documentation_links.ts diff --git a/src/legacy/ui/public/documentation_links/documentation_links.ts b/src/legacy/ui/public/documentation_links/documentation_links.ts new file mode 100644 index 00000000000000..2520fb71801600 --- /dev/null +++ b/src/legacy/ui/public/documentation_links/documentation_links.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* + WARNING: The links in this file are validated during the docs build. This is accomplished with some regex magic that + looks for these particular constants. As a result, we should not add new constants or change the existing ones. + If you absolutely must make a change, talk to Clinton Gormley first so he can update his Perl scripts. + */ +export const DOC_LINK_VERSION = 'stub'; +export const ELASTIC_WEBSITE_URL = 'stub'; + +export const documentationLinks = {}; From e7966e9d46870fa8da46d6d361200073593f9374 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Tue, 1 Sep 2020 11:06:38 +0100 Subject: [PATCH 136/216] [Security Solution] Add tests for import / export timelines (#75537) * add intergration test for install prebuilt timelines * add integration tests * add functional test for export timeline * clean up * asserts the content of the exported timeline * update selector * update selector * reuses the timeline id from the expected exported file Co-authored-by: Elastic Machine Co-authored-by: Gloria Hornero --- .../integration/timelines_export.spec.ts | 41 ++ .../cypress/screens/timeline.ts | 10 + .../cypress/tasks/timeline.ts | 14 + .../expected_timelines_export.ndjson | 1 + .../common/components/link_icon/index.tsx | 4 +- .../open_timeline/open_timeline.tsx | 2 +- .../basic/tests/add_prepackaged_rules.ts | 46 +- .../basic/tests/import_timelines.ts | 403 ++++++++++++++++++ .../basic/tests/index.ts | 1 + .../tests/install_prepackaged_timelines.ts | 87 ++++ 10 files changed, 605 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/integration/timelines_export.spec.ts create mode 100644 x-pack/plugins/security_solution/cypress/test_files/expected_timelines_export.ndjson create mode 100644 x-pack/test/detection_engine_api_integration/basic/tests/import_timelines.ts create mode 100644 x-pack/test/detection_engine_api_integration/basic/tests/install_prepackaged_timelines.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines_export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines_export.spec.ts new file mode 100644 index 00000000000000..d8f96aaf5e5639 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/timelines_export.spec.ts @@ -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 { exportTimeline, waitForTimelinesPanelToBeLoaded } from '../tasks/timeline'; +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; + +import { TIMELINES_URL } from '../urls/navigation'; + +const EXPECTED_EXPORTED_TIMELINE_PATH = 'cypress/test_files/expected_timelines_export.ndjson'; + +describe('Export timelines', () => { + before(() => { + esArchiverLoad('timeline'); + cy.server(); + cy.route('POST', '**api/timeline/_export?file_name=timelines_export.ndjson*').as('export'); + }); + + after(() => { + esArchiverUnload('timeline'); + }); + + it('Exports a custom timeline', () => { + loginAndWaitForPageWithoutDateRange(TIMELINES_URL); + waitForTimelinesPanelToBeLoaded(); + + cy.readFile(EXPECTED_EXPORTED_TIMELINE_PATH).then(($expectedExportedJson) => { + const parsedJson = JSON.parse($expectedExportedJson); + const timelineId = parsedJson.savedObjectId; + exportTimeline(timelineId); + + cy.wait('@export').then((response) => { + cy.wrap(response.status).should('eql', 200); + cy.wrap(response.xhr.responseText).should('eql', $expectedExportedJson); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 26203a8ca3b839..fd41cd63fc090a 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +export const BULK_ACTIONS = '[data-test-subj="utility-bar-action-button"]'; + export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]'; export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]'; @@ -11,6 +13,8 @@ export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]'; export const DRAGGABLE_HEADER = '[data-test-subj="events-viewer-panel"] [data-test-subj="headers-group"] [data-test-subj="draggable-header"]'; +export const EXPORT_TIMELINE_ACTION = '[data-test-subj="export-timeline-action"]'; + export const HEADER = '[data-test-subj="header"]'; export const HEADERS_GROUP = '[data-test-subj="headers-group"]'; @@ -41,6 +45,10 @@ export const TIMELINE = (id: string) => { export const TIMELINE_CHANGES_IN_PROGRESS = '[data-test-subj="timeline"] .euiProgress'; +export const TIMELINE_CHECKBOX = (id: string) => { + return `[data-test-subj="checkboxSelectRow-${id}"]`; +}; + export const TIMELINE_COLUMN_SPINNER = '[data-test-subj="timeline-loading-spinner"]'; export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; @@ -70,6 +78,8 @@ export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-gear"]'; export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]'; +export const TIMELINES_TABLE = '[data-test-subj="timelines-table"]'; + export const TIMESTAMP_HEADER_FIELD = '[data-test-subj="header-text-@timestamp"]'; export const TIMESTAMP_TOGGLE_FIELD = '[data-test-subj="toggle-field-@timestamp"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 08624df06e096d..6fb8bb5e29ae5e 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -5,8 +5,11 @@ */ import { + BULK_ACTIONS, CLOSE_TIMELINE_BTN, CREATE_NEW_TIMELINE, + EXPORT_TIMELINE_ACTION, + TIMELINE_CHECKBOX, HEADER, ID_FIELD, ID_HEADER_FIELD, @@ -20,6 +23,7 @@ import { TIMELINE_INSPECT_BUTTON, TIMELINE_SETTINGS_ICON, TIMELINE_TITLE, + TIMELINES_TABLE, TIMESTAMP_TOGGLE_FIELD, TOGGLE_TIMELINE_EXPAND_EVENT, REMOVE_COLUMN, @@ -66,6 +70,12 @@ export const expandFirstTimelineEventDetails = () => { cy.get(TOGGLE_TIMELINE_EXPAND_EVENT).first().click({ force: true }); }; +export const exportTimeline = (timelineId: string) => { + cy.get(TIMELINE_CHECKBOX(timelineId)).click({ force: true }); + cy.get(BULK_ACTIONS).click({ force: true }); + cy.get(EXPORT_TIMELINE_ACTION).click(); +}; + export const openTimelineFieldsBrowser = () => { cy.get(TIMELINE_FIELDS_BUTTON).click({ force: true }); }; @@ -122,6 +132,10 @@ export const resetFields = () => { cy.get(RESET_FIELDS).click({ force: true }); }; +export const waitForTimelinesPanelToBeLoaded = () => { + cy.get(TIMELINES_TABLE).should('exist'); +}; + export const waitForTimelineChanges = () => { cy.get(TIMELINE_CHANGES_IN_PROGRESS).should('exist'); cy.get(TIMELINE_CHANGES_IN_PROGRESS).should('not.exist'); diff --git a/x-pack/plugins/security_solution/cypress/test_files/expected_timelines_export.ndjson b/x-pack/plugins/security_solution/cypress/test_files/expected_timelines_export.ndjson new file mode 100644 index 00000000000000..9cca356a8b0521 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/test_files/expected_timelines_export.ndjson @@ -0,0 +1 @@ +{"savedObjectId":"0162c130-78be-11ea-9718-118a926974a4","version":"WzcsMV0=","columns":[{"columnHeaderType":"not-filtered","id":"@timestamp"},{"columnHeaderType":"not-filtered","id":"message"},{"columnHeaderType":"not-filtered","id":"event.category"},{"columnHeaderType":"not-filtered","id":"event.action"},{"columnHeaderType":"not-filtered","id":"host.name"},{"columnHeaderType":"not-filtered","id":"source.ip"},{"columnHeaderType":"not-filtered","id":"destination.ip"},{"columnHeaderType":"not-filtered","id":"user.name"}],"created":1586256805054,"createdBy":"elastic","dataProviders":[],"dateRange":{"end":1586256837669,"start":1546343624710},"description":"description","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"expression":"host.name:*","kind":"kuery"},"serializedQuery":"{\"bool\":{\"should\":[{\"exists\":{\"field\":\"host.name\"}}],\"minimum_should_match\":1}}"}},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"title":"SIEM test","updated":1586256839298,"updatedBy":"elastic","timelineType":"default","eventNotes":[],"globalNotes":[],"pinnedEventIds":[]} diff --git a/x-pack/plugins/security_solution/public/common/components/link_icon/index.tsx b/x-pack/plugins/security_solution/public/common/components/link_icon/index.tsx index 75d396fe384f81..19f1d70e6e230d 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_icon/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_icon/index.tsx @@ -6,7 +6,7 @@ import { EuiIcon, EuiLink, IconSize, IconType } from '@elastic/eui'; import { LinkAnchorProps } from '@elastic/eui/src/components/link/link'; -import React from 'react'; +import React, { ReactNode } from 'react'; import styled, { css } from 'styled-components'; interface LinkProps { @@ -47,7 +47,7 @@ export const Link = styled(({ iconSide, children, ...rest }) => ( Link.displayName = 'Link'; export interface LinkIconProps extends LinkProps { - children: string; + children: string | ReactNode; iconSize?: IconSize; iconType: IconType; dataTestSubj?: string; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx index 1f5f0ccca3b708..e9ae66703f0178 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx @@ -224,7 +224,7 @@ export const OpenTimeline = React.memo( popoverContent={getBatchItemsPopoverContent} data-test-subj="utility-bar-action" > - {i18n.BATCH_ACTIONS} + {i18n.BATCH_ACTIONS} )} diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts index a022b7c79c0795..c682c1f1f46402 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts @@ -49,7 +49,7 @@ export default ({ getService }: FtrProviderContext): void => { await deleteAllTimelines(es); }); - it('should contain two output keys of rules_installed and rules_updated', async () => { + it('should contain rules_installed, rules_updated, timelines_installed, and timelines_updated', async () => { const { body } = await supertest .put(DETECTION_ENGINE_PREPACKAGED_URL) .set('kbn-xsrf', 'true') @@ -74,6 +74,16 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.rules_installed).to.be.greaterThan(0); }); + it('should create the prepackaged timelines and return a count greater than zero', async () => { + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.timelines_installed).to.be.greaterThan(0); + }); + it('should create the prepackaged rules that the rules_updated is of size zero', async () => { const { body } = await supertest .put(DETECTION_ENGINE_PREPACKAGED_URL) @@ -84,6 +94,16 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.rules_updated).to.eql(0); }); + it('should create the prepackaged timelines and the timelines_updated is of size zero', async () => { + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.timelines_updated).to.eql(0); + }); + it('should be possible to call the API twice and the second time the number of rules installed should be zero', async () => { await supertest .put(DETECTION_ENGINE_PREPACKAGED_URL) @@ -109,6 +129,30 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.rules_installed).to.eql(0); }); + + it('should be possible to call the API twice and the second time the number of timelines installed should be zero', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + await waitFor(async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .expect(200); + return body.timelines_not_installed === 0; + }); + + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.timelines_installed).to.eql(0); + }); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/import_timelines.ts b/x-pack/test/detection_engine_api_integration/basic/tests/import_timelines.ts new file mode 100644 index 00000000000000..209c7974a7c2a3 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/import_timelines.ts @@ -0,0 +1,403 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { TIMELINE_IMPORT_URL } from '../../../../plugins/security_solution/common/constants'; + +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { deleteAllTimelines } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('import timelines', () => { + describe('creating a timeline', () => { + const getTimeline = () => { + return Buffer.from( + JSON.stringify({ + savedObjectId: '67664480-d191-11ea-ae67-4f4be8c1847b', + version: 'WzU1NSwxXQ==', + columns: [ + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: '@timestamp', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'message', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.category', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.action', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'host.name', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'source.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'destination.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'user.name', + searchable: null, + }, + ], + dataProviders: [], + description: '', + eventType: 'all', + filters: [], + kqlMode: 'filter', + timelineType: 'default', + kqlQuery: { + filterQuery: { + serializedQuery: + '{"bool":{"should":[{"exists":{"field":"@timestamp"}}],"minimum_should_match":1}}', + kuery: { + expression: '@timestamp : * ', + kind: 'kuery', + }, + }, + }, + title: 'x2', + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + created: 1596036895488, + createdBy: 'angela', + updated: 1596491470411, + updatedBy: 'elastic', + templateTimelineId: null, + templateTimelineVersion: null, + dateRange: { + start: '2020-04-10T14:10:58.373Z', + end: '2020-05-30T14:16:58.373Z', + }, + savedQueryId: null, + eventNotes: [ + { + noteId: '7d875ba0-d5d3-11ea-9899-ebec3d084fe0', + version: 'WzU1NiwxXQ==', + eventId: '8KtMKnIBOS_moQ_K9fAe', + note: 'hi Xavier', + timelineId: '67664480-d191-11ea-ae67-4f4be8c1847b', + created: 1596491490806, + createdBy: 'elastic', + updated: 1596491490806, + updatedBy: 'elastic', + }, + ], + globalNotes: [], + pinnedEventIds: [ + 'K99zy3EBDTDlbwBfpf6x', + 'GKpFKnIBOS_moQ_Ke5AO', + '8KtMKnIBOS_moQ_K9fAe', + ], + }) + ); + }; + beforeEach(async () => {}); + + afterEach(async () => { + await deleteAllTimelines(es); + }); + + it("if it doesn't exists", async () => { + const { body } = await supertest + .post(`${TIMELINE_IMPORT_URL}`) + .set('kbn-xsrf', 'true') + .attach('file', getTimeline(), 'timelines.ndjson') + .expect(200); + expect(body).to.eql({ + errors: [], + success: true, + success_count: 1, + timelines_installed: 1, + timelines_updated: 0, + }); + }); + }); + + describe('creating a timeline template', () => { + const getTimelineTemplate = () => { + return Buffer.from( + JSON.stringify({ + savedObjectId: 'cab434d0-d26c-11ea-b887-3b103296472a', + version: 'WzQ0NSwxXQ==', + columns: [ + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: '@timestamp', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'message', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.category', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.action', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'host.name', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'source.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'destination.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'user.name', + searchable: null, + }, + ], + dataProviders: [], + description: 'desc', + eventType: 'all', + filters: [], + kqlMode: 'filter', + timelineType: 'template', + kqlQuery: { filterQuery: null }, + title: 'my t template', + sort: { columnId: '@timestamp', sortDirection: 'desc' }, + templateTimelineId: '46a50505-0a48-49cb-9ab2-d15d683efa3b', + templateTimelineVersion: 1, + created: 1596473742379, + createdBy: 'elastic', + updated: 1596473909169, + updatedBy: 'elastic', + dateRange: { start: '2020-08-02T16:55:22.160Z', end: '2020-08-03T16:55:22.161Z' }, + savedQueryId: null, + eventNotes: [], + globalNotes: [ + { + noteId: '358f45c0-d5aa-11ea-9b6d-53d136d390dc', + version: 'WzQzOCwxXQ==', + note: 'I have a global note', + timelineId: 'cab434d0-d26c-11ea-b887-3b103296472a', + created: 1596473760688, + createdBy: 'elastic', + updated: 1596473760688, + updatedBy: 'elastic', + }, + ], + pinnedEventIds: [], + }) + ); + }; + + afterEach(async () => { + await deleteAllTimelines(es); + }); + + it("if it doesn't exists", async () => { + const { body } = await supertest + .post(`${TIMELINE_IMPORT_URL}`) + .set('kbn-xsrf', 'true') + .attach('file', getTimelineTemplate(), 'timelines.ndjson') + .expect(200); + expect(body).to.eql({ + errors: [], + success: true, + success_count: 1, + timelines_installed: 1, + timelines_updated: 0, + }); + }); + }); + + describe('Updating a timeline template', () => { + const getTimelineTemplate = (templateTimelineVersion: number) => { + return Buffer.from( + JSON.stringify({ + savedObjectId: 'cab434d0-d26c-11ea-b887-3b103296472a', + version: 'WzQ0NSwxXQ==', + columns: [ + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: '@timestamp', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'message', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.category', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.action', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'host.name', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'source.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'destination.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'user.name', + searchable: null, + }, + ], + dataProviders: [], + description: 'desc', + eventType: 'all', + filters: [], + kqlMode: 'filter', + timelineType: 'template', + kqlQuery: { filterQuery: null }, + title: 'my t template', + sort: { columnId: '@timestamp', sortDirection: 'desc' }, + templateTimelineId: '46a50505-0a48-49cb-9ab2-d15d683efa3b', + templateTimelineVersion, + created: 1596473742379, + createdBy: 'elastic', + updated: 1596473909169, + updatedBy: 'elastic', + dateRange: { start: '2020-08-02T16:55:22.160Z', end: '2020-08-03T16:55:22.161Z' }, + savedQueryId: null, + eventNotes: [], + globalNotes: [ + { + noteId: '358f45c0-d5aa-11ea-9b6d-53d136d390dc', + version: 'WzQzOCwxXQ==', + note: 'I have a global note', + timelineId: 'cab434d0-d26c-11ea-b887-3b103296472a', + created: 1596473760688, + createdBy: 'elastic', + updated: 1596473760688, + updatedBy: 'elastic', + }, + ], + pinnedEventIds: [], + }) + ); + }; + + afterEach(async () => { + await deleteAllTimelines(es); + }); + + it("if it doesn't exists", async () => { + await supertest + .post(`${TIMELINE_IMPORT_URL}`) + .set('kbn-xsrf', 'true') + .attach('file', getTimelineTemplate(1), 'timelines.ndjson') + .expect(200); + const { body } = await supertest + .post(`${TIMELINE_IMPORT_URL}`) + .set('kbn-xsrf', 'true') + .attach('file', getTimelineTemplate(2), 'timelines.ndjson') + .expect(200); + expect(body).to.eql({ + errors: [], + success: true, + success_count: 1, + timelines_installed: 0, + timelines_updated: 1, + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/index.ts b/x-pack/test/detection_engine_api_integration/basic/tests/index.ts index a480e63ff4a923..09feb7ac5e0ec3 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/index.ts @@ -28,5 +28,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./patch_rules')); loadTestFile(require.resolve('./query_signals')); loadTestFile(require.resolve('./open_close_signals')); + loadTestFile(require.resolve('./import_timelines')); }); }; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/install_prepackaged_timelines.ts b/x-pack/test/detection_engine_api_integration/basic/tests/install_prepackaged_timelines.ts new file mode 100644 index 00000000000000..556217877968b6 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/install_prepackaged_timelines.ts @@ -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 expect from '@kbn/expect'; + +import { TIMELINE_PREPACKAGED_URL } from '../../../../plugins/security_solution/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteAllTimelines, + deleteSignalsIndex, + waitFor, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('install_prepackaged_timelines', () => { + describe('creating prepackaged rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + await deleteAllTimelines(es); + }); + + it('should contain timelines_installed, and timelines_updated', async () => { + const { body } = await supertest + .put(TIMELINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(Object.keys(body)).to.eql(['timelines_installed', 'timelines_updated']); + }); + + it('should create the prepackaged timelines and return a count greater than zero', async () => { + const { body } = await supertest + .put(TIMELINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.timelines_installed).to.be.greaterThan(0); + }); + + it('should create the prepackaged timelines and the timelines_updated is of size zero', async () => { + const { body } = await supertest + .put(TIMELINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.timelines_updated).to.eql(0); + }); + + it('should be possible to call the API twice and the second time the number of timelines installed should be zero', async () => { + await supertest.put(TIMELINE_PREPACKAGED_URL).set('kbn-xsrf', 'true').send().expect(200); + + await waitFor(async () => { + const { body } = await supertest + .get(`${TIMELINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .expect(200); + return body.timelines_not_installed === 0; + }); + + const { body } = await supertest + .put(TIMELINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.timelines_installed).to.eql(0); + }); + }); + }); +}; From d802bf03f76050a03774591db11804f0159e9b1b Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Tue, 1 Sep 2020 12:19:06 +0200 Subject: [PATCH 137/216] xpack_main legacy plugin pre-removal cleanup (#76257) * cleanup xpack_main legacy plugin, remove capabilities mixin * fix test env * delete injectXpackSignature and related tests --- .../capabilities/capabilities_mixin.test.ts | 65 ----- .../server/capabilities/capabilities_mixin.ts | 42 ---- src/legacy/server/capabilities/index.ts | 20 -- src/legacy/server/kbn_server.js | 4 - x-pack/legacy/plugins/xpack_main/index.js | 28 --- .../lib/__tests__/call_cluster_factory.js | 77 ------ .../__tests__/inject_xpack_info_signature.js | 125 ---------- .../lib/__tests__/replace_injected_vars.js | 225 ------------------ .../server/lib/__tests__/setup_xpack_main.js | 22 -- .../server/lib/call_cluster_factory.js | 45 ---- .../server/lib/inject_xpack_info_signature.js | 25 -- .../server/lib/replace_injected_vars.js | 38 --- .../xpack_main/server/lib/setup_xpack_main.js | 6 - .../plugins/xpack_main/server/xpack_main.d.ts | 1 - x-pack/plugins/features/server/plugin.test.ts | 43 ++-- x-pack/plugins/features/server/plugin.ts | 7 +- x-pack/scripts/functional_tests.js | 1 - x-pack/test/licensing_plugin/config.legacy.ts | 15 -- x-pack/test/licensing_plugin/legacy/index.ts | 16 -- .../test/licensing_plugin/legacy/updates.ts | 58 ----- 20 files changed, 35 insertions(+), 828 deletions(-) delete mode 100644 src/legacy/server/capabilities/capabilities_mixin.test.ts delete mode 100644 src/legacy/server/capabilities/capabilities_mixin.ts delete mode 100644 src/legacy/server/capabilities/index.ts delete mode 100644 x-pack/legacy/plugins/xpack_main/server/lib/__tests__/call_cluster_factory.js delete mode 100644 x-pack/legacy/plugins/xpack_main/server/lib/__tests__/inject_xpack_info_signature.js delete mode 100644 x-pack/legacy/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js delete mode 100644 x-pack/legacy/plugins/xpack_main/server/lib/call_cluster_factory.js delete mode 100644 x-pack/legacy/plugins/xpack_main/server/lib/inject_xpack_info_signature.js delete mode 100644 x-pack/legacy/plugins/xpack_main/server/lib/replace_injected_vars.js delete mode 100644 x-pack/test/licensing_plugin/config.legacy.ts delete mode 100644 x-pack/test/licensing_plugin/legacy/index.ts delete mode 100644 x-pack/test/licensing_plugin/legacy/updates.ts diff --git a/src/legacy/server/capabilities/capabilities_mixin.test.ts b/src/legacy/server/capabilities/capabilities_mixin.test.ts deleted file mode 100644 index 3422d6a8cbb346..00000000000000 --- a/src/legacy/server/capabilities/capabilities_mixin.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 { Server } from 'hapi'; -import KbnServer from '../kbn_server'; - -import { capabilitiesMixin } from './capabilities_mixin'; - -describe('capabilitiesMixin', () => { - let registerMock: jest.Mock; - - const getKbnServer = (pluginSpecs: any[] = []) => { - return ({ - afterPluginsInit: (callback: () => void) => callback(), - pluginSpecs, - newPlatform: { - setup: { - core: { - capabilities: { - registerProvider: registerMock, - }, - }, - }, - }, - } as unknown) as KbnServer; - }; - - let server: Server; - beforeEach(() => { - server = new Server(); - server.getUiNavLinks = () => []; - registerMock = jest.fn(); - }); - - it('calls capabilities#registerCapabilitiesProvider for each legacy plugin specs', async () => { - const getPluginSpec = (provider: () => any) => ({ - getUiCapabilitiesProvider: () => provider, - }); - - const capaA = { catalogue: { A: true } }; - const capaB = { catalogue: { B: true } }; - const kbnServer = getKbnServer([getPluginSpec(() => capaA), getPluginSpec(() => capaB)]); - await capabilitiesMixin(kbnServer, server); - - expect(registerMock).toHaveBeenCalledTimes(2); - expect(registerMock.mock.calls[0][0]()).toEqual(capaA); - expect(registerMock.mock.calls[1][0]()).toEqual(capaB); - }); -}); diff --git a/src/legacy/server/capabilities/capabilities_mixin.ts b/src/legacy/server/capabilities/capabilities_mixin.ts deleted file mode 100644 index 1f8c869f17f663..00000000000000 --- a/src/legacy/server/capabilities/capabilities_mixin.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 { Server } from 'hapi'; -import KbnServer from '../kbn_server'; - -export async function capabilitiesMixin(kbnServer: KbnServer, server: Server) { - const registerLegacyCapabilities = async () => { - const capabilitiesList = await Promise.all( - kbnServer.pluginSpecs - .map((spec) => spec.getUiCapabilitiesProvider()) - .filter((provider) => !!provider) - .map((provider) => provider(server)) - ); - - capabilitiesList.forEach((capabilities) => { - kbnServer.newPlatform.setup.core.capabilities.registerProvider(() => capabilities); - }); - }; - - // Some plugin capabilities are derived from data provided by other plugins, - // so we need to wait until after all plugins have been init'd to fetch uiCapabilities. - kbnServer.afterPluginsInit(async () => { - await registerLegacyCapabilities(); - }); -} diff --git a/src/legacy/server/capabilities/index.ts b/src/legacy/server/capabilities/index.ts deleted file mode 100644 index 8c5dea1226f2b6..00000000000000 --- a/src/legacy/server/capabilities/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 { capabilitiesMixin } from './capabilities_mixin'; diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 1084521235ea03..320086b6d65310 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -34,7 +34,6 @@ import configCompleteMixin from './config/complete'; import { optimizeMixin } from '../../optimize'; import * as Plugins from './plugins'; import { savedObjectsMixin } from './saved_objects/saved_objects_mixin'; -import { capabilitiesMixin } from './capabilities'; import { serverExtensionsMixin } from './server_extensions'; import { uiMixin } from '../ui'; import { i18nMixin } from './i18n'; @@ -115,9 +114,6 @@ export default class KbnServer { // setup saved object routes savedObjectsMixin, - // setup capabilities routes - capabilitiesMixin, - // setup routes that serve the @kbn/optimizer output optimizeMixin, diff --git a/x-pack/legacy/plugins/xpack_main/index.js b/x-pack/legacy/plugins/xpack_main/index.js index b83c5a5ff606e3..854fba6624719f 100644 --- a/x-pack/legacy/plugins/xpack_main/index.js +++ b/x-pack/legacy/plugins/xpack_main/index.js @@ -6,12 +6,9 @@ import { resolve } from 'path'; import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; -import { replaceInjectedVars } from './server/lib/replace_injected_vars'; import { setupXPackMain } from './server/lib/setup_xpack_main'; import { xpackInfoRoute, settingsRoute } from './server/routes/api/v1'; -export { callClusterFactory } from './server/lib/call_cluster_factory'; - export const xpackMain = (kibana) => { return new kibana.Plugin({ id: 'xpack_main', @@ -25,32 +22,7 @@ export const xpackMain = (kibana) => { }).default(); }, - uiCapabilities(server) { - const featuresPlugin = server.newPlatform.setup.plugins.features; - if (!featuresPlugin) { - throw new Error('New Platform XPack Features plugin is not available.'); - } - return featuresPlugin.getFeaturesUICapabilities(); - }, - - uiExports: { - replaceInjectedVars, - injectDefaultVars(server) { - const config = server.config(); - - return { - activeSpace: null, - spacesEnabled: config.get('xpack.spaces.enabled'), - }; - }, - }, - init(server) { - const featuresPlugin = server.newPlatform.setup.plugins.features; - if (!featuresPlugin) { - throw new Error('New Platform XPack Features plugin is not available.'); - } - mirrorPluginStatus(server.plugins.elasticsearch, this, 'yellow', 'red'); setupXPackMain(server); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/call_cluster_factory.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/call_cluster_factory.js deleted file mode 100644 index abe0d327d7b5cb..00000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/call_cluster_factory.js +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import { callClusterFactory } from '../call_cluster_factory'; - -describe('callClusterFactory', () => { - let mockServer; - let mockCluster; - - beforeEach(() => { - mockCluster = { - callWithRequest: sinon.stub().returns(Promise.resolve({ hits: { total: 0 } })), - callWithInternalUser: sinon.stub().returns(Promise.resolve({ hits: { total: 0 } })), - }; - mockServer = { - plugins: { - elasticsearch: { - getCluster: sinon.stub().withArgs('admin').returns(mockCluster), - }, - }, - log() {}, - }; - }); - - it('returns an object with getter methods', () => { - const _callClusterFactory = callClusterFactory(mockServer); - expect(_callClusterFactory).to.be.an('object'); - expect(_callClusterFactory.getCallClusterWithReq).to.be.an('function'); - expect(_callClusterFactory.getCallClusterInternal).to.be.an('function'); - expect(mockCluster.callWithRequest.called).to.be(false); - expect(mockCluster.callWithInternalUser.called).to.be(false); - }); - - describe('getCallClusterWithReq', () => { - it('throws an error if req is not passed', async () => { - const runCallCluster = () => callClusterFactory(mockServer).getCallClusterWithReq(); - expect(runCallCluster).to.throwException(); - }); - - it('returns a method that wraps callWithRequest', async () => { - const mockReq = { - headers: { - authorization: 'Basic dSQzcm5AbTM6cEAkJHcwcmQ=', // u$3rn@m3:p@$$w0rd - }, - }; - const callCluster = callClusterFactory(mockServer).getCallClusterWithReq(mockReq); - - const result = await callCluster('search', { body: { match: { match_all: {} } } }); - expect(result).to.eql({ hits: { total: 0 } }); - - expect(mockCluster.callWithInternalUser.called).to.be(false); - expect(mockCluster.callWithRequest.calledOnce).to.be(true); - const [req, method] = mockCluster.callWithRequest.getCall(0).args; - expect(req).to.be(mockReq); - expect(method).to.be('search'); - }); - }); - - describe('getCallClusterInternal', () => { - it('returns a method that wraps callWithInternalUser', async () => { - const callCluster = callClusterFactory(mockServer).getCallClusterInternal(); - - const result = await callCluster('search', { body: { match: { match_all: {} } } }); - expect(result).to.eql({ hits: { total: 0 } }); - - expect(mockCluster.callWithRequest.called).to.be(false); - expect(mockCluster.callWithInternalUser.calledOnce).to.be(true); - const [method] = mockCluster.callWithInternalUser.getCall(0).args; - expect(method).to.eql('search'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/inject_xpack_info_signature.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/inject_xpack_info_signature.js deleted file mode 100644 index 420f3b2d6631cc..00000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/inject_xpack_info_signature.js +++ /dev/null @@ -1,125 +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 sinon from 'sinon'; -import expect from '@kbn/expect'; -import { injectXPackInfoSignature } from '../inject_xpack_info_signature'; - -describe('injectXPackInfoSignature()', () => { - class MockErrorResponse extends Error { - constructor() { - super(); - this.output = { - headers: {}, - }; - - this.headers = {}; - } - } - - const fakeH = { continue: 'blah' }; - - let mockXPackInfo; - beforeEach(() => { - mockXPackInfo = sinon.stub({ - isAvailable() {}, - getSignature() {}, - refreshNow() {}, - }); - }); - - describe('error response', () => { - it('refreshes `xpackInfo` and do not inject signature if it is not available.', async () => { - mockXPackInfo.isAvailable.returns(true); - mockXPackInfo.getSignature.returns('this-should-never-be-set'); - - // We need this to make sure the code waits for `refreshNow` to complete before it tries - // to access its properties. - mockXPackInfo.refreshNow = () => { - return new Promise((resolve) => { - mockXPackInfo.isAvailable.returns(false); - resolve(); - }); - }; - - const mockResponse = new MockErrorResponse(); - const response = await injectXPackInfoSignature( - mockXPackInfo, - { response: mockResponse }, - fakeH - ); - - expect(mockResponse.headers).to.eql({}); - expect(mockResponse.output.headers).to.eql({}); - expect(response).to.be(fakeH.continue); - }); - - it('refreshes `xpackInfo` and injects its updated signature.', async () => { - mockXPackInfo.isAvailable.returns(true); - mockXPackInfo.getSignature.returns('old-signature'); - - // We need this to make sure the code waits for `refreshNow` to complete before it tries - // to access its properties. - mockXPackInfo.refreshNow = () => { - return new Promise((resolve) => { - mockXPackInfo.getSignature.returns('new-signature'); - resolve(); - }); - }; - - const mockResponse = new MockErrorResponse(); - const response = await injectXPackInfoSignature( - mockXPackInfo, - { response: mockResponse }, - fakeH - ); - - expect(mockResponse.headers).to.eql({}); - expect(mockResponse.output.headers).to.eql({ - 'kbn-xpack-sig': 'new-signature', - }); - expect(response).to.be(fakeH.continue); - }); - }); - - describe('non-error response', () => { - it('do not inject signature if `xpackInfo` is not available.', async () => { - mockXPackInfo.isAvailable.returns(false); - mockXPackInfo.getSignature.returns('this-should-never-be-set'); - - const mockResponse = { headers: {}, output: { headers: {} } }; - const response = await injectXPackInfoSignature( - mockXPackInfo, - { response: mockResponse }, - fakeH - ); - - expect(mockResponse.headers).to.eql({}); - expect(mockResponse.output.headers).to.eql({}); - sinon.assert.notCalled(mockXPackInfo.refreshNow); - expect(response).to.be(fakeH.continue); - }); - - it('injects signature if `xpackInfo` is available.', async () => { - mockXPackInfo.isAvailable.returns(true); - mockXPackInfo.getSignature.returns('available-signature'); - - const mockResponse = { headers: {}, output: { headers: {} } }; - const response = await injectXPackInfoSignature( - mockXPackInfo, - { response: mockResponse }, - fakeH - ); - - expect(mockResponse.headers).to.eql({ - 'kbn-xpack-sig': 'available-signature', - }); - expect(mockResponse.output.headers).to.eql({}); - sinon.assert.notCalled(mockXPackInfo.refreshNow); - expect(response).to.be(fakeH.continue); - }); - }); -}); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js deleted file mode 100644 index ce6e20bd874b29..00000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js +++ /dev/null @@ -1,225 +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 sinon from 'sinon'; -import expect from '@kbn/expect'; - -import { replaceInjectedVars } from '../replace_injected_vars'; -import { KibanaRequest } from '../../../../../../../src/core/server'; - -const buildRequest = (path = '/app/kibana') => { - const get = sinon.stub(); - - return { - app: {}, - path, - route: { settings: {} }, - headers: {}, - raw: { - req: { - socket: {}, - }, - }, - getSavedObjectsClient: () => { - return { - get, - create: sinon.stub(), - - errors: { - isNotFoundError: (error) => { - return error.message === 'not found exception'; - }, - }, - }; - }, - }; -}; - -describe('replaceInjectedVars uiExport', () => { - it('sends xpack info if request is authenticated and license is not basic', async () => { - const originalInjectedVars = { a: 1 }; - const request = buildRequest(); - const server = mockServer(); - - const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.eql({ - a: 1, - xpackInitialInfo: { - b: 1, - }, - }); - - sinon.assert.calledOnce(server.newPlatform.setup.plugins.security.authc.isAuthenticated); - sinon.assert.calledWithExactly( - server.newPlatform.setup.plugins.security.authc.isAuthenticated, - sinon.match.instanceOf(KibanaRequest) - ); - }); - - it('sends the xpack info if security plugin is disabled', async () => { - const originalInjectedVars = { a: 1 }; - const request = buildRequest(); - const server = mockServer(); - delete server.plugins.security; - delete server.newPlatform.setup.plugins.security; - - const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.eql({ - a: 1, - xpackInitialInfo: { - b: 1, - }, - }); - }); - - it('sends the xpack info if xpack license is basic', async () => { - const originalInjectedVars = { a: 1 }; - const request = buildRequest(); - const server = mockServer(); - server.plugins.xpack_main.info.license.isOneOf.returns(true); - - const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.eql({ - a: 1, - xpackInitialInfo: { - b: 1, - }, - }); - }); - - it('respects the telemetry opt-in document when opted-out', async () => { - const originalInjectedVars = { a: 1 }; - const request = buildRequest(); - const server = mockServer(); - server.plugins.xpack_main.info.license.isOneOf.returns(true); - - const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.eql({ - a: 1, - xpackInitialInfo: { - b: 1, - }, - }); - }); - - it('respects the telemetry opt-in document when opted-in', async () => { - const originalInjectedVars = { a: 1 }; - const request = buildRequest(); - const server = mockServer(); - server.plugins.xpack_main.info.license.isOneOf.returns(true); - - const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.eql({ - a: 1, - xpackInitialInfo: { - b: 1, - }, - }); - }); - - it('indicates that telemetry is opted-out when not loading an application', async () => { - const originalInjectedVars = { a: 1 }; - const request = buildRequest(true, '/'); - const server = mockServer(); - server.plugins.xpack_main.info.license.isOneOf.returns(true); - - const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.eql({ - a: 1, - xpackInitialInfo: { - b: 1, - }, - }); - }); - - it('sends the originalInjectedVars if not authenticated', async () => { - const originalInjectedVars = { a: 1 }; - const request = buildRequest(); - const server = mockServer(); - server.newPlatform.setup.plugins.security.authc.isAuthenticated.returns(false); - - const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.eql(originalInjectedVars); - }); - - it('sends the originalInjectedVars if xpack info is unavailable', async () => { - const originalInjectedVars = { a: 1 }; - const request = buildRequest(); - const server = mockServer(); - server.plugins.xpack_main.info.isAvailable.returns(false); - - const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.eql(originalInjectedVars); - }); - - it('sends the originalInjectedVars (with xpackInitialInfo = undefined) if security is disabled, xpack info is unavailable', async () => { - const originalInjectedVars = { - a: 1, - uiCapabilities: { navLinks: { foo: true }, bar: { baz: true }, catalogue: { cfoo: true } }, - }; - const request = buildRequest(); - const server = mockServer(); - delete server.plugins.security; - server.plugins.xpack_main.info.isAvailable.returns(false); - - const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.eql({ - a: 1, - xpackInitialInfo: undefined, - uiCapabilities: { - navLinks: { foo: true }, - bar: { baz: true }, - catalogue: { - cfoo: true, - }, - }, - }); - }); -}); - -// creates a mock server object that defaults to being authenticated with a -// non-basic license -function mockServer() { - const getLicenseCheckResults = sinon.stub().returns({}); - return { - newPlatform: { - setup: { - plugins: { security: { authc: { isAuthenticated: sinon.stub().returns(true) } } }, - }, - }, - plugins: { - security: {}, - xpack_main: { - getFeatures: () => [ - { - id: 'mockFeature', - name: 'Mock Feature', - privileges: { - all: { - app: [], - savedObject: { - all: [], - read: [], - }, - ui: ['mockFeatureCapability'], - }, - }, - }, - ], - info: { - isAvailable: sinon.stub().returns(true), - feature: () => ({ - getLicenseCheckResults, - }), - license: { - isOneOf: sinon.stub().returns(false), - }, - toJSON: () => ({ b: 1 }), - }, - }, - }, - }; -} diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js index b4a2c090d63092..c34e27642d2ceb 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js @@ -8,7 +8,6 @@ import { BehaviorSubject } from 'rxjs'; import sinon from 'sinon'; import { XPackInfo } from '../xpack_info'; import { setupXPackMain } from '../setup_xpack_main'; -import * as InjectXPackInfoSignatureNS from '../inject_xpack_info_signature'; describe('setupXPackMain()', () => { const sandbox = sinon.createSandbox(); @@ -19,7 +18,6 @@ describe('setupXPackMain()', () => { beforeEach(() => { sandbox.useFakeTimers(); - sandbox.stub(InjectXPackInfoSignatureNS, 'injectXPackInfoSignature'); mockElasticsearchPlugin = { getCluster: sinon.stub(), @@ -63,29 +61,9 @@ describe('setupXPackMain()', () => { setupXPackMain(mockServer); sinon.assert.calledWithExactly(mockServer.expose, 'info', sinon.match.instanceOf(XPackInfo)); - sinon.assert.calledWithExactly(mockServer.ext, 'onPreResponse', sinon.match.func); sinon.assert.calledWithExactly(mockElasticsearchPlugin.status.on, 'change', sinon.match.func); }); - it('onPreResponse hook calls `injectXPackInfoSignature` for every request.', () => { - setupXPackMain(mockServer); - - const xPackInfo = mockServer.expose.firstCall.args[1]; - const onPreResponse = mockServer.ext.firstCall.args[1]; - - const mockRequest = {}; - const mockReply = sinon.stub(); - - onPreResponse(mockRequest, mockReply); - - sinon.assert.calledWithExactly( - InjectXPackInfoSignatureNS.injectXPackInfoSignature, - xPackInfo, - sinon.match.same(mockRequest), - sinon.match.same(mockReply) - ); - }); - describe('Elasticsearch plugin state changes cause XPackMain plugin state change.', () => { let xPackInfo; let onElasticsearchPluginStatusChange; diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/call_cluster_factory.js b/x-pack/legacy/plugins/xpack_main/server/lib/call_cluster_factory.js deleted file mode 100644 index f946725e017ffd..00000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/call_cluster_factory.js +++ /dev/null @@ -1,45 +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. - */ - -/* - * Factory for acquiring a method that can send an authenticated query to Elasticsearch. - * - * The method can either: - * - take authentication from an HTTP request object, which should be used when - * the caller is an HTTP API - * - fabricate authentication using the system username and password from the - * Kibana config, which should be used when the caller is an internal process - * - * @param {Object} server: the Kibana server object - * @return {Function}: callCluster function - */ -export function callClusterFactory(server) { - const { callWithRequest, callWithInternalUser } = server.plugins.elasticsearch.getCluster( - 'admin' - ); - - return { - /* - * caller is coming from a request, so use callWithRequest with actual - * Authorization header credentials - * @param {Object} req: HTTP request object - */ - getCallClusterWithReq(req) { - if (req === undefined) { - throw new Error('request object is required'); - } - return (...args) => callWithRequest(req, ...args); - }, - - getCallClusterInternal() { - /* - * caller is an internal function of the stats collection system, so use - * internal system user - */ - return callWithInternalUser; - }, - }; -} diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/inject_xpack_info_signature.js b/x-pack/legacy/plugins/xpack_main/server/lib/inject_xpack_info_signature.js deleted file mode 100644 index 166bd2b4755f05..00000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/inject_xpack_info_signature.js +++ /dev/null @@ -1,25 +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. - */ - -export async function injectXPackInfoSignature(info, request, h) { - // If we're returning an error response, refresh xpack info from - // Elasticsearch in case the error is due to a change in license information - // in Elasticsearch. - const isErrorResponse = request.response instanceof Error; - if (isErrorResponse) { - await info.refreshNow(); - } - - if (info.isAvailable()) { - // Note: request.response.output is used instead of request.response because - // evidently HAPI does not allow headers to be set on the latter in case of - // error responses. - const response = isErrorResponse ? request.response.output : request.response; - response.headers['kbn-xpack-sig'] = info.getSignature(); - } - - return h.continue; -} diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/replace_injected_vars.js b/x-pack/legacy/plugins/xpack_main/server/lib/replace_injected_vars.js deleted file mode 100644 index f09f97d44bfe8b..00000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/replace_injected_vars.js +++ /dev/null @@ -1,38 +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 { KibanaRequest } from '../../../../../../src/core/server'; - -export async function replaceInjectedVars(originalInjectedVars, request, server) { - const xpackInfo = server.plugins.xpack_main.info; - - const withXpackInfo = async () => ({ - ...originalInjectedVars, - xpackInitialInfo: xpackInfo.isAvailable() ? xpackInfo.toJSON() : undefined, - }); - - // security feature is disabled - if (!server.plugins.security || !server.newPlatform.setup.plugins.security) { - return await withXpackInfo(); - } - - // not enough license info to make decision one way or another - if (!xpackInfo.isAvailable()) { - return originalInjectedVars; - } - - // request is not authenticated - if ( - !(await server.newPlatform.setup.plugins.security.authc.isAuthenticated( - KibanaRequest.from(request) - )) - ) { - return originalInjectedVars; - } - - // plugin enabled, license is appropriate, request is authenticated - return await withXpackInfo(); -} diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js b/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js index 9196b210bba20a..33b551bbe864ff 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { injectXPackInfoSignature } from './inject_xpack_info_signature'; import { XPackInfo } from './xpack_info'; /** @@ -20,11 +19,6 @@ export function setupXPackMain(server) { server.expose('info', info); - server.ext('onPreResponse', (request, h) => injectXPackInfoSignature(info, request, h)); - - const { getFeatures } = server.newPlatform.setup.plugins.features; - server.expose('getFeatures', getFeatures); - const setPluginStatus = () => { if (info.isAvailable()) { server.plugins.xpack_main.status.green('Ready'); diff --git a/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts b/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts index 2a59c2f1366d4a..f4363a8e57b37c 100644 --- a/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts +++ b/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts @@ -11,5 +11,4 @@ export { XPackFeature } from './lib/xpack_info'; export interface XPackMainPlugin { info: XPackInfo; - getFeatures(): Feature[]; } diff --git a/x-pack/plugins/features/server/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts index 3d85c2e9eb751c..00d578f5ca8664 100644 --- a/x-pack/plugins/features/server/plugin.test.ts +++ b/x-pack/plugins/features/server/plugin.test.ts @@ -4,23 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ import { coreMock, savedObjectsServiceMock } from 'src/core/server/mocks'; - import { Plugin } from './plugin'; -const initContext = coreMock.createPluginInitializerContext(); -const coreSetup = coreMock.createSetup(); -const coreStart = coreMock.createStart(); -const typeRegistry = savedObjectsServiceMock.createTypeRegistryMock(); -typeRegistry.getVisibleTypes.mockReturnValue([ - { - name: 'foo', - hidden: false, - mappings: { properties: {} }, - namespaceType: 'single' as 'single', - }, -]); -coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); describe('Features Plugin', () => { + let initContext: ReturnType; + let coreSetup: ReturnType; + let coreStart: ReturnType; + let typeRegistry: ReturnType; + + beforeEach(() => { + initContext = coreMock.createPluginInitializerContext(); + coreSetup = coreMock.createSetup(); + coreStart = coreMock.createStart(); + typeRegistry = savedObjectsServiceMock.createTypeRegistryMock(); + typeRegistry.getVisibleTypes.mockReturnValue([ + { + name: 'foo', + hidden: false, + mappings: { properties: {} }, + namespaceType: 'single' as 'single', + }, + ]); + coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); + }); + it('returns OSS + registered features', async () => { const plugin = new Plugin(initContext); const { registerFeature } = await plugin.setup(coreSetup, {}); @@ -88,4 +95,12 @@ describe('Features Plugin', () => { expect(soTypes.includes('foo')).toBe(true); expect(soTypes.includes('bar')).toBe(false); }); + + it('registers a capabilities provider', async () => { + const plugin = new Plugin(initContext); + await plugin.setup(coreSetup, {}); + + expect(coreSetup.capabilities.registerProvider).toHaveBeenCalledTimes(1); + expect(coreSetup.capabilities.registerProvider).toHaveBeenCalledWith(expect.any(Function)); + }); }); diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index 5783b20eae6484..61b66d95ca44f3 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -61,10 +61,15 @@ export class Plugin { featureRegistry: this.featureRegistry, }); + const getFeaturesUICapabilities = () => + uiCapabilitiesForFeatures(this.featureRegistry.getAll()); + + core.capabilities.registerProvider(getFeaturesUICapabilities); + return deepFreeze({ registerFeature: this.featureRegistry.register.bind(this.featureRegistry), getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry), - getFeaturesUICapabilities: () => uiCapabilitiesForFeatures(this.featureRegistry.getAll()), + getFeaturesUICapabilities, }); } diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 421e709444d0f7..6271c4b6013071 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -56,7 +56,6 @@ const onlyNotInCoverageTests = [ require.resolve('../test/upgrade_assistant_integration/config.js'), require.resolve('../test/licensing_plugin/config.ts'), require.resolve('../test/licensing_plugin/config.public.ts'), - require.resolve('../test/licensing_plugin/config.legacy.ts'), require.resolve('../test/endpoint_api_integration_no_ingest/config.ts'), require.resolve('../test/reporting_api_integration/reporting_and_security.config.ts'), require.resolve('../test/reporting_api_integration/reporting_without_security.config.ts'), diff --git a/x-pack/test/licensing_plugin/config.legacy.ts b/x-pack/test/licensing_plugin/config.legacy.ts deleted file mode 100644 index 14eadc3194f9e3..00000000000000 --- a/x-pack/test/licensing_plugin/config.legacy.ts +++ /dev/null @@ -1,15 +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 { FtrConfigProviderContext } from '@kbn/test/types/ftr'; - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const commonConfig = await readConfigFile(require.resolve('./config')); - - return { - ...commonConfig.getAll(), - testFiles: [require.resolve('./legacy')], - }; -} diff --git a/x-pack/test/licensing_plugin/legacy/index.ts b/x-pack/test/licensing_plugin/legacy/index.ts deleted file mode 100644 index 6274bd39690420..00000000000000 --- a/x-pack/test/licensing_plugin/legacy/index.ts +++ /dev/null @@ -1,16 +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 { FtrProviderContext } from '../services'; - -// eslint-disable-next-line import/no-default-export -export default function ({ loadTestFile }: FtrProviderContext) { - describe('Legacy licensing plugin', function () { - this.tags('ciGroup2'); - // MUST BE LAST! CHANGES LICENSE TYPE! - loadTestFile(require.resolve('./updates')); - }); -} diff --git a/x-pack/test/licensing_plugin/legacy/updates.ts b/x-pack/test/licensing_plugin/legacy/updates.ts deleted file mode 100644 index 1de8659672d2f5..00000000000000 --- a/x-pack/test/licensing_plugin/legacy/updates.ts +++ /dev/null @@ -1,58 +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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../services'; -import { createScenario } from '../scenario'; -import '../../../../test/plugin_functional/plugins/core_provider_plugin/types'; - -// eslint-disable-next-line import/no-default-export -export default function (ftrContext: FtrProviderContext) { - const { getService } = ftrContext; - const supertest = getService('supertest'); - const testSubjects = getService('testSubjects'); - - const scenario = createScenario(ftrContext); - - describe('changes in license types', () => { - after(async () => { - await scenario.teardown(); - }); - - it('provides changes in license types', async () => { - await scenario.setup(); - await scenario.waitForPluginToDetectLicenseUpdate(); - - const { - body: legacyInitialLicense, - header: legacyInitialLicenseHeaders, - } = await supertest.get('/api/xpack/v1/info').expect(200); - - expect(legacyInitialLicense.license?.type).to.be('basic'); - expect(legacyInitialLicenseHeaders['kbn-xpack-sig']).to.be.a('string'); - - await scenario.startTrial(); - await scenario.waitForPluginToDetectLicenseUpdate(); - - const { body: legacyTrialLicense, header: legacyTrialLicenseHeaders } = await supertest - .get('/api/xpack/v1/info') - .expect(200); - - expect(legacyTrialLicense.license?.type).to.be('trial'); - expect(legacyTrialLicenseHeaders['kbn-xpack-sig']).to.not.be( - legacyInitialLicenseHeaders['kbn-xpack-sig'] - ); - - await scenario.startBasic(); - await scenario.waitForPluginToDetectLicenseUpdate(); - - const { body: legacyBasicLicense } = await supertest.get('/api/xpack/v1/info').expect(200); - expect(legacyBasicLicense.license?.type).to.be('basic'); - - // banner shown only when license expired not just deleted - await testSubjects.missingOrFail('licenseExpiredBanner'); - }); - }); -} From b8c7945b124351ec38d5b1b46790de42da3091c4 Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Tue, 1 Sep 2020 13:29:35 +0300 Subject: [PATCH 138/216] [TSVB] Fix panel config updates on history changes (#75896) * Apply history changes to panel config * Add functional tests * Update jest snapshot * Add waiters for stabilizing vis chart * Fix comments * Remove unused method isValidKueryQuery * Add a waiter for component render Co-authored-by: Elastic Machine --- .../application/components/panel_config.js | 4 +- .../components/panel_config/gauge.js | 4 +- .../components/panel_config/markdown.js | 4 +- .../components/panel_config/metric.js | 4 +- .../components/panel_config/table.js | 4 +- .../components/panel_config/timeseries.js | 5 +- .../components/panel_config/top_n.js | 4 +- .../splits/__snapshots__/terms.test.js.snap | 1 + .../application/components/splits/terms.js | 1 + .../application/components/vis_editor.js | 31 ++++---- test/functional/apps/visualize/_tsvb_chart.ts | 75 +++++++++++++++++++ .../page_objects/visual_builder_page.ts | 44 +++++++++++ 12 files changed, 153 insertions(+), 28 deletions(-) diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config.js index c950c39f06a66a..3b081d8eb7db9e 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config.js @@ -67,7 +67,9 @@ export function PanelConfig(props) { return ( - +
+ +
); diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.js index 18380680283ef5..7f26701e38277a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.js @@ -330,7 +330,7 @@ class GaugePanelConfigUi extends Component { ); } return ( -
+ <> this.switchTab('data')}> {view} -
+ ); } } diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js index 03d84d67ea7879..36d0e3a80e227d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js @@ -294,7 +294,7 @@ class MarkdownPanelConfigUi extends Component { ); } return ( -
+ <> {view} -
+ ); } } diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/metric.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/metric.js index feb809f20995a9..568194c2e09184 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/metric.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/metric.js @@ -167,7 +167,7 @@ export class MetricPanelConfig extends Component { ); } return ( -
+ <> this.switchTab('data')}> {view} -
+ ); } } diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js index 2b729e403898e9..bb3f0041abca7f 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js @@ -269,7 +269,7 @@ export class TablePanelConfig extends Component { ); } return ( -
+ <> this.switchTab('data')}> {view} -
+ ); } } diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.js index 3e5e335a9ea397..03da52b10f08b5 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.js @@ -44,6 +44,7 @@ import { import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { getDefaultQueryLanguage } from '../lib/get_default_query_language'; import { QueryBarWrapper } from '../query_bar_wrapper'; + class TimeseriesPanelConfigUi extends Component { constructor(props) { super(props); @@ -401,7 +402,7 @@ class TimeseriesPanelConfigUi extends Component { ); } return ( -
+ <> this.switchTab('data')}> {view} -
+ ); } } diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/top_n.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/top_n.js index 4752e0ee7de65e..14c4400180280f 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/top_n.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/top_n.js @@ -227,7 +227,7 @@ export class TopNPanelConfig extends Component { ); } return ( -
+ <> this.switchTab('data')}> {view} -
+ ); } } diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap index 3bf8b77621cf5e..daff68e40dbaea 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap +++ b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap @@ -44,6 +44,7 @@ exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js labelType="label" > { - if (filterQuery && filterQuery.language === 'kuery') { - try { - const queryOptions = this.coreContext.uiSettings.get( - UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS - ); - esKuery.fromKueryExpression(filterQuery.query, { allowLeadingWildcards: queryOptions }); - } catch (error) { - return false; - } - } - return true; - }; - handleChange = (partialModel) => { if (isEmpty(partialModel)) { return; @@ -134,6 +119,14 @@ export class VisEditor extends Component { }); }; + updateModel = () => { + const { params } = this.props.vis.clone(); + + this.setState({ + model: params, + }); + }; + handleCommit = () => { this.updateVisState(); this.setState({ dirty: false }); @@ -219,6 +212,10 @@ export class VisEditor extends Component { componentDidMount() { this.props.renderComplete(); + + if (this.props.isEditorMode && this.props.eventEmitter) { + this.props.eventEmitter.on('updateEditor', this.updateModel); + } } componentDidUpdate() { @@ -227,6 +224,10 @@ export class VisEditor extends Component { componentWillUnmount() { this.updateVisState.cancel(); + + if (this.props.isEditorMode && this.props.eventEmitter) { + this.props.eventEmitter.off('updateEditor', this.updateModel); + } } } diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 74d5798d127c3e..06828e8e98cce7 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -21,6 +21,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { + const browser = getService('browser'); const esArchiver = getService('esArchiver'); const log = getService('log'); const inspector = getService('inspector'); @@ -146,5 +147,79 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(newValue).to.eql('10'); }); }); + + describe('browser history changes', () => { + it('should activate previous/next chart tab and panel config', async () => { + await PageObjects.visualBuilder.resetPage(); + + log.debug('Click metric chart'); + await PageObjects.visualBuilder.clickMetric(); + await PageObjects.visualBuilder.checkMetricTabIsPresent(); + await PageObjects.visualBuilder.checkTabIsSelected('metric'); + + log.debug('Click Top N chart'); + await PageObjects.visualBuilder.clickTopN(); + await PageObjects.visualBuilder.checkTopNTabIsPresent(); + await PageObjects.visualBuilder.checkTabIsSelected('top_n'); + + log.debug('Go back in browser history'); + await browser.goBack(); + + log.debug('Check metric chart and panel config is rendered'); + await PageObjects.visualBuilder.checkMetricTabIsPresent(); + await PageObjects.visualBuilder.checkTabIsSelected('metric'); + await PageObjects.visualBuilder.checkPanelConfigIsPresent('metric'); + + log.debug('Go back in browser history'); + await browser.goBack(); + + log.debug('Check timeseries chart and panel config is rendered'); + await PageObjects.visualBuilder.checkTimeSeriesChartIsPresent(); + await PageObjects.visualBuilder.checkTabIsSelected('timeseries'); + await PageObjects.visualBuilder.checkPanelConfigIsPresent('timeseries'); + + log.debug('Go forward in browser history'); + await browser.goForward(); + + log.debug('Check metric chart and panel config is rendered'); + await PageObjects.visualBuilder.checkMetricTabIsPresent(); + await PageObjects.visualBuilder.checkTabIsSelected('metric'); + await PageObjects.visualBuilder.checkPanelConfigIsPresent('metric'); + }); + + it('should update panel config', async () => { + await PageObjects.visualBuilder.resetPage(); + + const initialLegendItems = ['Count: 156']; + const finalLegendItems = ['jpg: 106', 'css: 22', 'png: 14', 'gif: 8', 'php: 6']; + + log.debug('Group metrics by terms: extension.raw'); + await PageObjects.visualBuilder.setMetricsGroupByTerms('extension.raw'); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + const legendItems1 = await PageObjects.visualBuilder.getLegendItemsContent(); + expect(legendItems1).to.eql(finalLegendItems); + + log.debug('Go back in browser history'); + await browser.goBack(); + const isTermsSelected = await PageObjects.visualBuilder.checkSelectedMetricsGroupByValue( + 'Terms' + ); + expect(isTermsSelected).to.be(true); + + log.debug('Go back in browser history'); + await browser.goBack(); + await PageObjects.visualBuilder.checkSelectedMetricsGroupByValue('Everything'); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + const legendItems2 = await PageObjects.visualBuilder.getLegendItemsContent(); + expect(legendItems2).to.eql(initialLegendItems); + + log.debug('Go forward twice in browser history'); + await browser.goForward(); + await browser.goForward(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + const legendItems3 = await PageObjects.visualBuilder.getLegendItemsContent(); + expect(legendItems3).to.eql(finalLegendItems); + }); + }); }); } diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index f376c39ff67bbe..a95d535281fa33 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -64,6 +64,19 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro } } + public async checkTabIsSelected(chartType: string) { + const chartTypeBtn = await testSubjects.find(`${chartType}TsvbTypeBtn`); + const isSelected = await chartTypeBtn.getAttribute('aria-selected'); + + if (isSelected !== 'true') { + throw new Error(`TSVB ${chartType} tab is not selected`); + } + } + + public async checkPanelConfigIsPresent(chartType: string) { + await testSubjects.existOrFail(`tvbPanelConfig__${chartType}`); + } + public async checkVisualBuilderIsPresent() { await this.checkTabIsLoaded('tvbVisEditor', 'Time Series'); } @@ -558,9 +571,40 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro return await find.allByCssSelector('.echLegendItem'); } + public async getLegendItemsContent(): Promise { + const legendList = await find.byCssSelector('.echLegendList'); + const $ = await legendList.parseDomContent(); + + return $('li') + .toArray() + .map((li) => { + const label = $(li).find('.echLegendItem__label').text(); + const value = $(li).find('.echLegendItem__extra').text(); + + return `${label}: ${value}`; + }); + } + public async getSeries(): Promise { return await find.allByCssSelector('.tvbSeriesEditor'); } + + public async setMetricsGroupByTerms(field: string) { + const groupBy = await find.byCssSelector( + '.tvbAggRow--split [data-test-subj="comboBoxInput"]' + ); + await comboBox.setElement(groupBy, 'Terms', { clickWithMouse: true }); + await PageObjects.common.sleep(1000); + const byField = await testSubjects.find('groupByField'); + await comboBox.setElement(byField, field, { clickWithMouse: true }); + } + + public async checkSelectedMetricsGroupByValue(value: string) { + const groupBy = await find.byCssSelector( + '.tvbAggRow--split [data-test-subj="comboBoxInput"]' + ); + return await comboBox.isOptionSelected(groupBy, value); + } } return new VisualBuilderPage(); From a0b64baadb22422fb83c9d444569e5b088512af1 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Tue, 1 Sep 2020 13:32:49 +0200 Subject: [PATCH 139/216] Align extract-zip 2.x versions (#76350) --- yarn.lock | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/yarn.lock b/yarn.lock index bd02196e487969..c0c2305609f580 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12673,18 +12673,7 @@ extract-zip@1.7.0, extract-zip@^1.6.6, extract-zip@^1.7.0: mkdirp "^0.5.4" yauzl "^2.10.0" -extract-zip@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.0.tgz#f53b71d44f4ff5a4527a2259ade000fb8b303492" - integrity sha512-i42GQ498yibjdvIhivUsRslx608whtGoFIhF26Z7O4MYncBxp8CwalOs1lnHy21A9sIohWO2+uiE4SRtC9JXDg== - dependencies: - debug "^4.1.1" - get-stream "^5.1.0" - yauzl "^2.10.0" - optionalDependencies: - "@types/yauzl" "^2.9.1" - -extract-zip@^2.0.1: +extract-zip@^2.0.0, extract-zip@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== From bf7b4782e633b87909303930af4083b5002552f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 1 Sep 2020 12:38:45 +0100 Subject: [PATCH 140/216] [APM] Remove additional "No data" message and re-ordering charts (#75399) * removing extra message * adding x-axis values when no data is available * reordering charts * fixing internationalization * fixing transaction RUM agent order * addressing PR comment * removing kpis list and show it in the chart legend instead * fixing API test * moving asPercent to the common directory * fixing api test * fixing unit test * removing unused prop * fixing unit test Co-authored-by: Elastic Machine --- .../utils}/formatters.test.ts | 2 +- x-pack/plugins/apm/common/utils/formatters.ts | 28 + .../ServiceMap/Popover/ServiceStatsList.tsx | 3 +- .../app/ServiceNodeOverview/index.tsx | 7 +- .../WaterfallWithSummmary/PercentOfParent.tsx | 2 +- .../app/TransactionDetails/index.tsx | 39 +- .../app/TransactionOverview/index.tsx | 15 - .../TransactionBreakdownGraph/index.tsx | 31 +- .../TransactionBreakdownKpiList.tsx | 75 --- .../shared/TransactionBreakdown/index.tsx | 27 +- .../shared/charts/CustomPlot/index.js | 14 +- .../shared/charts/CustomPlot/plotUtils.tsx | 2 +- .../__snapshots__/CustomPlot.test.js.snap | 580 +++++++++--------- .../ErroneousTransactionsRateChart/index.tsx | 4 +- .../shared/charts/MetricsChart/index.tsx | 2 +- .../TransactionLineChart/index.tsx | 3 + .../shared/charts/TransactionCharts/index.tsx | 81 +-- .../public/hooks/useTransactionBreakdown.ts | 2 +- .../apm/public/utils/formatters/formatters.ts | 21 - .../lib/transactions/breakdown/index.test.ts | 33 +- .../lib/transactions/breakdown/index.ts | 20 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../tests/transaction_groups/breakdown.ts | 26 +- .../expectation/breakdown.json | 34 +- .../breakdown_transaction_name.json | 55 -- 26 files changed, 469 insertions(+), 639 deletions(-) rename x-pack/plugins/apm/{public/utils/formatters/__test__ => common/utils}/formatters.test.ts (96%) create mode 100644 x-pack/plugins/apm/common/utils/formatters.ts delete mode 100644 x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx delete mode 100644 x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown_transaction_name.json diff --git a/x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts b/x-pack/plugins/apm/common/utils/formatters.test.ts similarity index 96% rename from x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts rename to x-pack/plugins/apm/common/utils/formatters.test.ts index 66101baf3a7461..ce317c5a6a8271 100644 --- a/x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts +++ b/x-pack/plugins/apm/common/utils/formatters.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { asPercent } from '../formatters'; +import { asPercent } from './formatters'; describe('formatters', () => { describe('asPercent', () => { diff --git a/x-pack/plugins/apm/common/utils/formatters.ts b/x-pack/plugins/apm/common/utils/formatters.ts new file mode 100644 index 00000000000000..f7c609d949adf7 --- /dev/null +++ b/x-pack/plugins/apm/common/utils/formatters.ts @@ -0,0 +1,28 @@ +/* + * 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 numeral from '@elastic/numeral'; + +export function asPercent( + numerator: number, + denominator: number | undefined, + fallbackResult = '' +) { + if (!denominator || isNaN(numerator)) { + return fallbackResult; + } + + const decimal = numerator / denominator; + + // 33.2 => 33% + // 3.32 => 3.3% + // 0 => 0% + if (Math.abs(decimal) >= 0.1 || decimal === 0) { + return numeral(decimal).format('0%'); + } + + return numeral(decimal).format('0.0%'); +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx index be52018735099a..ba4451c37b3046 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx @@ -8,8 +8,9 @@ import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; import React from 'react'; import styled from 'styled-components'; +import { asPercent } from '../../../../../common/utils/formatters'; import { ServiceNodeStats } from '../../../../../common/service_map'; -import { asDuration, asPercent, tpmUnit } from '../../../../utils/formatters'; +import { asDuration, tpmUnit } from '../../../../utils/formatters'; export const ItemRow = styled('tr')` line-height: 2; diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx index 3cde48aa483cba..9940a7aabb2190 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx @@ -13,6 +13,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; +import { asPercent } from '../../../../common/utils/formatters'; import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../common/i18n'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; import { Projection } from '../../../../common/projections'; @@ -20,11 +21,7 @@ import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ManagedTable, ITableColumn } from '../../shared/ManagedTable'; import { useFetcher } from '../../../hooks/useFetcher'; -import { - asDynamicBytes, - asInteger, - asPercent, -} from '../../../utils/formatters'; +import { asDynamicBytes, asInteger } from '../../../utils/formatters'; import { ServiceNodeMetricOverviewLink } from '../../shared/Links/apm/ServiceNodeMetricOverviewLink'; import { truncate, px, unit } from '../../../style/variables'; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx index 1725a13f4da66c..41c958838803e2 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiToolTip } from '@elastic/eui'; -import { asPercent } from '../../../../utils/formatters'; +import { asPercent } from '../../../../../common/utils/formatters'; interface PercentOfParentProps { duration: number; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx index 0dc2f607b1ef27..515fcbc88c9014 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -5,32 +5,29 @@ */ import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, EuiPanel, EuiSpacer, EuiTitle, - EuiHorizontalRule, - EuiFlexGroup, - EuiFlexItem, } from '@elastic/eui'; import React, { useMemo } from 'react'; -import { EuiFlexGrid } from '@elastic/eui'; +import { useTrackPageview } from '../../../../../observability/public'; +import { Projection } from '../../../../common/projections'; +import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; +import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { useLocation } from '../../../hooks/useLocation'; import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; import { useTransactionDistribution } from '../../../hooks/useTransactionDistribution'; +import { useUrlParams } from '../../../hooks/useUrlParams'; import { useWaterfall } from '../../../hooks/useWaterfall'; -import { TransactionCharts } from '../../shared/charts/TransactionCharts'; import { ApmHeader } from '../../shared/ApmHeader'; +import { TransactionCharts } from '../../shared/charts/TransactionCharts'; +import { HeightRetainer } from '../../shared/HeightRetainer'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { TransactionDistribution } from './Distribution'; import { WaterfallWithSummmary } from './WaterfallWithSummmary'; -import { useLocation } from '../../../hooks/useLocation'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; -import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; -import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; -import { useTrackPageview } from '../../../../../observability/public'; -import { Projection } from '../../../../common/projections'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { HeightRetainer } from '../../shared/HeightRetainer'; -import { ErroneousTransactionsRateChart } from '../../shared/charts/ErroneousTransactionsRateChart'; export function TransactionDetails() { const location = useLocation(); @@ -86,21 +83,9 @@ export function TransactionDetails() { - - - - - - - - - - - diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index 0757ae572152c9..81d8a6f8073753 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -7,7 +7,6 @@ import { EuiCallOut, EuiCode, - EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, @@ -29,13 +28,11 @@ import { useServiceTransactionTypes } from '../../../hooks/useServiceTransaction import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; import { useTransactionList } from '../../../hooks/useTransactionList'; import { useUrlParams } from '../../../hooks/useUrlParams'; -import { ErroneousTransactionsRateChart } from '../../shared/charts/ErroneousTransactionsRateChart'; import { TransactionCharts } from '../../shared/charts/TransactionCharts'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter'; -import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; import { TransactionList } from './List'; import { useRedirect } from './useRedirect'; @@ -125,20 +122,8 @@ export function TransactionOverview() { - - - - - - - - - - - diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx index 209657971620bb..b908eb8da4d03e 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx @@ -4,19 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; import { throttle } from 'lodash'; +import React, { useMemo } from 'react'; +import { asPercent } from '../../../../../common/utils/formatters'; +import { useUiTracker } from '../../../../../../observability/public'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; -import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; import { Maybe } from '../../../../../typings/common'; -import { TransactionLineChart } from '../../charts/TransactionCharts/TransactionLineChart'; -import { asPercent } from '../../../../utils/formatters'; -import { unit } from '../../../../style/variables'; +import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; -import { useUiTracker } from '../../../../../../observability/public'; +import { getEmptySeries } from '../../charts/CustomPlot/getEmptySeries'; +import { TransactionLineChart } from '../../charts/TransactionCharts/TransactionLineChart'; interface Props { timeseries: TimeSeries[]; + noHits: boolean; } const tickFormatY = (y: Maybe) => { @@ -29,8 +31,9 @@ const formatTooltipValue = (coordinate: Coordinate) => { : NOT_AVAILABLE_LABEL; }; -function TransactionBreakdownGraph(props: Props) { - const { timeseries } = props; +function TransactionBreakdownGraph({ timeseries, noHits }: Props) { + const { urlParams } = useUrlParams(); + const { rangeFrom, rangeTo } = urlParams; const trackApmEvent = useUiTracker({ app: 'apm' }); const handleHover = useMemo( () => @@ -38,15 +41,23 @@ function TransactionBreakdownGraph(props: Props) { [trackApmEvent] ); + const emptySeries = + rangeFrom && rangeTo + ? getEmptySeries( + new Date(rangeFrom).getTime(), + new Date(rangeTo).getTime() + ) + : []; + return ( ); } diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx deleted file mode 100644 index d3761cf0fe38e3..00000000000000 --- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx +++ /dev/null @@ -1,75 +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 React from 'react'; -import { - EuiFlexGrid, - EuiFlexItem, - EuiFlexGroup, - EuiText, - EuiTitle, - EuiIcon, -} from '@elastic/eui'; -import styled from 'styled-components'; -import { asPercent } from '../../../utils/formatters'; - -interface TransactionBreakdownKpi { - name: string; - percentage: number; - color: string; -} - -interface Props { - kpis: TransactionBreakdownKpi[]; -} - -const Description = styled.span` - { - white-space: nowrap; - } -`; - -function KpiDescription({ name, color }: { name: string; color: string }) { - return ( - - - - - - - {name} - - - - ); -} - -function TransactionBreakdownKpiList({ kpis }: Props) { - return ( - - {kpis.map((kpi) => ( - - - - - - - - {asPercent(kpi.percentage, 1)} - - - - - ))} - - ); -} - -export { TransactionBreakdownKpiList }; diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx index 80ed9163ec08d4..55826497ca3858 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx @@ -3,28 +3,18 @@ * 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, - EuiFlexItem, - EuiPanel, - EuiText, - EuiTitle, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { isEmpty } from 'lodash'; import React from 'react'; import { FETCH_STATUS } from '../../../hooks/useFetcher'; import { useTransactionBreakdown } from '../../../hooks/useTransactionBreakdown'; import { TransactionBreakdownGraph } from './TransactionBreakdownGraph'; -import { TransactionBreakdownKpiList } from './TransactionBreakdownKpiList'; - -const emptyMessage = i18n.translate('xpack.apm.transactionBreakdown.noData', { - defaultMessage: 'No data within this time range.', -}); function TransactionBreakdown() { const { data, status } = useTransactionBreakdown(); - const { kpis, timeseries } = data; - const noHits = data.kpis.length === 0 && status === FETCH_STATUS.SUCCESS; + const { timeseries } = data; + const noHits = isEmpty(timeseries) && status === FETCH_STATUS.SUCCESS; return ( @@ -39,14 +29,7 @@ function TransactionBreakdown() { - {noHits ? ( - {emptyMessage} - ) : ( - - )} - - - + diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js index 41925d651e3617..501d30b5e2ba12 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js @@ -67,7 +67,7 @@ export class InnerCustomPlot extends PureComponent { (series) => { return series.slice( 0, - VISIBLE_LEGEND_COUNT + getHiddenLegendCount(series) + this.props.visibleLegendCount + getHiddenLegendCount(series) ); } ); @@ -128,14 +128,20 @@ export class InnerCustomPlot extends PureComponent { } render() { - const { series, truncateLegends, width, annotations } = this.props; + const { + series, + truncateLegends, + width, + annotations, + visibleLegendCount, + } = this.props; if (!width) { return null; } const hiddenSeriesCount = Math.max( - series.length - VISIBLE_LEGEND_COUNT - getHiddenLegendCount(series), + series.length - visibleLegendCount - getHiddenLegendCount(series), 0 ); const visibleSeries = this.getVisibleSeries({ series }); @@ -239,6 +245,7 @@ InnerCustomPlot.propTypes = { }) ), noHits: PropTypes.bool, + visibleLegendCount: PropTypes.number, onToggleLegend: PropTypes.func, }; @@ -249,6 +256,7 @@ InnerCustomPlot.defaultProps = { truncateLegends: false, xAxisTickSizeOuter: 0, noHits: false, + visibleLegendCount: VISIBLE_LEGEND_COUNT, }; export default makeWidthFlexible(InnerCustomPlot); diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx index 7721375a404c40..67b7fd31b05bc3 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx @@ -19,7 +19,7 @@ const XY_HEIGHT = unit * 16; const XY_MARGIN = { top: unit, left: unit * 5, - right: 0, + right: unit, bottom: unit * 2, }; diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap index f413610ebd9844..20636fa1444797 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap @@ -82,7 +82,7 @@ Array [ className="rv-xy-plot__axis__line" style={Object {}} x1={0} - x2={720} + x2={704} y1={0} y2={0} /> @@ -93,7 +93,7 @@ Array [ @@ -434,7 +434,7 @@ Array [ > `; @@ -3018,7 +3018,7 @@ Array [ className="rv-xy-plot__axis__line" style={Object {}} x1={0} - x2={720} + x2={704} y1={0} y2={0} /> @@ -3029,7 +3029,7 @@ Array [ @@ -3370,7 +3370,7 @@ Array [ > @@ -5132,7 +5132,7 @@ Array [ rv-hint--verticalAlign-bottom" style={ Object { - "left": 440, + "left": 432, "position": "absolute", "top": 120, } @@ -5339,7 +5339,7 @@ Array [ > @@ -6097,7 +6097,7 @@ Array [ diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx index 3b6d1684e08e1c..1676d3f68b57cf 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx @@ -8,11 +8,12 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import React, { useCallback } from 'react'; import { EuiPanel } from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; +import { asPercent } from '../../../../../common/utils/formatters'; import { useChartsSync } from '../../../../hooks/useChartsSync'; import { useFetcher } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { callApmApi } from '../../../../services/rest/createCallApmApi'; -import { asPercent } from '../../../../utils/formatters'; // @ts-ignore import CustomPlot from '../CustomPlot'; @@ -71,6 +72,7 @@ export function ErroneousTransactionsRateChart() { })} + void; + visibleLegendCount?: number; onToggleLegend?: (disabledSeriesState: boolean[]) => void; } @@ -32,6 +33,7 @@ function TransactionLineChart(props: Props) { truncateLegends, stacked = false, onHover, + visibleLegendCount, onToggleLegend, } = props; @@ -59,6 +61,7 @@ function TransactionLineChart(props: Props) { height={height} truncateLegends={truncateLegends} {...(stacked ? { stackBy: 'y' } : {})} + visibleLegendCount={visibleLegendCount} onToggleLegend={onToggleLegend} /> ); diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx index d11925dc0303d2..6ba080a07b9d38 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx @@ -13,7 +13,6 @@ import { EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Location } from 'history'; import React from 'react'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { @@ -29,6 +28,8 @@ import { asDecimal, tpmUnit } from '../../../../utils/formatters'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; import { BrowserLineChart } from './BrowserLineChart'; import { DurationByCountryMap } from './DurationByCountryMap'; +import { ErroneousTransactionsRateChart } from '../ErroneousTransactionsRateChart'; +import { TransactionBreakdown } from '../../TransactionBreakdown'; import { getResponseTimeTickFormatter, getResponseTimeTooltipFormatter, @@ -39,13 +40,11 @@ import { useFormatter } from './use_formatter'; interface TransactionChartProps { charts: ITransactionChartData; - location: Location; urlParams: IUrlParams; } export function TransactionCharts({ charts, - location, urlParams, }: TransactionChartProps) { const getTPMFormatter = (t: number) => { @@ -72,48 +71,56 @@ export function TransactionCharts({ - - - - - {responseTimeLabel(transactionType)} - - - - {(license) => ( - - )} - - - - + + + + {responseTimeLabel(transactionType)} + + + + {(license) => ( + + )} + + + - - - {tpmLabel(transactionType)} - - - + + {tpmLabel(transactionType)} + + + + + + + + + + + + + + {transactionType === TRANSACTION_PAGE_LOAD && ( <> diff --git a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts index 2449c13f294352..9db78fde2d8c86 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts @@ -13,7 +13,7 @@ export function useTransactionBreakdown() { uiFilters, } = useUrlParams(); - const { data = { kpis: [], timeseries: [] }, error, status } = useFetcher( + const { data = { timeseries: [] }, error, status } = useFetcher( (callApmApi) => { if (serviceName && start && end && transactionType) { return callApmApi({ diff --git a/x-pack/plugins/apm/public/utils/formatters/formatters.ts b/x-pack/plugins/apm/public/utils/formatters/formatters.ts index 649f11063b149a..6249ce53b6779e 100644 --- a/x-pack/plugins/apm/public/utils/formatters/formatters.ts +++ b/x-pack/plugins/apm/public/utils/formatters/formatters.ts @@ -23,24 +23,3 @@ export function tpmUnit(type?: string) { defaultMessage: 'tpm', }); } - -export function asPercent( - numerator: number, - denominator: number | undefined, - fallbackResult = '' -) { - if (!denominator || isNaN(numerator)) { - return fallbackResult; - } - - const decimal = numerator / denominator; - - // 33.2 => 33% - // 3.32 => 3.3% - // 0 => 0% - if (Math.abs(decimal) >= 0.1 || decimal === 0) { - return numeral(decimal).format('0%'); - } - - return numeral(decimal).format('0.0%'); -} diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts index 731f75226cbe4c..e943214b0b5177 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts @@ -50,40 +50,9 @@ describe('getTransactionBreakdown', () => { setup: getMockSetup(noDataResponse), }); - expect(response.kpis.length).toBe(0); - expect(Object.keys(response.timeseries).length).toBe(0); }); - it('returns transaction breakdowns grouped by type and subtype', async () => { - const response = await getTransactionBreakdown({ - serviceName: 'myServiceName', - transactionType: 'request', - setup: getMockSetup(dataResponse), - }); - - expect(response.kpis.length).toBe(4); - - expect(response.kpis.map((kpi) => kpi.name)).toEqual([ - 'app', - 'dispatcher-servlet', - 'http', - 'postgresql', - ]); - - expect(response.kpis[0]).toEqual({ - name: 'app', - color: '#54b399', - percentage: 0.5408550899466306, - }); - - expect(response.kpis[3]).toEqual({ - name: 'postgresql', - color: '#9170b8', - percentage: 0.047366859295002, - }); - }); - it('returns a timeseries grouped by type and subtype', async () => { const response = await getTransactionBreakdown({ serviceName: 'myServiceName', @@ -98,7 +67,7 @@ describe('getTransactionBreakdown', () => { const appTimeseries = timeseries[0]; expect(appTimeseries.title).toBe('app'); expect(appTimeseries.type).toBe('areaStacked'); - expect(appTimeseries.hideLegend).toBe(true); + expect(appTimeseries.hideLegend).toBe(false); // empty buckets should result in null values for visible types expect(appTimeseries.data.length).toBe(276); diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts index fbdddea32deb4f..9730ddbbf38d7c 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -5,6 +5,7 @@ */ import { flatten, orderBy, last } from 'lodash'; +import { asPercent } from '../../../../common/utils/formatters'; import { ProcessorEvent } from '../../../../common/processor_event'; import { SERVICE_NAME, @@ -149,9 +150,16 @@ export async function getTransactionBreakdown({ ) : []; - const kpis = orderBy(visibleKpis, 'name').map((kpi, index) => { - return { + const kpis = orderBy( + visibleKpis.map((kpi) => ({ ...kpi, + lowerCaseName: kpi.name.toLowerCase(), + })), + 'lowerCaseName' + ).map((kpi, index) => { + const { lowerCaseName, ...rest } = kpi; + return { + ...rest, color: getVizColorForIndex(index), }; }); @@ -213,11 +221,9 @@ export async function getTransactionBreakdown({ color: kpi.color, type: 'areaStacked', data: timeseriesPerSubtype[kpi.name], - hideLegend: true, + hideLegend: false, + legendValue: asPercent(kpi.percentage, 1), })); - return { - kpis, - timeseries, - }; + return { timeseries }; } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 89a020bd044cc3..b45598cb4f9985 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5042,7 +5042,6 @@ "xpack.apm.transactionActionMenu.viewInUptime": "ステータス", "xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "サンプルドキュメントを表示", "xpack.apm.transactionBreakdown.chartTitle": "スパンタイプ別時間", - "xpack.apm.transactionBreakdown.noData": "この時間範囲のデータがありません。", "xpack.apm.transactionCardinalityWarning.body": "一意のトランザクション名の数が構成された値{bucketSize}を超えています。エージェントを再構成し、類似したトランザクションをグループ化するか、{codeBlock}の値を増やしてください。", "xpack.apm.transactionCardinalityWarning.docsLink": "詳細はドキュメントをご覧ください", "xpack.apm.transactionCardinalityWarning.title": "このビューには、報告されたトランザクションのサブセットが表示されます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 68c1b5dab92955..248a6986bb2a3d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5044,7 +5044,6 @@ "xpack.apm.transactionActionMenu.viewInUptime": "状态", "xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "查看样例文档", "xpack.apm.transactionBreakdown.chartTitle": "跨度类型花费的时间", - "xpack.apm.transactionBreakdown.noData": "此时间范围内没有数据。", "xpack.apm.transactionCardinalityWarning.body": "唯一事务名称的数目超过 {bucketSize} 的已配置值。尝试重新配置您的代理以对类似的事务分组或增大 {codeBlock} 的值", "xpack.apm.transactionCardinalityWarning.docsLink": "在文档中了解详情", "xpack.apm.transactionCardinalityWarning.title": "此视图显示已报告事务的子集。", diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts index 5b61112a374c12..0b94abaa158909 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts @@ -6,7 +6,6 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import expectedBreakdown from './expectation/breakdown.json'; -import expectedBreakdownWithTransactionName from './expectation/breakdown_transaction_name.json'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -25,7 +24,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { `/api/apm/services/opbeans-node/transaction_groups/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}` ); expect(response.status).to.be(200); - expect(response.body).to.eql({ kpis: [], timeseries: [] }); + expect(response.body).to.eql({ timeseries: [] }); }); }); @@ -47,15 +46,32 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expect(response.body).to.eql(expectedBreakdownWithTransactionName); + const { timeseries } = response.body; + const { title, color, type, data, hideLegend, legendValue } = timeseries[0]; + expect(data).to.eql([ + { x: 1593413100000, y: null }, + { x: 1593413130000, y: null }, + { x: 1593413160000, y: null }, + { x: 1593413190000, y: null }, + { x: 1593413220000, y: null }, + { x: 1593413250000, y: null }, + { x: 1593413280000, y: null }, + { x: 1593413310000, y: 1 }, + { x: 1593413340000, y: null }, + ]); + expect(title).to.be('app'); + expect(color).to.be('#54b399'); + expect(type).to.be('areaStacked'); + expect(hideLegend).to.be(false); + expect(legendValue).to.be('100%'); }); - it('returns the top 4 by percentage and sorts them by name', async () => { + it('returns the transaction breakdown sorted by name', async () => { const response = await supertest.get( `/api/apm/services/opbeans-node/transaction_groups/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}` ); expect(response.status).to.be(200); - expect(response.body.kpis.map((kpi: { name: string }) => kpi.name)).to.eql([ + expect(response.body.timeseries.map((serie: { title: string }) => serie.title)).to.eql([ 'app', 'http', 'postgresql', diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json index 3b884a9eb7907d..8ffbba64ec7aba 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json @@ -1,26 +1,4 @@ { - "kpis":[ - { - "name":"app", - "percentage":0.16700861715223636, - "color":"#54b399" - }, - { - "name":"http", - "percentage":0.7702092736971686, - "color":"#6092c0" - }, - { - "name":"postgresql", - "percentage":0.0508822322527698, - "color":"#d36086" - }, - { - "name":"redis", - "percentage":0.011899876897825195, - "color":"#9170b8" - } - ], "timeseries":[ { "title":"app", @@ -64,7 +42,8 @@ "y":null } ], - "hideLegend":true + "hideLegend":false, + "legendValue": "17%" }, { "title":"http", @@ -108,7 +87,8 @@ "y":null } ], - "hideLegend":true + "hideLegend":false, + "legendValue": "77%" }, { "title":"postgresql", @@ -152,7 +132,8 @@ "y":null } ], - "hideLegend":true + "hideLegend":false, + "legendValue": "5.1%" }, { "title":"redis", @@ -196,7 +177,8 @@ "y":null } ], - "hideLegend":true + "hideLegend":false, + "legendValue": "1.2%" } ] } \ No newline at end of file diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown_transaction_name.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown_transaction_name.json deleted file mode 100644 index b4f8e376d36094..00000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown_transaction_name.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "kpis":[ - { - "name":"app", - "percentage":1, - "color":"#54b399" - } - ], - "timeseries":[ - { - "title":"app", - "color":"#54b399", - "type":"areaStacked", - "data":[ - { - "x":1593413100000, - "y":null - }, - { - "x":1593413130000, - "y":null - }, - { - "x":1593413160000, - "y":null - }, - { - "x":1593413190000, - "y":null - }, - { - "x":1593413220000, - "y":null - }, - { - "x":1593413250000, - "y":null - }, - { - "x":1593413280000, - "y":null - }, - { - "x":1593413310000, - "y":1 - }, - { - "x":1593413340000, - "y":null - } - ], - "hideLegend":true - } - ] -} \ No newline at end of file From 97c8c941f305810b045b398861cd98c0e0b3e67f Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Tue, 1 Sep 2020 08:07:39 -0400 Subject: [PATCH 141/216] [SECURITY_SOLUTION][ENDPOINT] Generate Trusted Apps artifacts and Manifest entries (#74988) * Generate Trusted Apps artifacts + manifest entries * Artifacts mocks support for generating trusted apps entries * Adjusted Manifest + Artifacts tests to account for trusted apps --- .../new/trusted_app_list_item_agnostic.json | 18 +++ .../endpoint/ingest_integration.test.ts | 30 ++++ .../server/endpoint/lib/artifacts/common.ts | 2 + .../endpoint/lib/artifacts/lists.test.ts | 93 ++++++++++-- .../server/endpoint/lib/artifacts/lists.ts | 12 +- .../endpoint/lib/artifacts/manifest.test.ts | 51 +++++++ .../server/endpoint/lib/artifacts/mocks.ts | 15 +- .../schemas/artifacts/saved_objects.mock.ts | 31 +++- .../manifest_manager/manifest_manager.test.ts | 139 ++++++++++++++++-- .../manifest_manager/manifest_manager.ts | 48 +++++- .../apps/endpoint/policy_details.ts | 36 +++++ 11 files changed, 429 insertions(+), 46 deletions(-) create mode 100644 x-pack/plugins/lists/server/scripts/exception_lists/new/trusted_app_list_item_agnostic.json diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/trusted_app_list_item_agnostic.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/trusted_app_list_item_agnostic.json new file mode 100644 index 00000000000000..9f0c306a408f0d --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/trusted_app_list_item_agnostic.json @@ -0,0 +1,18 @@ +{ + "list_id": "endpoint_trusted_apps", + "item_id": "endpoint_trusted_apps_item", + "_tags": ["endpoint", "os:linux", "os:windows", "os:macos", "trusted-app"], + "tags": ["user added string for a tag", "malware"], + "type": "simple", + "description": "This is a sample agnostic endpoint trusted app entry", + "name": "Sample Endpoint Trusted App Entry", + "namespace_type": "agnostic", + "entries": [ + { + "field": "actingProcess.file.signer", + "operator": "included", + "type": "match", + "value": "Elastic, N.V." + } + ] +} diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts index 73f2124736a79b..c28ffcf5b7a3f0 100644 --- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts @@ -49,6 +49,36 @@ describe('ingest_integration tests ', () => { relative_url: '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', }, + 'endpoint-trustlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-linux-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, + 'endpoint-trustlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-macos-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, + 'endpoint-trustlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-windows-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, }, manifest_version: '1.0.0', schema_version: 'v1', diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts index 7f90aa7b910632..457e50b686863a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts @@ -14,6 +14,8 @@ export const ArtifactConstants = { GLOBAL_ALLOWLIST_NAME: 'endpoint-exceptionlist', SAVED_OBJECT_TYPE: 'endpoint:user-artifact', SUPPORTED_OPERATING_SYSTEMS: ['macos', 'windows'], + SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS: ['macos', 'windows', 'linux'], + GLOBAL_TRUSTED_APPS_NAME: 'endpoint-trustlist', }; export const ManifestConstants = { diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts index fea3b2b9a45266..a10ba9d6be38c2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts @@ -11,6 +11,8 @@ import { getExceptionListItemSchemaMock } from '../../../../../lists/common/sche import { EntriesArray, EntryList } from '../../../../../lists/common/schemas/types'; import { buildArtifact, getFullEndpointExceptionList } from './lists'; import { TranslatedEntry, TranslatedExceptionListItem } from '../../schemas/artifacts'; +import { ArtifactConstants } from './common'; +import { ENDPOINT_LIST_ID } from '../../../../../lists/common'; describe('buildEventTypeSignal', () => { let mockExceptionClient: ExceptionListClient; @@ -47,7 +49,12 @@ describe('buildEventTypeSignal', () => { const first = getFoundExceptionListItemSchemaMock(); mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -88,7 +95,12 @@ describe('buildEventTypeSignal', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -134,7 +146,12 @@ describe('buildEventTypeSignal', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -182,7 +199,12 @@ describe('buildEventTypeSignal', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -229,7 +251,12 @@ describe('buildEventTypeSignal', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -267,7 +294,12 @@ describe('buildEventTypeSignal', () => { first.data[1].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -305,7 +337,12 @@ describe('buildEventTypeSignal', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -329,7 +366,12 @@ describe('buildEventTypeSignal', () => { .mockReturnValueOnce(first) .mockReturnValueOnce(second); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); // Expect 2 exceptions, the first two calls returned the same exception list items expect(resp.entries.length).toEqual(2); @@ -340,7 +382,12 @@ describe('buildEventTypeSignal', () => { exceptionsResponse.data = []; exceptionsResponse.total = 0; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(exceptionsResponse); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp.entries.length).toEqual(0); }); @@ -385,8 +432,18 @@ describe('buildEventTypeSignal', () => { ], }; - const artifact1 = await buildArtifact(translatedExceptionList, 'linux', 'v1'); - const artifact2 = await buildArtifact(translatedExceptionListReversed, 'linux', 'v1'); + const artifact1 = await buildArtifact( + translatedExceptionList, + 'linux', + 'v1', + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); + const artifact2 = await buildArtifact( + translatedExceptionListReversed, + 'linux', + 'v1', + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); expect(artifact1.decodedSha256).toEqual(artifact2.decodedSha256); }); @@ -430,8 +487,18 @@ describe('buildEventTypeSignal', () => { entries: translatedItems.reverse(), }; - const artifact1 = await buildArtifact(translatedExceptionList, 'linux', 'v1'); - const artifact2 = await buildArtifact(translatedExceptionListReversed, 'linux', 'v1'); + const artifact1 = await buildArtifact( + translatedExceptionList, + 'linux', + 'v1', + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); + const artifact2 = await buildArtifact( + translatedExceptionListReversed, + 'linux', + 'v1', + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); expect(artifact1.decodedSha256).toEqual(artifact2.decodedSha256); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index e41781dd605a07..731b083f3293c3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -28,19 +28,20 @@ import { internalArtifactCompleteSchema, InternalArtifactCompleteSchema, } from '../../schemas'; -import { ArtifactConstants } from './common'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; export async function buildArtifact( exceptions: WrappedTranslatedExceptionList, os: string, - schemaVersion: string + schemaVersion: string, + name: string ): Promise { const exceptionsBuffer = Buffer.from(JSON.stringify(exceptions)); const sha256 = createHash('sha256').update(exceptionsBuffer.toString()).digest('hex'); // Keep compression info empty in case its a duplicate. Lazily compress before committing if needed. return { - identifier: `${ArtifactConstants.GLOBAL_ALLOWLIST_NAME}-${os}-${schemaVersion}`, + identifier: `${name}-${os}-${schemaVersion}`, compressionAlgorithm: 'none', encryptionAlgorithm: 'none', decodedSha256: sha256, @@ -76,7 +77,8 @@ export function isCompressed(artifact: InternalArtifactSchema) { export async function getFullEndpointExceptionList( eClient: ExceptionListClient, os: string, - schemaVersion: string + schemaVersion: string, + listId: typeof ENDPOINT_LIST_ID | typeof ENDPOINT_TRUSTED_APPS_LIST_ID ): Promise { const exceptions: WrappedTranslatedExceptionList = { entries: [] }; let page = 1; @@ -84,7 +86,7 @@ export async function getFullEndpointExceptionList( while (paging) { const response = await eClient.findExceptionListItem({ - listId: ENDPOINT_LIST_ID, + listId, namespaceType: 'agnostic', filter: `exception-list-agnostic.attributes._tags:\"os:${os}\"`, perPage: 100, diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts index 3d70f7266277f6..507b81b12e10b0 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts @@ -94,6 +94,36 @@ describe('manifest', () => { relative_url: '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', }, + 'endpoint-trustlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + decoded_size: 432, + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + encoded_size: 147, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-linux-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + }, + 'endpoint-trustlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + decoded_size: 432, + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + encoded_size: 147, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-macos-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + }, + 'endpoint-trustlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + decoded_size: 432, + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + encoded_size: 147, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + }, }, manifest_version: '1.0.0', schema_version: 'v1', @@ -107,6 +137,9 @@ describe('manifest', () => { ids: [ 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', ], }); }); @@ -119,6 +152,21 @@ describe('manifest', () => { 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', type: 'delete', }, + { + id: + 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, { id: 'endpoint-exceptionlist-macos-v1-0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', @@ -139,6 +187,9 @@ describe('manifest', () => { expect(keys).toEqual([ 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', ]); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts index 61850bfb3bc7d3..cdfbb551940e1b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts @@ -16,13 +16,20 @@ import { ArtifactConstants } from './common'; import { Manifest } from './manifest'; export const getMockArtifacts = async (opts?: { compress: boolean }) => { - return Promise.all( - ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.map>( + return Promise.all([ + // Exceptions items + ...ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.map>( async (os) => { return getInternalArtifactMock(os, 'v1', opts); } - ) - ); + ), + // Trusted Apps items + ...ArtifactConstants.SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS.map< + Promise + >(async (os) => { + return getInternalArtifactMock(os, 'v1', opts, ArtifactConstants.GLOBAL_TRUSTED_APPS_NAME); + }), + ]); }; export const getMockArtifactsWithDiff = async (opts?: { compress: boolean }) => { diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts index ae565f785c399d..5d46fd0f2456b5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { buildArtifact, maybeCompressArtifact, isCompressed } from '../../lib/artifacts'; +import { + buildArtifact, + maybeCompressArtifact, + isCompressed, + ArtifactConstants, +} from '../../lib/artifacts'; import { getTranslatedExceptionListMock } from './lists.mock'; import { InternalManifestSchema, @@ -25,9 +30,15 @@ const compressArtifact = async (artifact: InternalArtifactCompleteSchema) => { export const getInternalArtifactMock = async ( os: string, schemaVersion: string, - opts?: { compress: boolean } + opts?: { compress: boolean }, + artifactName: string = ArtifactConstants.GLOBAL_ALLOWLIST_NAME ): Promise => { - const artifact = await buildArtifact(getTranslatedExceptionListMock(), os, schemaVersion); + const artifact = await buildArtifact( + getTranslatedExceptionListMock(), + os, + schemaVersion, + artifactName + ); return opts?.compress ? compressArtifact(artifact) : artifact; }; @@ -36,7 +47,12 @@ export const getEmptyInternalArtifactMock = async ( schemaVersion: string, opts?: { compress: boolean } ): Promise => { - const artifact = await buildArtifact({ entries: [] }, os, schemaVersion); + const artifact = await buildArtifact( + { entries: [] }, + os, + schemaVersion, + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); return opts?.compress ? compressArtifact(artifact) : artifact; }; @@ -47,7 +63,12 @@ export const getInternalArtifactMockWithDiffs = async ( ): Promise => { const mock = getTranslatedExceptionListMock(); mock.entries.pop(); - const artifact = await buildArtifact(mock, os, schemaVersion); + const artifact = await buildArtifact( + mock, + os, + schemaVersion, + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); return opts?.compress ? compressArtifact(artifact) : artifact; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index bb6504de6e0a42..40b408166b17f4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -24,11 +24,41 @@ describe('manifest_manager', () => { 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', type: 'delete', }, + { + id: + 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, { id: 'endpoint-exceptionlist-macos-v1-0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', type: 'add', }, + { + id: + 'endpoint-trustlist-macos-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + type: 'add', + }, + { + id: + 'endpoint-trustlist-windows-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + type: 'add', + }, + { + id: + 'endpoint-trustlist-linux-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + type: 'add', + }, ]); }); @@ -44,16 +74,53 @@ describe('manifest_manager', () => { 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', type: 'delete', }, + { + id: + 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, { id: 'endpoint-exceptionlist-macos-v1-0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', type: 'add', }, + { + id: + 'endpoint-trustlist-macos-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + type: 'add', + }, + { + id: + 'endpoint-trustlist-windows-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + type: 'add', + }, + { + id: + 'endpoint-trustlist-linux-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + type: 'add', + }, ]); - const newArtifactId = diffs[1].id; - await newManifest.compressArtifact(newArtifactId); - const artifact = newManifest.getArtifact(newArtifactId)!; + const firstNewArtifactId = diffs.find((diff) => diff.type === 'add')!.id; + + // Compress all `add` artifacts + for (const artifactDiff of diffs) { + if (artifactDiff.type === 'add') { + await newManifest.compressArtifact(artifactDiff.id); + } + } + + const artifact = newManifest.getArtifact(firstNewArtifactId)!; if (isCompleteArtifact(artifact)) { await manifestManager.pushArtifacts([artifact]); // caches the artifact @@ -61,7 +128,7 @@ describe('manifest_manager', () => { throw new Error('Artifact is missing a body.'); } - const entry = JSON.parse(inflateSync(cache.get(newArtifactId)! as Buffer).toString()); + const entry = JSON.parse(inflateSync(cache.get(firstNewArtifactId)! as Buffer).toString()); expect(entry).toEqual({ entries: [ { @@ -107,8 +174,12 @@ describe('manifest_manager', () => { const oldManifest = await manifestManager.getLastComputedManifest(); const newManifest = await manifestManager.buildNewManifest(oldManifest!); const diffs = newManifest.diff(oldManifest!); - const newArtifactId = diffs[1].id; - await newManifest.compressArtifact(newArtifactId); + + for (const artifactDiff of diffs) { + if (artifactDiff.type === 'add') { + await newManifest.compressArtifact(artifactDiff.id); + } + } newManifest.bumpSemanticVersion(); @@ -145,6 +216,36 @@ describe('manifest_manager', () => { relative_url: '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', }, + 'endpoint-trustlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-linux-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, + 'endpoint-trustlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-macos-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, + 'endpoint-trustlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-windows-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, }, }); }); @@ -155,8 +256,12 @@ describe('manifest_manager', () => { const oldManifest = await manifestManager.getLastComputedManifest(); const newManifest = await manifestManager.buildNewManifest(oldManifest!); const diffs = newManifest.diff(oldManifest!); - const newArtifactId = diffs[1].id; - await newManifest.compressArtifact(newArtifactId); + + for (const artifactDiff of diffs) { + if (artifactDiff.type === 'add') { + await newManifest.compressArtifact(artifactDiff.id); + } + } newManifest.bumpSemanticVersion(); @@ -174,11 +279,17 @@ describe('manifest_manager', () => { const oldManifest = await manifestManager.getLastComputedManifest(); const newManifest = await manifestManager.buildNewManifest(oldManifest!); const diffs = newManifest.diff(oldManifest!); - const oldArtifactId = diffs[0].id; - const newArtifactId = diffs[1].id; - await newManifest.compressArtifact(newArtifactId); + const firstOldArtifactId = diffs.find((diff) => diff.type === 'delete')!.id; + const FirstNewArtifactId = diffs.find((diff) => diff.type === 'add')!.id; + + // Compress all new artifacts + for (const artifactDiff of diffs) { + if (artifactDiff.type === 'add') { + await newManifest.compressArtifact(artifactDiff.id); + } + } - const artifact = newManifest.getArtifact(newArtifactId)!; + const artifact = newManifest.getArtifact(FirstNewArtifactId)!; if (isCompleteArtifact(artifact)) { await manifestManager.pushArtifacts([artifact]); } else { @@ -186,7 +297,7 @@ describe('manifest_manager', () => { } await manifestManager.commit(newManifest); - await manifestManager.deleteArtifacts([oldArtifactId]); + await manifestManager.deleteArtifacts([firstOldArtifactId]); // created new artifact expect(savedObjectsClient.create.mock.calls[0][0]).toEqual( @@ -201,7 +312,7 @@ describe('manifest_manager', () => { // deleted old artifact expect(savedObjectsClient.delete).toHaveBeenCalledWith( ArtifactConstants.SAVED_OBJECT_TYPE, - oldArtifactId + firstOldArtifactId ); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 70557886e57c51..f9836c7ecdc30c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -13,11 +13,11 @@ import { manifestDispatchSchema } from '../../../../../common/endpoint/schema/ma import { ArtifactConstants, - Manifest, buildArtifact, + getArtifactId, getFullEndpointExceptionList, + Manifest, ManifestDiff, - getArtifactId, } from '../../../lib/artifacts'; import { InternalArtifactCompleteSchema, @@ -25,6 +25,8 @@ import { } from '../../../schemas/artifacts'; import { ArtifactClient } from '../artifact_client'; import { ManifestClient } from '../manifest_client'; +import { ENDPOINT_LIST_ID } from '../../../../../../lists/common'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../../lists/common/constants'; export interface ManifestManagerContext { savedObjectsClient: SavedObjectsClientContract; @@ -87,9 +89,43 @@ export class ManifestManager { const exceptionList = await getFullEndpointExceptionList( this.exceptionListClient, os, - artifactSchemaVersion ?? 'v1' + artifactSchemaVersion ?? 'v1', + ENDPOINT_LIST_ID + ); + const artifact = await buildArtifact( + exceptionList, + os, + artifactSchemaVersion ?? 'v1', + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); + artifacts.push(artifact); + } + return artifacts; + } + + /** + * Builds an array of artifacts (one per supported OS) based on the current state of the + * Trusted Apps list (which uses the `exception-list-agnostic` SO type) + * @param artifactSchemaVersion + */ + protected async buildTrustedAppsArtifacts( + artifactSchemaVersion?: string + ): Promise { + const artifacts: InternalArtifactCompleteSchema[] = []; + + for (const os of ArtifactConstants.SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS) { + const trustedApps = await getFullEndpointExceptionList( + this.exceptionListClient, + os, + artifactSchemaVersion ?? 'v1', + ENDPOINT_TRUSTED_APPS_LIST_ID + ); + const artifact = await buildArtifact( + trustedApps, + os, + 'v1', + ArtifactConstants.GLOBAL_TRUSTED_APPS_NAME ); - const artifact = await buildArtifact(exceptionList, os, artifactSchemaVersion ?? 'v1'); artifacts.push(artifact); } return artifacts; @@ -205,7 +241,9 @@ export class ManifestManager { */ public async buildNewManifest(baselineManifest?: Manifest): Promise { // Build new exception list artifacts - const artifacts = await this.buildExceptionListArtifacts(); + const artifacts = ( + await Promise.all([this.buildExceptionListArtifacts(), this.buildTrustedAppsArtifacts()]) + ).flat(); // Build new manifest const manifest = Manifest.fromArtifacts( diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 3e04a507d3810f..a0998f1a838baf 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -142,6 +142,42 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { relative_url: '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', }, + 'endpoint-trustlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-trustlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-trustlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, }, // The manifest version could have changed when the Policy was updated because the // policy details page ensures that a save action applies the udpated policy on top From 7c3ad238e52092fd57b89e3d15a4226beba79eac Mon Sep 17 00:00:00 2001 From: Diana Derevyankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Tue, 1 Sep 2020 15:33:26 +0300 Subject: [PATCH 142/216] KQL autocomplete cut off in visualize editor with styled-components (#75557) * KQL autocomplete cut off in visualize editor Closes #70964 * Refactor some code * Accept api changes, refactor query_string_input and suggestions_component * Add a comment to suggestions_component * Fix dropdown position, add close event on scroll and hide description if it doesn't fit * Update tests to pass type check * Fix displaying bugs * Remove closeList * Replace dropdownHeight with className * Update suggestions_component.test and public.api.md * KQL autocomplete cut off in visualize editor with styled-components * Update suggestions_component.test values * Add logic to open the list up * Remove unnecessary semicolon * Remove a gap between the list and input by inlining offset -2px from _suggestion.scss * Rename the constants and add docs to them * Wrap div inside SuggestionsComponent by styled component instead of wrapping the whole component * Update public.api.md * Refactor .kbnTypeahead__popover--top style * Remove unnecessary condition * Fix eslint problems Co-authored-by: Elastic Machine --- ...in-plugins-data-public.querystringinput.md | 2 +- src/plugins/data/public/public.api.md | 2 +- .../query_string_input/query_string_input.tsx | 43 +++++++++++---- .../suggestions_component.test.tsx.snap | 32 +++++++---- .../data/public/ui/typeahead/_suggestion.scss | 34 +++++++----- .../data/public/ui/typeahead/constants.ts | 36 +++++++++++++ .../typeahead/suggestion_component.test.tsx | 5 ++ .../ui/typeahead/suggestion_component.tsx | 5 +- .../typeahead/suggestions_component.test.tsx | 6 +++ .../ui/typeahead/suggestions_component.tsx | 54 ++++++++++++++++--- .../public/components/controls/filter.tsx | 1 + 11 files changed, 177 insertions(+), 43 deletions(-) create mode 100644 src/plugins/data/public/ui/typeahead/constants.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md index e139b326b75008..9f3ed8c1263ba0 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md @@ -7,5 +7,5 @@ Signature: ```typescript -QueryStringInput: React.FC> +QueryStringInput: React.FC> ``` diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index ba40dece25df9b..0c4465ae7f4b91 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1508,7 +1508,7 @@ export interface QueryState { // Warning: (ae-missing-release-tag) "QueryStringInput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const QueryStringInput: React.FC>; +export const QueryStringInput: React.FC>; // @public (undocumented) export type QuerySuggestion = QuerySuggestionBasic | QuerySuggestionField; diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 86ee98b7af9d84..2d311fd88eb394 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -17,8 +17,7 @@ * under the License. */ -import { Component } from 'react'; -import React from 'react'; +import React, { Component, RefObject, createRef } from 'react'; import { i18n } from '@kbn/i18n'; import { @@ -30,6 +29,7 @@ import { EuiButton, EuiLink, htmlIdGenerator, + EuiPortal, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -42,6 +42,7 @@ import { withKibana, KibanaReactContextValue, toMountPoint } from '../../../../k import { fetchIndexPatterns } from './fetch_index_patterns'; import { QueryLanguageSwitcher } from './language_switcher'; import { PersistedLog, getQueryLog, matchPairs, toUser, fromUser } from '../../query'; +import { SuggestionsListSize } from '../typeahead/suggestions_component'; import { SuggestionsComponent } from '..'; interface Props { @@ -60,6 +61,7 @@ interface Props { onChangeQueryInputFocus?: (isFocused: boolean) => void; onSubmit?: (query: Query) => void; dataTestSubj?: string; + size?: SuggestionsListSize; } interface State { @@ -70,6 +72,7 @@ interface State { selectionStart: number | null; selectionEnd: number | null; indexPatterns: IIndexPattern[]; + queryBarRect: DOMRect | undefined; } const KEY_CODES = { @@ -93,6 +96,7 @@ export class QueryStringInputUI extends Component { selectionStart: null, selectionEnd: null, indexPatterns: [], + queryBarRect: undefined, }; public inputRef: HTMLTextAreaElement | null = null; @@ -101,6 +105,7 @@ export class QueryStringInputUI extends Component { private abortController?: AbortController; private services = this.props.kibana.services; private componentIsUnmounting = false; + private queryBarInputDivRefInstance: RefObject = createRef(); private getQueryString = () => { return toUser(this.props.query.query); @@ -494,8 +499,13 @@ export class QueryStringInputUI extends Component { this.initPersistedLog(); this.fetchIndexPatterns().then(this.updateSuggestions); + this.handleListUpdate(); window.addEventListener('resize', this.handleAutoHeight); + window.addEventListener('scroll', this.handleListUpdate, { + passive: true, // for better performance as we won't call preventDefault + capture: true, // scroll events don't bubble, they must be captured instead + }); } public componentDidUpdate(prevProps: Props) { @@ -533,12 +543,19 @@ export class QueryStringInputUI extends Component { this.updateSuggestions.cancel(); this.componentIsUnmounting = true; window.removeEventListener('resize', this.handleAutoHeight); + window.removeEventListener('scroll', this.handleListUpdate); } + handleListUpdate = () => + this.setState({ + queryBarRect: this.queryBarInputDivRefInstance.current?.getBoundingClientRect(), + }); + handleAutoHeight = () => { if (this.inputRef !== null && document.activeElement === this.inputRef) { this.inputRef.style.setProperty('height', `${this.inputRef.scrollHeight}px`, 'important'); } + this.handleListUpdate(); }; handleRemoveHeight = () => { @@ -587,6 +604,7 @@ export class QueryStringInputUI extends Component {
{ {this.getQueryString()}
- - + + +
diff --git a/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap b/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap index 3b51c1db50d00c..2fa7834872f6b1 100644 --- a/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap +++ b/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap @@ -1,17 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SuggestionsComponent Passing the index should control which suggestion is selected 1`] = ` -
-
+ `; exports[`SuggestionsComponent Should display given suggestions if the show prop is true 1`] = ` -
-
+ `; diff --git a/src/plugins/data/public/ui/typeahead/_suggestion.scss b/src/plugins/data/public/ui/typeahead/_suggestion.scss index 81c05f1a8a78c7..67ff17d017053e 100644 --- a/src/plugins/data/public/ui/typeahead/_suggestion.scss +++ b/src/plugins/data/public/ui/typeahead/_suggestion.scss @@ -6,28 +6,36 @@ $kbnTypeaheadTypes: ( conjunction: $euiColorVis3, ); +.kbnTypeahead.kbnTypeahead--small { + max-height: 20vh; +} + +.kbnTypeahead__popover--top { + @include euiBottomShadowFlat; + border-top-left-radius: $euiBorderRadius; + border-top-right-radius: $euiBorderRadius; +} + +.kbnTypeahead__popover--bottom { + @include euiBottomShadow($adjustBorders: true); + border-bottom-left-radius: $euiBorderRadius; + border-bottom-right-radius: $euiBorderRadius; +} + .kbnTypeahead { - position: relative; + max-height: 60vh; .kbnTypeahead__popover { - @include euiBottomShadow($adjustBorders: true); + max-height: inherit; + @include euiScrollBar; border: 1px solid; border-color: $euiBorderColor; color: $euiTextColor; background-color: $euiColorEmptyShade; - position: absolute; - top: -2px; + position: relative; z-index: $euiZContentMenu; width: 100%; - border-bottom-left-radius: $euiBorderRadius; - border-bottom-right-radius: $euiBorderRadius; - - .kbnTypeahead__items { - @include euiScrollBar; - - max-height: 60vh; - overflow-y: auto; - } + overflow-y: auto; .kbnTypeahead__item { height: $euiSizeXL; diff --git a/src/plugins/data/public/ui/typeahead/constants.ts b/src/plugins/data/public/ui/typeahead/constants.ts new file mode 100644 index 00000000000000..08f9bd23e16f35 --- /dev/null +++ b/src/plugins/data/public/ui/typeahead/constants.ts @@ -0,0 +1,36 @@ +/* + * 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. + */ + +/** + * Minimum width in px to display suggestion description correctly + * @public + */ +export const SUGGESTIONS_LIST_REQUIRED_WIDTH = 600; + +/** + * Minimum bottom distance in px to display list of suggestions + * @public + */ +export const SUGGESTIONS_LIST_REQUIRED_BOTTOM_SPACE = 250; + +/** + * A distance in px to display suggestions list right under the query input without a gap + * @public + */ +export const SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET = 2; diff --git a/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx b/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx index 9fe33b003527e9..ba78bdd802601d 100644 --- a/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx @@ -44,6 +44,7 @@ describe('SuggestionComponent', () => { suggestion={mockSuggestion} innerRef={noop} ariaId={'suggestion-1'} + shouldDisplayDescription={true} /> ); @@ -59,6 +60,7 @@ describe('SuggestionComponent', () => { suggestion={mockSuggestion} innerRef={noop} ariaId={'suggestion-1'} + shouldDisplayDescription={true} /> ); @@ -79,6 +81,7 @@ describe('SuggestionComponent', () => { suggestion={mockSuggestion} innerRef={innerRefCallback} ariaId={'suggestion-1'} + shouldDisplayDescription={true} /> ); }); @@ -94,6 +97,7 @@ describe('SuggestionComponent', () => { suggestion={mockSuggestion} innerRef={noop} ariaId={'suggestion-1'} + shouldDisplayDescription={true} /> ); @@ -113,6 +117,7 @@ describe('SuggestionComponent', () => { suggestion={mockSuggestion} innerRef={noop} ariaId={'suggestion-1'} + shouldDisplayDescription={true} /> ); diff --git a/src/plugins/data/public/ui/typeahead/suggestion_component.tsx b/src/plugins/data/public/ui/typeahead/suggestion_component.tsx index b859428e6ed7ea..724287b874bf70 100644 --- a/src/plugins/data/public/ui/typeahead/suggestion_component.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestion_component.tsx @@ -46,6 +46,7 @@ interface Props { suggestion: QuerySuggestion; innerRef: (node: HTMLDivElement) => void; ariaId: string; + shouldDisplayDescription: boolean; } export function SuggestionComponent(props: Props) { @@ -72,7 +73,9 @@ export function SuggestionComponent(props: Props) {
{props.suggestion.text}
-
{props.suggestion.description}
+ {props.shouldDisplayDescription && ( +
{props.suggestion.description}
+ )}
); diff --git a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx index 011a729c6a6169..583940015c152a 100644 --- a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx @@ -54,6 +54,7 @@ describe('SuggestionsComponent', () => { show={false} suggestions={mockSuggestions} loadMore={noop} + queryBarRect={{ top: 0 } as DOMRect} /> ); @@ -69,6 +70,7 @@ describe('SuggestionsComponent', () => { show={true} suggestions={[]} loadMore={noop} + queryBarRect={{ top: 0 } as DOMRect} /> ); @@ -84,6 +86,7 @@ describe('SuggestionsComponent', () => { show={true} suggestions={mockSuggestions} loadMore={noop} + queryBarRect={{ top: 0 } as DOMRect} /> ); @@ -100,6 +103,7 @@ describe('SuggestionsComponent', () => { show={true} suggestions={mockSuggestions} loadMore={noop} + queryBarRect={{ top: 0 } as DOMRect} /> ); @@ -116,6 +120,7 @@ describe('SuggestionsComponent', () => { show={true} suggestions={mockSuggestions} loadMore={noop} + queryBarRect={{ top: 0 } as DOMRect} /> ); @@ -134,6 +139,7 @@ describe('SuggestionsComponent', () => { show={true} suggestions={mockSuggestions} loadMore={noop} + queryBarRect={{ top: 0 } as DOMRect} /> ); diff --git a/src/plugins/data/public/ui/typeahead/suggestions_component.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx index 77dd7dcec01ee6..dc7c55374f1d5f 100644 --- a/src/plugins/data/public/ui/typeahead/suggestions_component.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx @@ -19,8 +19,15 @@ import { isEmpty } from 'lodash'; import React, { Component } from 'react'; +import classNames from 'classnames'; +import styled from 'styled-components'; import { QuerySuggestion } from '../../autocomplete'; import { SuggestionComponent } from './suggestion_component'; +import { + SUGGESTIONS_LIST_REQUIRED_BOTTOM_SPACE, + SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET, + SUGGESTIONS_LIST_REQUIRED_WIDTH, +} from './constants'; interface Props { index: number | null; @@ -29,18 +36,24 @@ interface Props { show: boolean; suggestions: QuerySuggestion[]; loadMore: () => void; + queryBarRect?: DOMRect; + size?: SuggestionsListSize; } +export type SuggestionsListSize = 's' | 'l'; + export class SuggestionsComponent extends Component { private childNodes: HTMLDivElement[] = []; private parentNode: HTMLDivElement | null = null; public render() { - if (!this.props.show || isEmpty(this.props.suggestions)) { + if (!this.props.queryBarRect || !this.props.show || isEmpty(this.props.suggestions)) { return null; } const suggestions = this.props.suggestions.map((suggestion, index) => { + const isDescriptionFittable = + this.props.queryBarRect!.width >= SUGGESTIONS_LIST_REQUIRED_WIDTH; return ( (this.childNodes[index] = node)} @@ -50,17 +63,38 @@ export class SuggestionsComponent extends Component { onMouseEnter={() => this.props.onMouseEnter(index)} ariaId={'suggestion-' + index} key={`${suggestion.type} - ${suggestion.text}`} + shouldDisplayDescription={isDescriptionFittable} /> ); }); + const documentHeight = document.documentElement.clientHeight || window.innerHeight; + const { queryBarRect } = this.props; + + // reflects if the suggestions list has enough space below to be opened down + const isSuggestionsListFittable = + documentHeight - (queryBarRect.top + queryBarRect.height) > + SUGGESTIONS_LIST_REQUIRED_BOTTOM_SPACE; + const verticalListPosition = isSuggestionsListFittable + ? `top: ${window.scrollY + queryBarRect.bottom - SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET}px;` + : `bottom: ${documentHeight - (window.scrollY + queryBarRect.top)}px;`; + return ( -
-
-
+ +
+
(this.parentNode = node)} onScroll={this.handleScroll} @@ -69,7 +103,7 @@ export class SuggestionsComponent extends Component {
-
+ ); } @@ -116,3 +150,11 @@ export class SuggestionsComponent extends Component { } }; } + +const StyledSuggestionsListDiv = styled.div` + ${(props: { queryBarRect: DOMRect; verticalListPosition: string }) => ` + position: absolute; + left: ${props.queryBarRect.left}px; + width: ${props.queryBarRect.width}px; + ${props.verticalListPosition}`} +`; diff --git a/src/plugins/vis_default_editor/public/components/controls/filter.tsx b/src/plugins/vis_default_editor/public/components/controls/filter.tsx index 101ca3e8ad457b..c373f9331c9a72 100644 --- a/src/plugins/vis_default_editor/public/components/controls/filter.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/filter.tsx @@ -115,6 +115,7 @@ function FilterRow({ dataTestSubj={dataTestSubj} bubbleSubmitEvent={true} languageSwitcherPopoverAnchorPosition="leftDown" + size="s" /> {showCustomLabel ? ( From 27bdc88011c14f35048c1e11b5cfd6eb5cd4d41c Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 1 Sep 2020 14:46:55 +0200 Subject: [PATCH 143/216] [Discover] Add sidebar jest test (#76286) * Add Jest test removed in #73226 --- .../components/sidebar/discover_field.test.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx index a0d9e3c541e47e..6d1238e02c7fb9 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx @@ -80,11 +80,10 @@ function getComponent(selected = false, showDetails = false, useShortDots = fals const props = { indexPattern, field, - getDetails: jest.fn(), + getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: true, columns: [] })), onAddFilter: jest.fn(), onAddField: jest.fn(), onRemoveField: jest.fn(), - onShowDetails: jest.fn(), showDetails, selected, useShortDots, @@ -104,4 +103,9 @@ describe('discover sidebar field', function () { findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); expect(props.onRemoveField).toHaveBeenCalledWith('bytes'); }); + it('should trigger getDetails', function () { + const { comp, props } = getComponent(true); + findTestSubject(comp, 'field-bytes-showDetails').simulate('click'); + expect(props.getDetails).toHaveBeenCalledWith(props.field); + }); }); From ef7246f157100a1454c7a974d5de93ee9bddf65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 1 Sep 2020 14:48:11 +0200 Subject: [PATCH 144/216] [Form lib] Add useFormData() hook to listen to fields value changes (#76107) --- .../components/fields/combobox_field.tsx | 2 +- .../forms/hook_form_lib/components/form.tsx | 17 +- .../components/form_data_provider.test.tsx | 19 +- .../components/form_data_provider.ts | 45 +--- .../forms/hook_form_lib/form_data_context.tsx | 50 ++++ .../static/forms/hook_form_lib/hooks/index.ts | 1 + .../forms/hook_form_lib/hooks/use_field.ts | 18 +- .../hook_form_lib/hooks/use_form.test.tsx | 2 +- .../forms/hook_form_lib/hooks/use_form.ts | 8 + .../hooks/use_form_data.test.tsx | 234 ++++++++++++++++++ .../hook_form_lib/hooks/use_form_data.ts | 91 +++++++ .../static/forms/hook_form_lib/index.ts | 2 +- .../static/forms/hook_form_lib/types.ts | 8 + .../fields/edit_field/edit_field.tsx | 4 +- .../template_form/steps/step_logistics.tsx | 82 +++--- .../template_form/template_form.tsx | 2 +- .../template_form/template_form_schemas.tsx | 13 +- .../index_management/public/shared_imports.ts | 1 + 18 files changed, 482 insertions(+), 117 deletions(-) create mode 100644 src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx create mode 100644 src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx create mode 100644 src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx index 9fb804eb7fafac..b2f1a70341315f 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx @@ -74,7 +74,7 @@ export const ComboBoxField = ({ field, euiFieldProps = {}, ...rest }: Props) => }; const onSearchComboChange = (value: string) => { - if (value) { + if (value !== undefined) { field.clearErrors(VALIDATION_TYPES.ARRAY_ITEM); } }; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx index b3a15fea8b187e..287ac562434460 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx @@ -21,6 +21,7 @@ import React, { ReactNode } from 'react'; import { EuiForm } from '@elastic/eui'; import { FormProvider } from '../form_context'; +import { FormDataContextProvider } from '../form_data_context'; import { FormHook } from '../types'; interface Props { @@ -30,8 +31,14 @@ interface Props { [key: string]: any; } -export const Form = ({ form, FormWrapper = EuiForm, ...rest }: Props) => ( - - - -); +export const Form = ({ form, FormWrapper = EuiForm, ...rest }: Props) => { + const { getFormData, __getFormData$ } = form; + + return ( + + + + + + ); +}; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx index 25448dff18e8ac..d9095944eaa335 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx @@ -75,16 +75,7 @@ describe('', () => { setInputValue('lastNameField', 'updated value'); }); - /** - * The children will be rendered three times: - * - Twice for each input value that has changed - * - once because after updating both fields, the **form** isValid state changes (from "undefined" to "true") - * causing a new "form" object to be returned and thus a re-render. - * - * When the form object will be memoized (in a future PR), te bellow call count should only be 2 as listening - * to form data changes should not receive updates when the "isValid" state of the form changes. - */ - expect(onFormData.mock.calls.length).toBe(3); + expect(onFormData).toBeCalledTimes(2); const [formDataUpdated] = onFormData.mock.calls[onFormData.mock.calls.length - 1] as Parameters< OnUpdateHandler @@ -130,7 +121,7 @@ describe('', () => { find, } = setup() as TestBed; - expect(onFormData.mock.calls.length).toBe(0); // Not present in the DOM yet + expect(onFormData).toBeCalledTimes(0); // Not present in the DOM yet // Make some changes to the form fields await act(async () => { @@ -188,7 +179,7 @@ describe('', () => { setInputValue('lastNameField', 'updated value'); }); - expect(onFormData.mock.calls.length).toBe(0); + expect(onFormData).toBeCalledTimes(0); }); test('props.pathsToWatch (Array): should not re-render the children when the field that changed is not in the watch list', async () => { @@ -228,14 +219,14 @@ describe('', () => { }); // No re-render - expect(onFormData.mock.calls.length).toBe(0); + expect(onFormData).toBeCalledTimes(0); // Make some changes to fields in the watch list await act(async () => { setInputValue('nameField', 'updated value'); }); - expect(onFormData.mock.calls.length).toBe(1); + expect(onFormData).toBeCalledTimes(1); onFormData.mockReset(); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts index 3630b902f05649..ac141baf8fc719 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts @@ -17,10 +17,10 @@ * under the License. */ -import React, { useState, useEffect, useRef, useCallback } from 'react'; +import React from 'react'; import { FormData } from '../types'; -import { useFormContext } from '../form_context'; +import { useFormData } from '../hooks'; interface Props { children: (formData: FormData) => JSX.Element | null; @@ -28,46 +28,9 @@ interface Props { } export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) => { - const form = useFormContext(); - const { subscribe } = form; - const previousRawData = useRef(form.__getFormData$().value); - const isMounted = useRef(false); - const [formData, setFormData] = useState(previousRawData.current); + const { 0: formData, 2: isReady } = useFormData({ watch: pathsToWatch }); - const onFormData = useCallback( - ({ data: { raw } }) => { - // To avoid re-rendering the children for updates on the form data - // that we are **not** interested in, we can specify one or multiple path(s) - // to watch. - if (pathsToWatch) { - const valuesToWatchArray = Array.isArray(pathsToWatch) - ? (pathsToWatch as string[]) - : ([pathsToWatch] as string[]); - - if (valuesToWatchArray.some((value) => previousRawData.current[value] !== raw[value])) { - previousRawData.current = raw; - setFormData(raw); - } - } else { - setFormData(raw); - } - }, - [pathsToWatch] - ); - - useEffect(() => { - const subscription = subscribe(onFormData); - return subscription.unsubscribe; - }, [subscribe, onFormData]); - - useEffect(() => { - isMounted.current = true; - return () => { - isMounted.current = false; - }; - }, []); - - if (!isMounted.current && Object.keys(formData).length === 0) { + if (!isReady) { // No field has mounted yet, don't render anything return null; } diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx new file mode 100644 index 00000000000000..0e6a75e9c5065a --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx @@ -0,0 +1,50 @@ +/* + * 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 React, { createContext, useContext, useMemo } from 'react'; + +import { FormData, FormHook } from './types'; +import { Subject } from './lib'; + +export interface Context { + getFormData$: () => Subject; + getFormData: FormHook['getFormData']; +} + +const FormDataContext = createContext | undefined>(undefined); + +interface Props extends Context { + children: React.ReactNode; +} + +export const FormDataContextProvider = ({ children, getFormData$, getFormData }: Props) => { + const value = useMemo( + () => ({ + getFormData, + getFormData$, + }), + [getFormData, getFormData$] + ); + + return {children}; +}; + +export function useFormDataContext() { + return useContext | undefined>(FormDataContext); +} diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts index 6a04a592227f95..45c11dd6272e4b 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts @@ -19,3 +19,4 @@ export { useField } from './use_field'; export { useForm } from './use_form'; +export { useFormData } from './use_form_data'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index 9d22e4eb2ee5e5..fa29f900af2ef5 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -254,6 +254,8 @@ export const useField = ( validationErrors.push({ ...validationResult, + // See comment below that explains why we add "__isBlocking__". + __isBlocking__: validationResult.__isBlocking__ ?? validation.isBlocking, validationType: validationType || VALIDATION_TYPES.FIELD, }); @@ -306,6 +308,11 @@ export const useField = ( validationErrors.push({ ...(validationResult as ValidationError), + // We add an "__isBlocking__" property to know if this error is a blocker or no. + // Most validation errors are blockers but in some cases a validation is more a warning than an error + // like with the ComboBox items when they are added. + __isBlocking__: + (validationResult as ValidationError).__isBlocking__ ?? validation.isBlocking, validationType: validationType || VALIDATION_TYPES.FIELD, }); @@ -394,7 +401,13 @@ export const useField = ( ); const _setErrors: FieldHook['setErrors'] = useCallback((_errors) => { - setErrors(_errors.map((error) => ({ validationType: VALIDATION_TYPES.FIELD, ...error }))); + setErrors( + _errors.map((error) => ({ + validationType: VALIDATION_TYPES.FIELD, + __isBlocking__: true, + ...error, + })) + ); }, []); /** @@ -463,7 +476,8 @@ export const useField = ( [setValue, deserializeValue, defaultValue] ); - const isValid = errors.length === 0; + // Don't take into account non blocker validation. Some are just warning (like trying to add a wrong ComboBox item) + const isValid = errors.filter((e) => e.__isBlocking__ !== false).length === 0; const field = useMemo>(() => { return { diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx index 007e492243baca..4a880415b6d228 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx @@ -39,7 +39,7 @@ const onFormHook = (_form: FormHook) => { formHook = _form; }; -describe('use_form() hook', () => { +describe('useForm() hook', () => { beforeEach(() => { formHook = null; }); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index 35bac5b9a58c65..7b72a9eeacf7b1 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -240,6 +240,12 @@ export function useForm( if (!field.isValidated) { setIsValid(undefined); + + // When we submit the form (and set "isSubmitted" to "true"), we validate **all fields**. + // If a field is added and it is not validated it means that we have swapped fields and added new ones: + // --> we have basically have a new form in front of us. + // For that reason we make sure that the "isSubmitted" state is false. + setIsSubmitted(false); } }, [updateFormDataAt] @@ -389,6 +395,7 @@ export function useForm( isValid, id, submit: submitForm, + validate: validateAllFields, subscribe, setFieldValue, setFieldErrors, @@ -428,6 +435,7 @@ export function useForm( addField, removeField, validateFields, + validateAllFields, ]); useEffect(() => { diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx new file mode 100644 index 00000000000000..0fb65daecf2f4f --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx @@ -0,0 +1,234 @@ +/* + * 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 React, { useEffect } from 'react'; +import { act } from 'react-dom/test-utils'; + +import { registerTestBed, TestBed } from '../shared_imports'; +import { Form, UseField } from '../components'; +import { useForm } from './use_form'; +import { useFormData, HookReturn } from './use_form_data'; + +interface Props { + onChange(data: HookReturn): void; + watch?: string | string[]; +} + +describe('useFormData() hook', () => { + const HookListenerComp = React.memo(({ onChange, watch }: Props) => { + const hookValue = useFormData({ watch }); + + useEffect(() => { + onChange(hookValue); + }, [hookValue, onChange]); + + return null; + }); + + describe('form data updates', () => { + let testBed: TestBed; + let onChangeSpy: jest.Mock; + + const getLastMockValue = () => { + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + }; + + const TestComp = (props: Props) => { + const { form } = useForm(); + + return ( +
+ + + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + beforeEach(() => { + onChangeSpy = jest.fn(); + testBed = setup({ onChange: onChangeSpy }) as TestBed; + }); + + test('should return the form data', () => { + // Called twice: + // once when the hook is called and once when the fields have mounted and updated the form data + expect(onChangeSpy).toBeCalledTimes(2); + const [data] = getLastMockValue(); + expect(data).toEqual({ title: 'titleInitialValue' }); + }); + + test('should listen to field changes', async () => { + const { + form: { setInputValue }, + } = testBed; + + await act(async () => { + setInputValue('titleField', 'titleChanged'); + }); + + expect(onChangeSpy).toBeCalledTimes(3); + const [data] = getLastMockValue(); + expect(data).toEqual({ title: 'titleChanged' }); + }); + }); + + describe('format form data', () => { + let onChangeSpy: jest.Mock; + + const getLastMockValue = () => { + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + }; + + const TestComp = (props: Props) => { + const { form } = useForm(); + + return ( +
+ + + + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + beforeEach(() => { + onChangeSpy = jest.fn(); + setup({ onChange: onChangeSpy }); + }); + + test('should expose a handler to build the form data', () => { + const { 1: format } = getLastMockValue(); + expect(format()).toEqual({ + user: { + firstName: 'John', + lastName: 'Snow', + }, + }); + }); + }); + + describe('options', () => { + describe('watch', () => { + let testBed: TestBed; + let onChangeSpy: jest.Mock; + + const getLastMockValue = () => { + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + }; + + const TestComp = (props: Props) => { + const { form } = useForm(); + + return ( +
+ + + + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + beforeEach(() => { + onChangeSpy = jest.fn(); + testBed = setup({ watch: 'title', onChange: onChangeSpy }) as TestBed; + }); + + test('should not listen to changes on fields we are not interested in', async () => { + const { + form: { setInputValue }, + } = testBed; + + await act(async () => { + // Changing a field we are **not** interested in + setInputValue('subTitleField', 'subTitleChanged'); + // Changing a field we **are** interested in + setInputValue('titleField', 'titleChanged'); + }); + + const [data] = getLastMockValue(); + expect(data).toEqual({ title: 'titleChanged', subTitle: 'subTitleInitialValue' }); + }); + }); + + describe('form', () => { + let testBed: TestBed; + let onChangeSpy: jest.Mock; + + const getLastMockValue = () => { + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + }; + + const TestComp = ({ onChange }: Props) => { + const { form } = useForm(); + const hookValue = useFormData({ form }); + + useEffect(() => { + onChange(hookValue); + }, [hookValue, onChange]); + + return ( +
+ + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + beforeEach(() => { + onChangeSpy = jest.fn(); + testBed = setup({ onChange: onChangeSpy }) as TestBed; + }); + + test('should allow a form to be provided when the hook is called outside of the FormDataContext', async () => { + const { + form: { setInputValue }, + } = testBed; + + const [initialData] = getLastMockValue(); + expect(initialData).toEqual({ title: 'titleInitialValue' }); + + await act(async () => { + setInputValue('titleField', 'titleChanged'); + }); + + const [updatedData] = getLastMockValue(); + expect(updatedData).toEqual({ title: 'titleChanged' }); + }); + }); + }); +}); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts new file mode 100644 index 00000000000000..fb4a0984438ad4 --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts @@ -0,0 +1,91 @@ +/* + * 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 { useState, useEffect, useRef, useCallback } from 'react'; + +import { FormData, FormHook } from '../types'; +import { useFormDataContext, Context } from '../form_data_context'; + +interface Options { + watch?: string | string[]; + form?: FormHook; +} + +export type HookReturn = [FormData, () => T, boolean]; + +export const useFormData = (options: Options = {}): HookReturn => { + const { watch, form } = options; + const ctx = useFormDataContext(); + + let getFormData: Context['getFormData']; + let getFormData$: Context['getFormData$']; + + if (form !== undefined) { + getFormData = form.getFormData; + getFormData$ = form.__getFormData$; + } else if (ctx !== undefined) { + ({ getFormData, getFormData$ } = ctx); + } else { + throw new Error( + 'useFormData() must be used within a or you need to pass FormHook object in the options.' + ); + } + + const initialValue = getFormData$().value; + + const previousRawData = useRef(initialValue); + const isMounted = useRef(false); + const [formData, setFormData] = useState(previousRawData.current); + + const formatFormData = useCallback(() => { + return getFormData({ unflatten: true }); + }, [getFormData]); + + useEffect(() => { + const subscription = getFormData$().subscribe((raw) => { + if (watch) { + const valuesToWatchArray = Array.isArray(watch) + ? (watch as string[]) + : ([watch] as string[]); + + if (valuesToWatchArray.some((value) => previousRawData.current[value] !== raw[value])) { + previousRawData.current = raw; + // Only update the state if one of the field we watch has changed. + setFormData(raw); + } + } else { + setFormData(raw); + } + }); + return subscription.unsubscribe; + }, [getFormData$, watch]); + + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); + + if (!isMounted.current && Object.keys(formData).length === 0) { + // No field has mounted yet + return [formData, formatFormData, false]; + } + + return [formData, formatFormData, true]; +}; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts index 3079814c9ad14b..8d6b57fbeb3155 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts @@ -19,7 +19,7 @@ // Only export the useForm hook. The "useField" hook is for internal use // as the consumer of the library must use the component -export { useForm } from './hooks'; +export { useForm, useFormData } from './hooks'; export { getFieldValidityAndErrorMessage } from './helpers'; export * from './form_context'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index dc495f6eb56b4e..4b343ec5e9f2e1 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -30,6 +30,7 @@ export interface FormHook { readonly isValid: boolean | undefined; readonly id: string; submit: (e?: FormEvent | MouseEvent) => Promise<{ data: T; isValid: boolean }>; + validate: () => Promise; subscribe: (handler: OnUpdateHandler) => Subscription; setFieldValue: (fieldName: string, value: FieldValue) => void; setFieldErrors: (fieldName: string, errors: ValidationError[]) => void; @@ -147,6 +148,7 @@ export interface ValidationError { message: string; code?: T; validationType?: string; + __isBlocking__?: boolean; [key: string]: any; } @@ -185,5 +187,11 @@ type FieldValue = unknown; export interface ValidationConfig { validator: ValidationFunc; type?: string; + /** + * By default all validation are blockers, which means that if they fail, the field is invalid. + * In some cases, like when trying to add an item to the ComboBox, if the item is not valid we want + * to show a validation error. But this validation is **not** blocking. Simply, the item has not been added. + */ + isBlocking?: boolean; exitOnFail?: boolean; } diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx index 6b5a848ce85d32..95575124b6abdd 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx @@ -163,7 +163,7 @@ export const EditField = React.memo(({ form, field, allFields, exitEdit, updateF - {form.isSubmitted && form.isValid === false && ( + {form.isSubmitted && !form.isValid && ( <> {i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldUpdateButtonLabel', { diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx index fcc9795617ebb6..56f040fc59a7b1 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx @@ -17,13 +17,13 @@ import { i18n } from '@kbn/i18n'; import { useForm, + useFormData, Form, getUseField, getFormRow, Field, Forms, JsonEditorField, - FormDataProvider, } from '../../../../shared_imports'; import { documentationService } from '../../../services/documentation'; import { schemas, nameConfig, nameConfigWithoutValidations } from '../template_form_schemas'; @@ -118,9 +118,7 @@ interface LogisticsForm { } interface LogisticsFormInternal extends LogisticsForm { - __internal__: { - addMeta: boolean; - }; + addMeta: boolean; } interface Props { @@ -133,14 +131,12 @@ interface Props { function formDeserializer(formData: LogisticsForm): LogisticsFormInternal { return { ...formData, - __internal__: { - addMeta: Boolean(formData._meta && Object.keys(formData._meta).length), - }, + addMeta: Boolean(formData._meta && Object.keys(formData._meta).length), }; } function formSerializer(formData: LogisticsFormInternal): LogisticsForm { - const { __internal__, ...rest } = formData; + const { addMeta, ...rest } = formData; return rest; } @@ -153,7 +149,18 @@ export const StepLogistics: React.FunctionComponent = React.memo( serializer: formSerializer, deserializer: formDeserializer, }); - const { subscribe, submit, isSubmitted, isValid: isFormValid, getErrors: getFormErrors } = form; + const { + submit, + isSubmitted, + isValid: isFormValid, + getErrors: getFormErrors, + getFormData, + } = form; + + const [{ addMeta }] = useFormData({ + form, + watch: 'addMeta', + }); /** * When the consumer call validate() on this step, we submit the form so it enters the "isSubmitted" state @@ -164,15 +171,12 @@ export const StepLogistics: React.FunctionComponent = React.memo( }, [submit]); useEffect(() => { - const subscription = subscribe(({ data, isValid }) => { - onChange({ - isValid, - validate, - getData: data.format, - }); + onChange({ + isValid: isFormValid, + getData: getFormData, + validate, }); - return subscription.unsubscribe; - }, [onChange, validate, subscribe]); + }, [onChange, isFormValid, validate, getFormData]); const { name, indexPatterns, dataStream, order, priority, version } = getFieldsMeta( documentationService.getEsDocsBase() @@ -296,34 +300,28 @@ export const StepLogistics: React.FunctionComponent = React.memo( defaultMessage="Use the _meta field to store any metadata you want." /> - + } > - - {({ '__internal__.addMeta': addMeta }) => { - return ( - addMeta && ( - - ) - ); - }} - + {addMeta && ( + + )} )} 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 537f4211733588..3a03835e859701 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 @@ -192,8 +192,8 @@ export const TemplateForm = ({ wizardData: WizardContent ): TemplateDeserialized => { const outputTemplate = { - ...initialTemplate, ...wizardData.logistics, + _kbnMeta: initialTemplate._kbnMeta, composedOf: wizardData.components, template: { settings: wizardData.settings, diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx index 0d9ce57a64c843..c85126f08685e0 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx @@ -125,6 +125,7 @@ export const schemas: Record = { { validator: indexPatternField(i18n), type: VALIDATION_TYPES.ARRAY_ITEM, + isBlocking: false, }, ], }, @@ -213,13 +214,11 @@ export const schemas: Record = { } }, }, - __internal__: { - addMeta: { - type: FIELD_TYPES.TOGGLE, - label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.addMetadataLabel', { - defaultMessage: 'Add metadata', - }), - }, + addMeta: { + type: FIELD_TYPES.TOGGLE, + label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.addMetadataLabel', { + defaultMessage: 'Add metadata', + }), }, }, }; diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts index 2ba2a5c493c493..f7f992a090501f 100644 --- a/x-pack/plugins/index_management/public/shared_imports.ts +++ b/x-pack/plugins/index_management/public/shared_imports.ts @@ -21,6 +21,7 @@ export { VALIDATION_TYPES, FieldConfig, useForm, + useFormData, Form, getUseField, UseField, From 4bd9ccec02d9a9723d541c40c6286fee6142d02a Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Tue, 1 Sep 2020 09:34:01 -0400 Subject: [PATCH 145/216] [Canvas][tech-debt] Renderer stories (#74373) * Add stories for renderers * Fix Typecheck Co-authored-by: Elastic Machine --- .../canvas_plugin_src/functions/common/pie.ts | 6 +- .../__snapshots__/image.stories.storyshot | 14 ++++ .../repeat_image.stories.storyshot | 14 ++++ .../__snapshots__/table.stories.storyshot | 14 ++++ .../renderers/__stories__/image.stories.tsx | 20 +++++ .../renderers/__stories__/render.tsx | 65 +++++++++++++++++ .../__stories__/repeat_image.stories.tsx | 23 ++++++ .../renderers/__stories__/table.stories.tsx | 45 ++++++++++++ .../__snapshots__/error.stories.storyshot | 14 ++++ .../error/__stories__/error.stories.tsx | 17 +++++ .../__snapshots__/markdown.stories.storyshot | 27 +++++++ .../markdown/__stories__/markdown.stories.tsx | 35 +++++++++ .../__snapshots__/pie.stories.storyshot | 27 +++++++ .../renderers/pie/__stories__/pie.stories.tsx | 73 +++++++++++++++++++ .../__snapshots__/plot.stories.storyshot | 14 ++++ .../plot/__stories__/plot.stories.tsx | 68 +++++++++++++++++ .../__snapshots__/progress.stories.storyshot | 14 ++++ .../progress/__stories__/progress.stories.tsx | 30 ++++++++ .../reveal_image.stories.storyshot | 14 ++++ .../__stories__/reveal_image.stories.tsx | 23 ++++++ .../__snapshots__/shape.stories.storyshot | 14 ++++ .../shape/__stories__/shape.stories.tsx | 23 ++++++ 22 files changed, 591 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/image.stories.storyshot create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/repeat_image.stories.storyshot create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/table.stories.storyshot create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/image.stories.tsx create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/repeat_image.stories.tsx create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/table.stories.tsx create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/__snapshots__/error.stories.storyshot create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/error.stories.tsx create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/__snapshots__/markdown.stories.storyshot create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/markdown.stories.tsx create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/__snapshots__/pie.stories.storyshot create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/pie.stories.tsx create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/__snapshots__/plot.stories.storyshot create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/plot.stories.tsx create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/__snapshots__/progress.stories.storyshot create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/progress.stories.tsx create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/__snapshots__/reveal_image.stories.storyshot create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/reveal_image.stories.tsx create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/__snapshots__/shape.stories.storyshot create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/shape.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts index 16eee349475ef6..11551c50d9f257 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts @@ -39,9 +39,9 @@ interface PieOptions { colors: string[]; legend: { show: boolean; - backgroundOpacity: number; - labelBoxBorderColor: string; - position: Legend; + backgroundOpacity?: number; + labelBoxBorderColor?: string; + position?: Legend; }; grid: { show: boolean; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/image.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/image.stories.storyshot new file mode 100644 index 00000000000000..b9bc21dd6e3ea1 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/image.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/image default 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/repeat_image.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/repeat_image.stories.storyshot new file mode 100644 index 00000000000000..9b97ae1fdacb32 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/repeat_image.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/repeatImage default 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/table.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/table.stories.storyshot new file mode 100644 index 00000000000000..cf9cc6dd82f7f8 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/table.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/table default 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/image.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/image.stories.tsx new file mode 100644 index 00000000000000..bcd83650344480 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/image.stories.tsx @@ -0,0 +1,20 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { image } from '../image'; +import { Render } from './render'; +import { elasticLogo } from '../../lib/elastic_logo'; + +storiesOf('renderers/image', module).add('default', () => { + const config = { + type: 'image' as 'image', + mode: 'cover', + dataurl: elasticLogo, + }; + + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx new file mode 100644 index 00000000000000..647c63c2c10427 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx @@ -0,0 +1,65 @@ +/* + * 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 { action } from '@storybook/addon-actions'; +import React, { useRef, useEffect } from 'react'; +import { RendererFactory, RendererHandlers } from '../../../types'; + +export const defaultHandlers: RendererHandlers = { + destroy: () => action('destroy'), + getElementId: () => 'element-id', + getFilter: () => 'filter', + onComplete: (fn) => undefined, + onEmbeddableDestroyed: action('onEmbeddableDestroyed'), + onEmbeddableInputChange: action('onEmbeddableInputChange'), + onResize: action('onResize'), + resize: action('resize'), + setFilter: action('setFilter'), + done: action('done'), + onDestroy: action('onDestroy'), + reload: action('reload'), + update: action('update'), + event: action('event'), +}; + +/* + Uses a RenderDefinitionFactory and Config to render into an element. + + Intended to be used for stories for RenderDefinitionFactory +*/ +interface RenderAdditionalProps { + height?: string; + width?: string; + handlers?: RendererHandlers; +} + +export const Render = ({ + renderer, + config, + ...rest +}: Renderer extends RendererFactory + ? { renderer: Renderer; config: Config } & RenderAdditionalProps + : { renderer: undefined; config: undefined } & RenderAdditionalProps) => { + const { height, width, handlers } = { + height: '200px', + width: '200px', + handlers: defaultHandlers, + ...rest, + }; + + const containerRef = useRef(null); + + useEffect(() => { + if (renderer && containerRef.current !== null) { + renderer().render(containerRef.current, config, handlers); + } + }, [renderer, config, handlers]); + + return ( +
+ {' '} +
+ ); +}; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/repeat_image.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/repeat_image.stories.tsx new file mode 100644 index 00000000000000..41ccc054a77fbd --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/repeat_image.stories.tsx @@ -0,0 +1,23 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { repeatImage } from '../repeat_image'; +import { Render } from './render'; +import { elasticLogo } from '../../lib/elastic_logo'; +import { elasticOutline } from '../../lib/elastic_outline'; + +storiesOf('renderers/repeatImage', module).add('default', () => { + const config = { + count: 42, + image: elasticLogo, + size: 20, + max: 60, + emptyImage: elasticOutline, + }; + + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/table.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/table.stories.tsx new file mode 100644 index 00000000000000..f3c70cb30de454 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/table.stories.tsx @@ -0,0 +1,45 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { table } from '../table'; +import { Render } from './render'; + +storiesOf('renderers/table', module).add('default', () => { + const config = { + paginate: true, + perPage: 5, + showHeader: true, + datatable: { + type: 'datatable' as 'datatable', + columns: [ + { + name: 'Foo', + type: 'string' as 'string', + id: 'id-foo', + meta: { type: 'string' as 'string' }, + }, + { + name: 'Bar', + type: 'number' as 'number', + id: 'id-bar', + meta: { type: 'string' as 'string' }, + }, + ], + rows: [ + { Foo: 'a', Bar: 700 }, + { Foo: 'b', Bar: 600 }, + { Foo: 'c', Bar: 500 }, + { Foo: 'd', Bar: 400 }, + { Foo: 'e', Bar: 300 }, + { Foo: 'f', Bar: 200 }, + { Foo: 'g', Bar: 100 }, + ], + }, + }; + + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/__snapshots__/error.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/__snapshots__/error.stories.storyshot new file mode 100644 index 00000000000000..b7039ee1847c74 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/__snapshots__/error.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/error default 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/error.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/error.stories.tsx new file mode 100644 index 00000000000000..c71999bc04bb1c --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/error.stories.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { error } from '../'; +import { Render } from '../../__stories__/render'; + +storiesOf('renderers/error', module).add('default', () => { + const thrownError = new Error('There was an error'); + const config = { + error: thrownError, + }; + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/__snapshots__/markdown.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/__snapshots__/markdown.stories.storyshot new file mode 100644 index 00000000000000..79f447c953d6d1 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/__snapshots__/markdown.stories.storyshot @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/markdown default 1`] = ` +
+ +
+`; + +exports[`Storyshots renderers/markdown links in new tab 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/markdown.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/markdown.stories.tsx new file mode 100644 index 00000000000000..d5b190c74a92fd --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/markdown.stories.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 from 'react'; +import { storiesOf } from '@storybook/react'; +import { markdown } from '../'; +import { Render } from '../../__stories__/render'; + +storiesOf('renderers/markdown', module) + .add('default', () => { + const config = { + content: '# This is Markdown', + font: { + css: '', + spec: {}, + type: 'style' as 'style', + }, + openLinksInNewTab: false, + }; + return ; + }) + .add('links in new tab', () => { + const config = { + content: '[Elastic.co](https://elastic.co)', + font: { + css: '', + spec: {}, + type: 'style' as 'style', + }, + openLinksInNewTab: true, + }; + return ; + }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/__snapshots__/pie.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/__snapshots__/pie.stories.storyshot new file mode 100644 index 00000000000000..3260dbe8c83c27 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/__snapshots__/pie.stories.storyshot @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/pie default 1`] = ` +
+ +
+`; + +exports[`Storyshots renderers/pie with legend 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/pie.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/pie.stories.tsx new file mode 100644 index 00000000000000..dea2876de0ec8a --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/pie.stories.tsx @@ -0,0 +1,73 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { pie } from '../'; +import { Render } from '../../__stories__/render'; + +const pieOptions = { + canvas: false, + colors: ['#882E72', '#B178A6', '#D6C1DE'], + grid: { show: false }, + legend: { show: false }, + series: { + pie: { + show: true, + innerRadius: 0, + label: { show: true, radius: 1 }, + radius: 'auto' as 'auto', + stroke: { width: 0 }, + tilt: 1, + }, + }, +}; + +const data = [ + { + data: [10], + label: 'A', + }, + { + data: [10], + label: 'B', + }, + { + data: [10], + label: 'C', + }, +]; + +storiesOf('renderers/pie', module) + .add('default', () => { + const config = { + data, + options: pieOptions, + font: { + css: '', + spec: {}, + type: 'style' as 'style', + }, + }; + return ; + }) + .add('with legend', () => { + const options = { + ...pieOptions, + legend: { show: true }, + }; + + const config = { + data, + options, + font: { + css: '', + spec: {}, + type: 'style' as 'style', + }, + }; + + return ; + }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/__snapshots__/plot.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/__snapshots__/plot.stories.storyshot new file mode 100644 index 00000000000000..7419d13fc7195b --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/__snapshots__/plot.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/plot default 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/plot.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/plot.stories.tsx new file mode 100644 index 00000000000000..0e9566d2a5c20c --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/plot.stories.tsx @@ -0,0 +1,68 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { plot } from '../'; +import { Render } from '../../__stories__/render'; + +const plotOptions = { + canvas: false, + colors: ['#882E72', '#B178A6', '#D6C1DE'], + grid: { + margin: { + bottom: 0, + left: 0, + right: 30, + top: 20, + }, + }, + legend: { show: true }, + series: { + bubbles: { + show: true, + fill: false, + }, + }, + xaxis: { + show: true, + mode: 'time', + }, + yaxis: { + show: true, + }, +}; + +const data = [ + { + bubbles: { show: true }, + data: [ + [1546351551031, 33, { size: 5 }], + [1546351551131, 38, { size: 2 }], + ], + label: 'done', + }, + { + bubbles: { show: true }, + data: [ + [1546351551032, 37, { size: 4 }], + [1546351551139, 45, { size: 3 }], + ], + label: 'running', + }, +]; + +storiesOf('renderers/plot', module).add('default', () => { + const config = { + data, + options: plotOptions, + font: { + css: '', + spec: {}, + type: 'style' as 'style', + }, + }; + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/__snapshots__/progress.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/__snapshots__/progress.stories.storyshot new file mode 100644 index 00000000000000..1fe884656ef3b4 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/__snapshots__/progress.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/progress default 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/progress.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/progress.stories.tsx new file mode 100644 index 00000000000000..3e20cfd4772a80 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/progress.stories.tsx @@ -0,0 +1,30 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { progress } from '../'; +import { Render } from '../../__stories__/render'; +import { Shape } from '../../../functions/common/progress'; + +storiesOf('renderers/progress', module).add('default', () => { + const config = { + barColor: '#bc1234', + barWeight: 20, + font: { + css: '', + spec: {}, + type: 'style' as 'style', + }, + label: '66%', + max: 1, + shape: Shape.UNICORN, + value: 0.66, + valueColor: '#000', + valueWeight: 15, + }; + + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/__snapshots__/reveal_image.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/__snapshots__/reveal_image.stories.storyshot new file mode 100644 index 00000000000000..b9963565a09f58 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/__snapshots__/reveal_image.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/revealImage default 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/reveal_image.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/reveal_image.stories.tsx new file mode 100644 index 00000000000000..637f9a2bb6986e --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/reveal_image.stories.tsx @@ -0,0 +1,23 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { revealImage } from '../'; +import { Render } from '../../__stories__/render'; +import { elasticOutline } from '../../../lib/elastic_outline'; +import { elasticLogo } from '../../../lib/elastic_logo'; +import { Origin } from '../../../functions/common/revealImage'; + +storiesOf('renderers/revealImage', module).add('default', () => { + const config = { + image: elasticLogo, + emptyImage: elasticOutline, + origin: Origin.LEFT, + percent: 0.45, + }; + + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/__snapshots__/shape.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/__snapshots__/shape.stories.storyshot new file mode 100644 index 00000000000000..317c20021015af --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/__snapshots__/shape.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/shape default 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/shape.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/shape.stories.tsx new file mode 100644 index 00000000000000..19df14c08688f5 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/shape.stories.tsx @@ -0,0 +1,23 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { shape } from '../'; +import { Render } from '../../__stories__/render'; +import { Shape } from '../../../functions/common/shape'; + +storiesOf('renderers/shape', module).add('default', () => { + const config = { + type: 'shape' as 'shape', + border: '#FFEEDD', + borderWidth: 8, + shape: Shape.BOOKMARK, + fill: '#112233', + maintainAspect: true, + }; + + return ; +}); From 2006ecaa32c78fdc8db82d4df81454396e4c1da2 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Tue, 1 Sep 2020 07:36:05 -0600 Subject: [PATCH 146/216] Add plugin status API (#75819) --- ...server.statusservicesetup.dependencies_.md | 13 + ...erver.statusservicesetup.derivedstatus_.md | 20 ++ ...a-plugin-core-server.statusservicesetup.md | 63 ++++ ...ugin-core-server.statusservicesetup.set.md | 28 ++ rfcs/text/0010_service_status.md | 2 +- src/core/server/legacy/legacy_service.ts | 3 + src/core/server/plugins/plugin_context.ts | 3 + .../server/plugins/plugins_system.test.ts | 30 +- src/core/server/plugins/plugins_system.ts | 21 +- src/core/server/plugins/types.ts | 6 + src/core/server/server.api.md | 13 +- src/core/server/server.test.ts | 30 +- src/core/server/server.ts | 13 +- .../server/status/get_summary_status.test.ts | 44 ++- src/core/server/status/get_summary_status.ts | 12 +- src/core/server/status/plugins_status.test.ts | 338 ++++++++++++++++++ src/core/server/status/plugins_status.ts | 98 +++++ src/core/server/status/status_service.mock.ts | 8 + src/core/server/status/status_service.test.ts | 75 ++++ src/core/server/status/status_service.ts | 37 +- src/core/server/status/types.ts | 91 ++++- 21 files changed, 916 insertions(+), 32 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.statusservicesetup.dependencies_.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.statusservicesetup.derivedstatus_.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md create mode 100644 src/core/server/status/plugins_status.test.ts create mode 100644 src/core/server/status/plugins_status.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.dependencies_.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.dependencies_.md new file mode 100644 index 00000000000000..7475f0e3a4c1c3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.dependencies_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [dependencies$](./kibana-plugin-core-server.statusservicesetup.dependencies_.md) + +## StatusServiceSetup.dependencies$ property + +Current status for all plugins this plugin depends on. Each key of the `Record` is a plugin id. + +Signature: + +```typescript +dependencies$: Observable>; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.derivedstatus_.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.derivedstatus_.md new file mode 100644 index 00000000000000..6c65e44270a063 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.derivedstatus_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) + +## StatusServiceSetup.derivedStatus$ property + +The status of this plugin as derived from its dependencies. + +Signature: + +```typescript +derivedStatus$: Observable; +``` + +## Remarks + +By default, plugins inherit this derived status from their dependencies. Calling overrides this default status. + +This may emit multliple times for a single status change event as propagates through the dependency tree + diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md index 3d3b73ccda25f8..ba0645be4d26c7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md @@ -12,10 +12,73 @@ API for accessing status of Core and this plugin's dependencies as well as for c export interface StatusServiceSetup ``` +## Remarks + +By default, a plugin inherits it's current status from the most severe status level of any Core services and any plugins that it depends on. This default status is available on the API. + +Plugins may customize their status calculation by calling the API with an Observable. Within this Observable, a plugin may choose to only depend on the status of some of its dependencies, to ignore severe status levels of particular Core services they are not concerned with, or to make its status dependent on other external services. + +## Example 1 + +Customize a plugin's status to only depend on the status of SavedObjects: + +```ts +core.status.set( + core.status.core$.pipe( +. map((coreStatus) => { + return coreStatus.savedObjects; + }) ; + ); +); + +``` + +## Example 2 + +Customize a plugin's status to include an external service: + +```ts +const externalStatus$ = interval(1000).pipe( + switchMap(async () => { + const resp = await fetch(`https://myexternaldep.com/_healthz`); + const body = await resp.json(); + if (body.ok) { + return of({ level: ServiceStatusLevels.available, summary: 'External Service is up'}); + } else { + return of({ level: ServiceStatusLevels.available, summary: 'External Service is unavailable'}); + } + }), + catchError((error) => { + of({ level: ServiceStatusLevels.unavailable, summary: `External Service is down`, meta: { error }}) + }) +); + +core.status.set( + combineLatest([core.status.derivedStatus$, externalStatus$]).pipe( + map(([derivedStatus, externalStatus]) => { + if (externalStatus.level > derivedStatus) { + return externalStatus; + } else { + return derivedStatus; + } + }) + ) +); + +``` + ## Properties | Property | Type | Description | | --- | --- | --- | | [core$](./kibana-plugin-core-server.statusservicesetup.core_.md) | Observable<CoreStatus> | Current status for all Core services. | +| [dependencies$](./kibana-plugin-core-server.statusservicesetup.dependencies_.md) | Observable<Record<string, ServiceStatus>> | Current status for all plugins this plugin depends on. Each key of the Record is a plugin id. | +| [derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) | Observable<ServiceStatus> | The status of this plugin as derived from its dependencies. | | [overall$](./kibana-plugin-core-server.statusservicesetup.overall_.md) | Observable<ServiceStatus> | Overall system status for all of Kibana. | +## Methods + +| Method | Description | +| --- | --- | +| [set(status$)](./kibana-plugin-core-server.statusservicesetup.set.md) | Allows a plugin to specify a custom status dependent on its own criteria. Completely overrides the default inherited status. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md new file mode 100644 index 00000000000000..143cd397c40ae4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [set](./kibana-plugin-core-server.statusservicesetup.set.md) + +## StatusServiceSetup.set() method + +Allows a plugin to specify a custom status dependent on its own criteria. Completely overrides the default inherited status. + +Signature: + +```typescript +set(status$: Observable): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| status$ | Observable<ServiceStatus> | | + +Returns: + +`void` + +## Remarks + +See the [StatusServiceSetup.derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) API for leveraging the default status calculation that is provided by Core. + diff --git a/rfcs/text/0010_service_status.md b/rfcs/text/0010_service_status.md index ded594930a3677..76195c4f1ab89f 100644 --- a/rfcs/text/0010_service_status.md +++ b/rfcs/text/0010_service_status.md @@ -137,7 +137,7 @@ interface StatusSetup { * Current status for all dependencies of the current plugin. * Each key of the `Record` is a plugin id. */ - plugins$: Observable>; + dependencies$: Observable>; /** * The status of this plugin as derived from its dependencies. diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index adfdecdd7c9761..7d5557be92b300 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -323,6 +323,9 @@ export class LegacyService implements CoreService { status: { core$: setupDeps.core.status.core$, overall$: setupDeps.core.status.overall$, + set: setupDeps.core.status.plugins.set.bind(null, 'legacy'), + dependencies$: setupDeps.core.status.plugins.getDependenciesStatus$('legacy'), + derivedStatus$: setupDeps.core.status.plugins.getDerivedStatus$('legacy'), }, uiSettings: { register: setupDeps.core.uiSettings.register, diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index fa2659ca130a03..eb31b2380d177f 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -185,6 +185,9 @@ export function createPluginSetupContext( status: { core$: deps.status.core$, overall$: deps.status.overall$, + set: deps.status.plugins.set.bind(null, plugin.name), + dependencies$: deps.status.plugins.getDependenciesStatus$(plugin.name), + derivedStatus$: deps.status.plugins.getDerivedStatus$(plugin.name), }, uiSettings: { register: deps.uiSettings.register, diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index 7af77491df1ab8..71ac31db13f928 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -100,15 +100,27 @@ test('getPluginDependencies returns dependency tree of symbols', () => { pluginsSystem.addPlugin(createPlugin('no-dep')); expect(pluginsSystem.getPluginDependencies()).toMatchInlineSnapshot(` - Map { - Symbol(plugin-a) => Array [ - Symbol(no-dep), - ], - Symbol(plugin-b) => Array [ - Symbol(plugin-a), - Symbol(no-dep), - ], - Symbol(no-dep) => Array [], + Object { + "asNames": Map { + "plugin-a" => Array [ + "no-dep", + ], + "plugin-b" => Array [ + "plugin-a", + "no-dep", + ], + "no-dep" => Array [], + }, + "asOpaqueIds": Map { + Symbol(plugin-a) => Array [ + Symbol(no-dep), + ], + Symbol(plugin-b) => Array [ + Symbol(plugin-a), + Symbol(no-dep), + ], + Symbol(no-dep) => Array [], + }, } `); }); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index f5c1b35d678a36..b2acd9a6fd04bb 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -20,10 +20,11 @@ import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { PluginWrapper } from './plugin'; -import { DiscoveredPlugin, PluginName, PluginOpaqueId } from './types'; +import { DiscoveredPlugin, PluginName } from './types'; import { createPluginSetupContext, createPluginStartContext } from './plugin_context'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; import { withTimeout } from '../../utils'; +import { PluginDependencies } from '.'; const Sec = 1000; /** @internal */ @@ -45,9 +46,19 @@ export class PluginsSystem { * @returns a ReadonlyMap of each plugin and an Array of its available dependencies * @internal */ - public getPluginDependencies(): ReadonlyMap { - // Return dependency map of opaque ids - return new Map( + public getPluginDependencies(): PluginDependencies { + const asNames = new Map( + [...this.plugins].map(([name, plugin]) => [ + plugin.name, + [ + ...new Set([ + ...plugin.requiredPlugins, + ...plugin.optionalPlugins.filter((optPlugin) => this.plugins.has(optPlugin)), + ]), + ].map((depId) => this.plugins.get(depId)!.name), + ]) + ); + const asOpaqueIds = new Map( [...this.plugins].map(([name, plugin]) => [ plugin.opaqueId, [ @@ -58,6 +69,8 @@ export class PluginsSystem { ].map((depId) => this.plugins.get(depId)!.opaqueId), ]) ); + + return { asNames, asOpaqueIds }; } public async setupPlugins(deps: PluginsServiceSetupDeps) { diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index eb2a9ca3daf5f7..517261b5bc9bb1 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -93,6 +93,12 @@ export type PluginName = string; /** @public */ export type PluginOpaqueId = symbol; +/** @internal */ +export interface PluginDependencies { + asNames: ReadonlyMap; + asOpaqueIds: ReadonlyMap; +} + /** * Describes the set of required and optional properties plugin can define in its * mandatory JSON manifest file. diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 49c97d837579d3..fb4e4494801ed6 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2853,10 +2853,17 @@ export type SharedGlobalConfig = RecursiveReadonly<{ // @public export type StartServicesAccessor = () => Promise<[CoreStart, TPluginsStart, TStart]>; +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ServiceStatusSetup" +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ServiceStatusSetup" +// // @public export interface StatusServiceSetup { core$: Observable; + dependencies$: Observable>; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "StatusSetup" + derivedStatus$: Observable; overall$: Observable; + set(status$: Observable): void; } // @public @@ -2949,8 +2956,8 @@ export const validBodyOutput: readonly ["data", "stream"]; // src/core/server/legacy/types.ts:165:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts // src/core/server/legacy/types.ts:166:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts // src/core/server/legacy/types.ts:167:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:268:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:272:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:272:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:274:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 417f66a2988c2e..1bd364c2f87b77 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -41,6 +41,7 @@ import { Server } from './server'; import { getEnvOptions } from './config/__mocks__/env'; import { loggingSystemMock } from './logging/logging_system.mock'; import { rawConfigServiceMock } from './config/raw_config_service.mock'; +import { PluginName } from './plugins'; const env = new Env('.', getEnvOptions()); const logger = loggingSystemMock.create(); @@ -49,7 +50,7 @@ const rawConfigService = rawConfigServiceMock.create({}); beforeEach(() => { mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); mockPluginsService.discover.mockResolvedValue({ - pluginTree: new Map(), + pluginTree: { asOpaqueIds: new Map(), asNames: new Map() }, uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, }); }); @@ -98,7 +99,7 @@ test('injects legacy dependency to context#setup()', async () => { [pluginB, [pluginA]], ]); mockPluginsService.discover.mockResolvedValue({ - pluginTree: pluginDependencies, + pluginTree: { asOpaqueIds: pluginDependencies, asNames: new Map() }, uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, }); @@ -113,6 +114,31 @@ test('injects legacy dependency to context#setup()', async () => { }); }); +test('injects legacy dependency to status#setup()', async () => { + const server = new Server(rawConfigService, env, logger); + + const pluginDependencies = new Map([ + ['a', []], + ['b', ['a']], + ]); + mockPluginsService.discover.mockResolvedValue({ + pluginTree: { asOpaqueIds: new Map(), asNames: pluginDependencies }, + uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, + }); + + await server.setup(); + + expect(mockStatusService.setup).toHaveBeenCalledWith({ + elasticsearch: expect.any(Object), + savedObjects: expect.any(Object), + pluginDependencies: new Map([ + ['a', []], + ['b', ['a']], + ['legacy', ['a', 'b']], + ]), + }); +}); + test('runs services on "start"', async () => { const server = new Server(rawConfigService, env, logger); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index cc6d8171e7a037..e2f77f0551f34c 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -121,10 +121,13 @@ export class Server { const contextServiceSetup = this.context.setup({ // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: - // 1) Can access context from any NP plugin + // 1) Can access context from any KP plugin // 2) Can register context providers that will only be available to other legacy plugins and will not leak into // New Platform plugins. - pluginDependencies: new Map([...pluginTree, [this.legacy.legacyId, [...pluginTree.keys()]]]), + pluginDependencies: new Map([ + ...pluginTree.asOpaqueIds, + [this.legacy.legacyId, [...pluginTree.asOpaqueIds.keys()]], + ]), }); const auditTrailSetup = this.auditTrail.setup(); @@ -154,6 +157,12 @@ export class Server { const statusSetup = await this.status.setup({ elasticsearch: elasticsearchServiceSetup, + // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy can access plugin status from + // any KP plugin + pluginDependencies: new Map([ + ...pluginTree.asNames, + ['legacy', [...pluginTree.asNames.keys()]], + ]), savedObjects: savedObjectsSetup, }); diff --git a/src/core/server/status/get_summary_status.test.ts b/src/core/server/status/get_summary_status.test.ts index 7516e82ee784de..d97083162b5028 100644 --- a/src/core/server/status/get_summary_status.test.ts +++ b/src/core/server/status/get_summary_status.test.ts @@ -94,6 +94,38 @@ describe('getSummaryStatus', () => { describe('summary', () => { describe('when a single service is at highest level', () => { it('returns all information about that single service', () => { + expect( + getSummaryStatus( + Object.entries({ + s1: degraded, + s2: { + level: ServiceStatusLevels.unavailable, + summary: 'Lorem ipsum', + meta: { + custom: { data: 'here' }, + }, + }, + }) + ) + ).toEqual({ + level: ServiceStatusLevels.unavailable, + summary: '[s2]: Lorem ipsum', + detail: 'See the status page for more information', + meta: { + affectedServices: { + s2: { + level: ServiceStatusLevels.unavailable, + summary: 'Lorem ipsum', + meta: { + custom: { data: 'here' }, + }, + }, + }, + }, + }); + }); + + it('allows the single service to override the detail and documentationUrl fields', () => { expect( getSummaryStatus( Object.entries({ @@ -115,7 +147,17 @@ describe('getSummaryStatus', () => { detail: 'Vivamus pulvinar sem ac luctus ultrices.', documentationUrl: 'http://helpmenow.com/problem1', meta: { - custom: { data: 'here' }, + affectedServices: { + s2: { + level: ServiceStatusLevels.unavailable, + summary: 'Lorem ipsum', + detail: 'Vivamus pulvinar sem ac luctus ultrices.', + documentationUrl: 'http://helpmenow.com/problem1', + meta: { + custom: { data: 'here' }, + }, + }, + }, }, }); }); diff --git a/src/core/server/status/get_summary_status.ts b/src/core/server/status/get_summary_status.ts index 748a54f0bf8bba..1dc92839e8261b 100644 --- a/src/core/server/status/get_summary_status.ts +++ b/src/core/server/status/get_summary_status.ts @@ -23,7 +23,10 @@ import { ServiceStatus, ServiceStatusLevels, ServiceStatusLevel } from './types' * Returns a single {@link ServiceStatus} that summarizes the most severe status level from a group of statuses. * @param statuses */ -export const getSummaryStatus = (statuses: Array<[string, ServiceStatus]>): ServiceStatus => { +export const getSummaryStatus = ( + statuses: Array<[string, ServiceStatus]>, + { allAvailableSummary = `All services are available` }: { allAvailableSummary?: string } = {} +): ServiceStatus => { const grouped = groupByLevel(statuses); const highestSeverityLevel = getHighestSeverityLevel(grouped.keys()); const highestSeverityGroup = grouped.get(highestSeverityLevel)!; @@ -31,13 +34,18 @@ export const getSummaryStatus = (statuses: Array<[string, ServiceStatus]>): Serv if (highestSeverityLevel === ServiceStatusLevels.available) { return { level: ServiceStatusLevels.available, - summary: `All services are available`, + summary: allAvailableSummary, }; } else if (highestSeverityGroup.size === 1) { const [serviceName, status] = [...highestSeverityGroup.entries()][0]; return { ...status, summary: `[${serviceName}]: ${status.summary!}`, + // TODO: include URL to status page + detail: status.detail ?? `See the status page for more information`, + meta: { + affectedServices: { [serviceName]: status }, + }, }; } else { return { diff --git a/src/core/server/status/plugins_status.test.ts b/src/core/server/status/plugins_status.test.ts new file mode 100644 index 00000000000000..b2d2ac8a5ef90c --- /dev/null +++ b/src/core/server/status/plugins_status.test.ts @@ -0,0 +1,338 @@ +/* + * 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 { PluginName } from '../plugins'; +import { PluginsStatusService } from './plugins_status'; +import { of, Observable, BehaviorSubject } from 'rxjs'; +import { ServiceStatusLevels, CoreStatus, ServiceStatus } from './types'; +import { first } from 'rxjs/operators'; +import { ServiceStatusLevelSnapshotSerializer } from './test_utils'; + +expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer); + +describe('PluginStatusService', () => { + const coreAllAvailable$: Observable = of({ + elasticsearch: { level: ServiceStatusLevels.available, summary: 'elasticsearch avail' }, + savedObjects: { level: ServiceStatusLevels.available, summary: 'savedObjects avail' }, + }); + const coreOneDegraded$: Observable = of({ + elasticsearch: { level: ServiceStatusLevels.available, summary: 'elasticsearch avail' }, + savedObjects: { level: ServiceStatusLevels.degraded, summary: 'savedObjects degraded' }, + }); + const coreOneCriticalOneDegraded$: Observable = of({ + elasticsearch: { level: ServiceStatusLevels.critical, summary: 'elasticsearch critical' }, + savedObjects: { level: ServiceStatusLevels.degraded, summary: 'savedObjects degraded' }, + }); + const pluginDependencies: Map = new Map([ + ['a', []], + ['b', ['a']], + ['c', ['a', 'b']], + ]); + + describe('getDerivedStatus$', () => { + it(`defaults to core's most severe status`, async () => { + const serviceAvailable = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies, + }); + expect(await serviceAvailable.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.available, + summary: 'All dependencies are available', + }); + + const serviceDegraded = new PluginsStatusService({ + core$: coreOneDegraded$, + pluginDependencies, + }); + expect(await serviceDegraded.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.degraded, + summary: '[savedObjects]: savedObjects degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }); + + const serviceCritical = new PluginsStatusService({ + core$: coreOneCriticalOneDegraded$, + pluginDependencies, + }); + expect(await serviceCritical.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.critical, + summary: '[elasticsearch]: elasticsearch critical', + detail: 'See the status page for more information', + meta: expect.any(Object), + }); + }); + + it(`provides a summary status when core and dependencies are at same severity level`, async () => { + const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); + service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a is degraded' })); + expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.degraded, + summary: '[2] services are degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }); + }); + + it(`allows dependencies status to take precedence over lower severity core statuses`, async () => { + const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); + service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a is not working' })); + expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.unavailable, + summary: '[a]: a is not working', + detail: 'See the status page for more information', + meta: expect.any(Object), + }); + }); + + it(`allows core status to take precedence over lower severity dependencies statuses`, async () => { + const service = new PluginsStatusService({ + core$: coreOneCriticalOneDegraded$, + pluginDependencies, + }); + service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a is not working' })); + expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.critical, + summary: '[elasticsearch]: elasticsearch critical', + detail: 'See the status page for more information', + meta: expect.any(Object), + }); + }); + + it(`allows a severe dependency status to take precedence over a less severe dependency status`, async () => { + const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); + service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a is degraded' })); + service.set('b', of({ level: ServiceStatusLevels.unavailable, summary: 'b is not working' })); + expect(await service.getDerivedStatus$('c').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.unavailable, + summary: '[b]: b is not working', + detail: 'See the status page for more information', + meta: expect.any(Object), + }); + }); + }); + + describe('getAll$', () => { + it('defaults to empty record if no plugins', async () => { + const service = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies: new Map(), + }); + expect(await service.getAll$().pipe(first()).toPromise()).toEqual({}); + }); + + it('defaults to core status when no plugin statuses are set', async () => { + const serviceAvailable = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies, + }); + expect(await serviceAvailable.getAll$().pipe(first()).toPromise()).toEqual({ + a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + c: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + }); + + const serviceDegraded = new PluginsStatusService({ + core$: coreOneDegraded$, + pluginDependencies, + }); + expect(await serviceDegraded.getAll$().pipe(first()).toPromise()).toEqual({ + a: { + level: ServiceStatusLevels.degraded, + summary: '[savedObjects]: savedObjects degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + b: { + level: ServiceStatusLevels.degraded, + summary: '[2] services are degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + c: { + level: ServiceStatusLevels.degraded, + summary: '[3] services are degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + }); + + const serviceCritical = new PluginsStatusService({ + core$: coreOneCriticalOneDegraded$, + pluginDependencies, + }); + expect(await serviceCritical.getAll$().pipe(first()).toPromise()).toEqual({ + a: { + level: ServiceStatusLevels.critical, + summary: '[elasticsearch]: elasticsearch critical', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + b: { + level: ServiceStatusLevels.critical, + summary: '[2] services are critical', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + c: { + level: ServiceStatusLevels.critical, + summary: '[3] services are critical', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + }); + }); + + it('uses the manually set status level if plugin specifies one', async () => { + const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); + service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a status' })); + + expect(await service.getAll$().pipe(first()).toPromise()).toEqual({ + a: { level: ServiceStatusLevels.available, summary: 'a status' }, // a is available depsite savedObjects being degraded + b: { + level: ServiceStatusLevels.degraded, + summary: '[savedObjects]: savedObjects degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + c: { + level: ServiceStatusLevels.degraded, + summary: '[2] services are degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + }); + }); + + it('updates when a new plugin status observable is set', async () => { + const service = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies: new Map([['a', []]]), + }); + const statusUpdates: Array> = []; + const subscription = service + .getAll$() + .subscribe((pluginStatuses) => statusUpdates.push(pluginStatuses)); + + service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a degraded' })); + service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a unavailable' })); + service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a available' })); + subscription.unsubscribe(); + + expect(statusUpdates).toEqual([ + { a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' } }, + { a: { level: ServiceStatusLevels.degraded, summary: 'a degraded' } }, + { a: { level: ServiceStatusLevels.unavailable, summary: 'a unavailable' } }, + { a: { level: ServiceStatusLevels.available, summary: 'a available' } }, + ]); + }); + }); + + describe('getDependenciesStatus$', () => { + it('only includes dependencies of specified plugin', async () => { + const service = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies, + }); + expect(await service.getDependenciesStatus$('a').pipe(first()).toPromise()).toEqual({}); + expect(await service.getDependenciesStatus$('b').pipe(first()).toPromise()).toEqual({ + a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + }); + expect(await service.getDependenciesStatus$('c').pipe(first()).toPromise()).toEqual({ + a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + }); + }); + + it('uses the manually set status level if plugin specifies one', async () => { + const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); + service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a status' })); + + expect(await service.getDependenciesStatus$('c').pipe(first()).toPromise()).toEqual({ + a: { level: ServiceStatusLevels.available, summary: 'a status' }, // a is available depsite savedObjects being degraded + b: { + level: ServiceStatusLevels.degraded, + summary: '[savedObjects]: savedObjects degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + }); + }); + + it('throws error if unknown plugin passed', () => { + const service = new PluginsStatusService({ core$: coreAllAvailable$, pluginDependencies }); + expect(() => { + service.getDependenciesStatus$('dont-exist'); + }).toThrowError(); + }); + + it('debounces events in quick succession', async () => { + const service = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies, + }); + const available: ServiceStatus = { + level: ServiceStatusLevels.available, + summary: 'a available', + }; + const degraded: ServiceStatus = { + level: ServiceStatusLevels.degraded, + summary: 'a degraded', + }; + const pluginA$ = new BehaviorSubject(available); + service.set('a', pluginA$); + + const statusUpdates: Array> = []; + const subscription = service + .getDependenciesStatus$('b') + .subscribe((status) => statusUpdates.push(status)); + const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + + pluginA$.next(degraded); + pluginA$.next(available); + pluginA$.next(degraded); + pluginA$.next(available); + pluginA$.next(degraded); + pluginA$.next(available); + pluginA$.next(degraded); + // Waiting for the debounce timeout should cut a new update + await delay(100); + pluginA$.next(available); + await delay(100); + subscription.unsubscribe(); + + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "a": Object { + "level": degraded, + "summary": "a degraded", + }, + }, + Object { + "a": Object { + "level": available, + "summary": "a available", + }, + }, + ] + `); + }); + }); +}); diff --git a/src/core/server/status/plugins_status.ts b/src/core/server/status/plugins_status.ts new file mode 100644 index 00000000000000..df6f13eeec4e58 --- /dev/null +++ b/src/core/server/status/plugins_status.ts @@ -0,0 +1,98 @@ +/* + * 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 { BehaviorSubject, Observable, combineLatest, of } from 'rxjs'; +import { map, distinctUntilChanged, switchMap, debounceTime } from 'rxjs/operators'; +import { isDeepStrictEqual } from 'util'; + +import { PluginName } from '../plugins'; +import { ServiceStatus, CoreStatus } from './types'; +import { getSummaryStatus } from './get_summary_status'; + +interface Deps { + core$: Observable; + pluginDependencies: ReadonlyMap; +} + +export class PluginsStatusService { + private readonly pluginStatuses = new Map>(); + private readonly update$ = new BehaviorSubject(true); + constructor(private readonly deps: Deps) {} + + public set(plugin: PluginName, status$: Observable) { + this.pluginStatuses.set(plugin, status$); + this.update$.next(true); // trigger all existing Observables to update from the new source Observable + } + + public getAll$(): Observable> { + return this.getPluginStatuses$([...this.deps.pluginDependencies.keys()]); + } + + public getDependenciesStatus$(plugin: PluginName): Observable> { + const dependencies = this.deps.pluginDependencies.get(plugin); + if (!dependencies) { + throw new Error(`Unknown plugin: ${plugin}`); + } + + return this.getPluginStatuses$(dependencies).pipe( + // Prevent many emissions at once from dependency status resolution from making this too noisy + debounceTime(100) + ); + } + + public getDerivedStatus$(plugin: PluginName): Observable { + return combineLatest([this.deps.core$, this.getDependenciesStatus$(plugin)]).pipe( + map(([coreStatus, pluginStatuses]) => { + return getSummaryStatus( + [...Object.entries(coreStatus), ...Object.entries(pluginStatuses)], + { + allAvailableSummary: `All dependencies are available`, + } + ); + }) + ); + } + + private getPluginStatuses$(plugins: PluginName[]): Observable> { + if (plugins.length === 0) { + return of({}); + } + + return this.update$.pipe( + switchMap(() => { + const pluginStatuses = plugins + .map( + (depName) => + [depName, this.pluginStatuses.get(depName) ?? this.getDerivedStatus$(depName)] as [ + PluginName, + Observable + ] + ) + .map(([pName, status$]) => + status$.pipe(map((status) => [pName, status] as [PluginName, ServiceStatus])) + ); + + return combineLatest(pluginStatuses).pipe( + map((statuses) => Object.fromEntries(statuses)), + distinctUntilChanged(isDeepStrictEqual) + ); + }) + ); + } +} diff --git a/src/core/server/status/status_service.mock.ts b/src/core/server/status/status_service.mock.ts index 47ef8659b40796..42b3eecdca310f 100644 --- a/src/core/server/status/status_service.mock.ts +++ b/src/core/server/status/status_service.mock.ts @@ -40,6 +40,9 @@ const createSetupContractMock = () => { const setupContract: jest.Mocked = { core$: new BehaviorSubject(availableCoreStatus), overall$: new BehaviorSubject(available), + set: jest.fn(), + dependencies$: new BehaviorSubject({}), + derivedStatus$: new BehaviorSubject(available), }; return setupContract; @@ -50,6 +53,11 @@ const createInternalSetupContractMock = () => { core$: new BehaviorSubject(availableCoreStatus), overall$: new BehaviorSubject(available), isStatusPageAnonymous: jest.fn().mockReturnValue(false), + plugins: { + set: jest.fn(), + getDependenciesStatus$: jest.fn(), + getDerivedStatus$: jest.fn(), + }, }; return setupContract; diff --git a/src/core/server/status/status_service.test.ts b/src/core/server/status/status_service.test.ts index 863fe34e8ecea8..341c40a86bf77f 100644 --- a/src/core/server/status/status_service.test.ts +++ b/src/core/server/status/status_service.test.ts @@ -34,6 +34,7 @@ describe('StatusService', () => { service = new StatusService(mockCoreContext.create()); }); + const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const available: ServiceStatus = { level: ServiceStatusLevels.available, summary: 'Available', @@ -53,6 +54,7 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, + pluginDependencies: new Map(), }); expect(await setup.core$.pipe(first()).toPromise()).toEqual({ elasticsearch: available, @@ -68,6 +70,7 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, + pluginDependencies: new Map(), }); const subResult1 = await setup.core$.pipe(first()).toPromise(); const subResult2 = await setup.core$.pipe(first()).toPromise(); @@ -96,6 +99,7 @@ describe('StatusService', () => { savedObjects: { status$: savedObjects$, }, + pluginDependencies: new Map(), }); const statusUpdates: CoreStatus[] = []; @@ -158,6 +162,7 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, + pluginDependencies: new Map(), }); expect(await setup.overall$.pipe(first()).toPromise()).toMatchObject({ level: ServiceStatusLevels.degraded, @@ -173,6 +178,7 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, + pluginDependencies: new Map(), }); const subResult1 = await setup.overall$.pipe(first()).toPromise(); const subResult2 = await setup.overall$.pipe(first()).toPromise(); @@ -201,26 +207,95 @@ describe('StatusService', () => { savedObjects: { status$: savedObjects$, }, + pluginDependencies: new Map(), }); const statusUpdates: ServiceStatus[] = []; const subscription = setup.overall$.subscribe((status) => statusUpdates.push(status)); + // Wait for timers to ensure that duplicate events are still filtered out regardless of debouncing. elasticsearch$.next(available); + await delay(100); elasticsearch$.next(available); + await delay(100); elasticsearch$.next({ level: ServiceStatusLevels.available, summary: `Wow another summary`, }); + await delay(100); savedObjects$.next(degraded); + await delay(100); savedObjects$.next(available); + await delay(100); savedObjects$.next(available); + await delay(100); subscription.unsubscribe(); expect(statusUpdates).toMatchInlineSnapshot(` Array [ Object { + "detail": "See the status page for more information", "level": degraded, + "meta": Object { + "affectedServices": Object { + "savedObjects": Object { + "level": degraded, + "summary": "This is degraded!", + }, + }, + }, + "summary": "[savedObjects]: This is degraded!", + }, + Object { + "level": available, + "summary": "All services are available", + }, + ] + `); + }); + + it('debounces events in quick succession', async () => { + const savedObjects$ = new BehaviorSubject(available); + const setup = await service.setup({ + elasticsearch: { + status$: new BehaviorSubject(available), + }, + savedObjects: { + status$: savedObjects$, + }, + pluginDependencies: new Map(), + }); + + const statusUpdates: ServiceStatus[] = []; + const subscription = setup.overall$.subscribe((status) => statusUpdates.push(status)); + + // All of these should debounced into a single `available` status + savedObjects$.next(degraded); + savedObjects$.next(available); + savedObjects$.next(degraded); + savedObjects$.next(available); + savedObjects$.next(degraded); + savedObjects$.next(available); + savedObjects$.next(degraded); + // Waiting for the debounce timeout should cut a new update + await delay(100); + savedObjects$.next(available); + await delay(100); + subscription.unsubscribe(); + + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "detail": "See the status page for more information", + "level": degraded, + "meta": Object { + "affectedServices": Object { + "savedObjects": Object { + "level": degraded, + "summary": "This is degraded!", + }, + }, + }, "summary": "[savedObjects]: This is degraded!", }, Object { diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts index aea335e64babf8..59e81343597c94 100644 --- a/src/core/server/status/status_service.ts +++ b/src/core/server/status/status_service.ts @@ -18,7 +18,7 @@ */ import { Observable, combineLatest } from 'rxjs'; -import { map, distinctUntilChanged, shareReplay, take } from 'rxjs/operators'; +import { map, distinctUntilChanged, shareReplay, take, debounceTime } from 'rxjs/operators'; import { isDeepStrictEqual } from 'util'; import { CoreService } from '../../types'; @@ -26,13 +26,16 @@ import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { InternalElasticsearchServiceSetup } from '../elasticsearch'; import { InternalSavedObjectsServiceSetup } from '../saved_objects'; +import { PluginName } from '../plugins'; import { config, StatusConfigType } from './status_config'; import { ServiceStatus, CoreStatus, InternalStatusServiceSetup } from './types'; import { getSummaryStatus } from './get_summary_status'; +import { PluginsStatusService } from './plugins_status'; interface SetupDeps { elasticsearch: Pick; + pluginDependencies: ReadonlyMap; savedObjects: Pick; } @@ -40,17 +43,29 @@ export class StatusService implements CoreService { private readonly logger: Logger; private readonly config$: Observable; + private pluginsStatus?: PluginsStatusService; + constructor(coreContext: CoreContext) { this.logger = coreContext.logger.get('status'); this.config$ = coreContext.configService.atPath(config.path); } - public async setup(core: SetupDeps) { + public async setup({ elasticsearch, pluginDependencies, savedObjects }: SetupDeps) { const statusConfig = await this.config$.pipe(take(1)).toPromise(); - const core$ = this.setupCoreStatus(core); - const overall$: Observable = core$.pipe( - map((coreStatus) => { - const summary = getSummaryStatus(Object.entries(coreStatus)); + const core$ = this.setupCoreStatus({ elasticsearch, savedObjects }); + this.pluginsStatus = new PluginsStatusService({ core$, pluginDependencies }); + + const overall$: Observable = combineLatest( + core$, + this.pluginsStatus.getAll$() + ).pipe( + // Prevent many emissions at once from dependency status resolution from making this too noisy + debounceTime(100), + map(([coreStatus, pluginsStatus]) => { + const summary = getSummaryStatus([ + ...Object.entries(coreStatus), + ...Object.entries(pluginsStatus), + ]); this.logger.debug(`Recalculated overall status`, { status: summary }); return summary; }), @@ -60,6 +75,11 @@ export class StatusService implements CoreService { return { core$, overall$, + plugins: { + set: this.pluginsStatus.set.bind(this.pluginsStatus), + getDependenciesStatus$: this.pluginsStatus.getDependenciesStatus$.bind(this.pluginsStatus), + getDerivedStatus$: this.pluginsStatus.getDerivedStatus$.bind(this.pluginsStatus), + }, isStatusPageAnonymous: () => statusConfig.allowAnonymous, }; } @@ -68,7 +88,10 @@ export class StatusService implements CoreService { public stop() {} - private setupCoreStatus({ elasticsearch, savedObjects }: SetupDeps): Observable { + private setupCoreStatus({ + elasticsearch, + savedObjects, + }: Pick): Observable { return combineLatest([elasticsearch.status$, savedObjects.status$]).pipe( map(([elasticsearchStatus, savedObjectsStatus]) => ({ elasticsearch: elasticsearchStatus, diff --git a/src/core/server/status/types.ts b/src/core/server/status/types.ts index 2ecf11deb2960e..f884b80316fa81 100644 --- a/src/core/server/status/types.ts +++ b/src/core/server/status/types.ts @@ -19,6 +19,7 @@ import { Observable } from 'rxjs'; import { deepFreeze } from '../../utils'; +import { PluginName } from '../plugins'; /** * The current status of a service at a point in time. @@ -116,6 +117,60 @@ export interface CoreStatus { /** * API for accessing status of Core and this plugin's dependencies as well as for customizing this plugin's status. + * + * @remarks + * By default, a plugin inherits it's current status from the most severe status level of any Core services and any + * plugins that it depends on. This default status is available on the + * {@link ServiceStatusSetup.derivedStatus$ | core.status.derviedStatus$} API. + * + * Plugins may customize their status calculation by calling the {@link ServiceStatusSetup.set | core.status.set} API + * with an Observable. Within this Observable, a plugin may choose to only depend on the status of some of its + * dependencies, to ignore severe status levels of particular Core services they are not concerned with, or to make its + * status dependent on other external services. + * + * @example + * Customize a plugin's status to only depend on the status of SavedObjects: + * ```ts + * core.status.set( + * core.status.core$.pipe( + * . map((coreStatus) => { + * return coreStatus.savedObjects; + * }) ; + * ); + * ); + * ``` + * + * @example + * Customize a plugin's status to include an external service: + * ```ts + * const externalStatus$ = interval(1000).pipe( + * switchMap(async () => { + * const resp = await fetch(`https://myexternaldep.com/_healthz`); + * const body = await resp.json(); + * if (body.ok) { + * return of({ level: ServiceStatusLevels.available, summary: 'External Service is up'}); + * } else { + * return of({ level: ServiceStatusLevels.available, summary: 'External Service is unavailable'}); + * } + * }), + * catchError((error) => { + * of({ level: ServiceStatusLevels.unavailable, summary: `External Service is down`, meta: { error }}) + * }) + * ); + * + * core.status.set( + * combineLatest([core.status.derivedStatus$, externalStatus$]).pipe( + * map(([derivedStatus, externalStatus]) => { + * if (externalStatus.level > derivedStatus) { + * return externalStatus; + * } else { + * return derivedStatus; + * } + * }) + * ) + * ); + * ``` + * * @public */ export interface StatusServiceSetup { @@ -134,9 +189,43 @@ export interface StatusServiceSetup { * only depend on the statuses of {@link StatusServiceSetup.core$ | Core} or their dependencies. */ overall$: Observable; + + /** + * Allows a plugin to specify a custom status dependent on its own criteria. + * Completely overrides the default inherited status. + * + * @remarks + * See the {@link StatusServiceSetup.derivedStatus$} API for leveraging the default status + * calculation that is provided by Core. + */ + set(status$: Observable): void; + + /** + * Current status for all plugins this plugin depends on. + * Each key of the `Record` is a plugin id. + */ + dependencies$: Observable>; + + /** + * The status of this plugin as derived from its dependencies. + * + * @remarks + * By default, plugins inherit this derived status from their dependencies. + * Calling {@link StatusSetup.set} overrides this default status. + * + * This may emit multliple times for a single status change event as propagates + * through the dependency tree + */ + derivedStatus$: Observable; } /** @internal */ -export interface InternalStatusServiceSetup extends StatusServiceSetup { +export interface InternalStatusServiceSetup extends Pick { isStatusPageAnonymous: () => boolean; + // Namespaced under `plugins` key to improve clarity that these are APIs for plugins specifically. + plugins: { + set(plugin: PluginName, status$: Observable): void; + getDependenciesStatus$(plugin: PluginName): Observable>; + getDerivedStatus$(plugin: PluginName): Observable; + }; } From 21140178589c86c946867cf00a0595ba19dd64cf Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 1 Sep 2020 06:49:00 -0700 Subject: [PATCH 147/216] [docs] Updates to development branching (#76063) Signed-off-by: Tyler Smalley Co-authored-by: Stacey Gammon Co-authored-by: Brandon Kobel Co-authored-by: Peter Schretlen --- .../contributing/development-github.asciidoc | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/developer/contributing/development-github.asciidoc b/docs/developer/contributing/development-github.asciidoc index a6d4e29940487a..84f51843098a70 100644 --- a/docs/developer/contributing/development-github.asciidoc +++ b/docs/developer/contributing/development-github.asciidoc @@ -1,5 +1,5 @@ [[development-github]] -== How we use git and github +== How we use Git and GitHub [discrete] === Forking @@ -12,17 +12,21 @@ repo, which we'll refer to in later code snippets. [discrete] === Branching -* All work on the next major release goes into master. -* Past major release branches are named `{majorVersion}.x`. They contain -work that will go into the next minor release. For example, if the next -minor release is `5.2.0`, work for it should go into the `5.x` branch. -* Past minor release branches are named `{majorVersion}.{minorVersion}`. -They contain work that will go into the next patch release. For example, -if the next patch release is `5.3.1`, work for it should go into the -`5.3` branch. -* All work is done on feature branches and merged into one of these -branches. -* Where appropriate, we'll backport changes into older release branches. +At Elastic, all products in the stack, including Kibana, are released at the same time with the same version number. Most of these projects have the following branching strategy: + +* `master` is the next major version. +* `.x` is the next minor version. +* `.` is the next release of a minor version, including patch releases. + +As an example, let's assume that the `7.x` branch is currently a not-yet-released `7.6.0`. Once `7.6.0` has reached feature freeze, it will be branched to `7.6` and `7.x` will be updated to reflect `7.7.0`. The release of `7.6.0` and subsequent patch releases will be cut from the `7.6` branch. At any time, you can verify the current version of a branch by inspecting the `version` attribute in the `package.json` file within the Kibana source. + +Pull requests are made into the `master` branch and then backported when it is safe and appropriate. + +* Breaking changes do not get backported and only go into `master`. +* All non-breaking changes can be backported to the `.x` branch. +* Features should not be backported to a `.` branch. +* Bugs can be backported to a `.` branch if the changes are safe and appropriate. Safety is a judgment call you make based on factors like the bug's severity, test coverage, confidence in the changes, etc. Your reasoning should be included in the pull request description. +* Documentation changes can be backported to any branch at any time. [discrete] === Commits and Merging @@ -109,4 +113,4 @@ Assuming you've successfully rebased and you're happy with the code, you should [discrete] === Creating a pull request -See <> for the next steps on getting your code changes merged into {kib}. \ No newline at end of file +See <> for the next steps on getting your code changes merged into {kib}. From 9a27beba75a57e39caeb3dcfa1c57000da6cb630 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 1 Sep 2020 07:11:42 -0700 Subject: [PATCH 148/216] [ML] Remove "Are you sure" from anomaly detection jobs (#75931) --- .../__snapshots__/delete_rule_modal.test.js.snap | 12 ++---------- .../select_rule_action/delete_rule_modal.js | 11 ++--------- .../components/delete_job_modal/delete_job_modal.js | 13 ++----------- x-pack/plugins/translations/translations/ja-JP.json | 2 -- x-pack/plugins/translations/translations/zh-CN.json | 2 -- 5 files changed, 6 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap index 708bddd1453930..a132e6682ee250 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap +++ b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap @@ -64,20 +64,12 @@ exports[`DeleteRuleModal renders modal after clicking delete rule link 1`] = ` onConfirm={[Function]} title={ } - > -

- -

- + /> `; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js index 9fcd457df008f0..5140fe77ff9792 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js @@ -47,7 +47,7 @@ export class DeleteRuleModal extends Component { title={ } onCancel={this.closeModal} @@ -66,14 +66,7 @@ export class DeleteRuleModal extends Component { /> } defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - > -

- -

- + /> ); } diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js index 1e3ec6241311bc..f80578cb18341c 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js @@ -98,7 +98,7 @@ export class DeleteJobModal extends Component { const title = ( -

- -

Date: Tue, 1 Sep 2020 09:27:03 -0700 Subject: [PATCH 149/216] [Reporting/CSV] Do not fail the job if scroll ID can not be cleared (#76014) Co-authored-by: Elastic Machine --- .../csv/generate_csv/hit_iterator.test.ts | 36 +++++++++++++++---- .../csv/generate_csv/hit_iterator.ts | 16 ++++++--- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.test.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.test.ts index 831bf45cf72ea2..b7147fe0a9ebde 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.test.ts @@ -7,17 +7,13 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { CancellationToken } from '../../../../common'; -import { LevelLogger } from '../../../lib'; +import { createMockLevelLogger } from '../../../test_helpers/create_mock_levellogger'; import { ScrollConfig } from '../../../types'; import { createHitIterator } from './hit_iterator'; -const mockLogger = { - error: new Function(), - debug: new Function(), - warning: new Function(), -} as LevelLogger; +const mockLogger = createMockLevelLogger(); const debugLogStub = sinon.stub(mockLogger, 'debug'); -const warnLogStub = sinon.stub(mockLogger, 'warning'); +const warnLogStub = sinon.stub(mockLogger, 'warn'); const errorLogStub = sinon.stub(mockLogger, 'error'); const mockCallEndpoint = sinon.stub(); const mockSearchRequest = {}; @@ -134,4 +130,30 @@ describe('hitIterator', function () { expect(errorLogStub.callCount).to.be(1); expect(errorThrown).to.be(true); }); + + it('handles scroll id could not be cleared', async () => { + // Setup + mockCallEndpoint.withArgs('clearScroll').rejects({ status: 404 }); + + // Begin + const hitIterator = createHitIterator(mockLogger); + const iterator = hitIterator( + mockConfig, + mockCallEndpoint, + mockSearchRequest, + realCancellationToken + ); + + while (true) { + const { done: iterationDone, value: hit } = await iterator.next(); + if (iterationDone) { + break; + } + expect(hit).to.be('you found me'); + } + + expect(mockCallEndpoint.callCount).to.be(13); + expect(warnLogStub.callCount).to.be(1); + expect(errorLogStub.callCount).to.be(1); + }); }); diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.ts index dee653cf30007b..b95a3112002660 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.ts @@ -68,11 +68,17 @@ export function createHitIterator(logger: LevelLogger) { ); } - function clearScroll(scrollId: string | undefined) { + async function clearScroll(scrollId: string | undefined) { logger.debug('executing clearScroll request'); - return callEndpoint('clearScroll', { - scrollId: [scrollId], - }); + try { + await callEndpoint('clearScroll', { + scrollId: [scrollId], + }); + } catch (err) { + // Do not throw the error, as the job can still be completed successfully + logger.warn('Scroll context can not be cleared!'); + logger.error(err); + } } try { @@ -86,7 +92,7 @@ export function createHitIterator(logger: LevelLogger) { ({ scrollId, hits } = await scroll(scrollId)); if (cancellationToken.isCancelled()) { - logger.warning( + logger.warn( 'Any remaining scrolling searches have been cancelled by the cancellation token.' ); } From 846647436e829c34e4f67edb9c4ce2df4abe7aab Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 1 Sep 2020 10:33:28 -0700 Subject: [PATCH 150/216] [yarn] remove typings-tester, use @ts-expect-error (#76341) Co-authored-by: spalger --- package.json | 2 - .../public/lib/aeroelastic/tsconfig.json | 21 --- .../typespec_tests.ts => typespec.test.ts} | 172 +++++++++--------- .../scripts/optimize_tsconfig/tsconfig.json | 3 +- x-pack/tsconfig.json | 3 +- yarn.lock | 9 +- 6 files changed, 94 insertions(+), 116 deletions(-) delete mode 100644 x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json rename x-pack/plugins/canvas/public/lib/aeroelastic/{__fixtures__/typescript/typespec_tests.ts => typespec.test.ts} (67%) diff --git a/package.json b/package.json index 5c95f538a00e1f..3485ce5d7a7fc2 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "test:ftr:server": "node scripts/functional_tests_server", "test:ftr:runner": "node scripts/functional_test_runner", "test:coverage": "grunt test:coverage", - "typespec": "typings-tester --config x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts", "checkLicenses": "node scripts/check_licenses --dev", "build": "node scripts/build --all-platforms", "start": "node scripts/kibana --dev", @@ -473,7 +472,6 @@ "topojson-client": "3.0.0", "tree-kill": "^1.2.2", "typescript": "4.0.2", - "typings-tester": "^0.3.2", "ui-select": "0.19.8", "vega": "^5.13.0", "vega-lite": "^4.13.1", diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json b/x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json deleted file mode 100644 index 3b61e4b414626c..00000000000000 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "extends": "../../../../../tsconfig", - "compilerOptions": { - "module": "commonjs", - "lib": ["es2018", "dom"], - "noImplicitAny": true, - "strictNullChecks": true, - "strictFunctionTypes": false, - "strictPropertyInitialization": true, - "noImplicitThis": true, - "noImplicitReturns": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "baseUrl": ".", - "paths": { - "layout/*": ["aeroelastic/*"] - }, - "types": ["@kbn/x-pack/plugins/canvas/public/lib/aeroelastic"] - }, - "exclude": ["node_modules", "**/*.spec.ts", "node_modules/@types/mocha"] -} diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts b/x-pack/plugins/canvas/public/lib/aeroelastic/typespec.test.ts similarity index 67% rename from x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts rename to x-pack/plugins/canvas/public/lib/aeroelastic/typespec.test.ts index cb46a3d6be4025..3b1fde12edcc48 100644 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/typespec.test.ts @@ -4,49 +4,47 @@ * you may not use this file except in compliance with the Elastic License. */ -import { select } from '../../select'; -import { Json, Selector, Vector2d, Vector3d, TransformMatrix2d, TransformMatrix3d } from '../..'; +import { select } from './select'; +import { Json, Selector, Vector2d, Vector3d, TransformMatrix2d, TransformMatrix3d } from './index'; import { mvMultiply as mult2d, ORIGIN as UNIT2D, UNITMATRIX as UNITMATRIX2D, add as add2d, -} from '../../matrix2d'; +} from './matrix2d'; import { mvMultiply as mult3d, ORIGIN as UNIT3D, NANMATRIX as NANMATRIX3D, add as add3d, -} from '../../matrix'; +} from './matrix'; -/* +// helper to mark variables as "used" so they don't trigger errors +const use = (...vars: any[]) => vars.includes(null); +/* Type checking isn't too useful if future commits can accidentally weaken the type constraints, because a TypeScript linter will not complain - everything that passed before will continue to pass. The coder will not have feedback that the original intent with the typing got compromised. To declare the intent via passing and failing type checks, test cases are needed, some of which designed to expect a TS pass, some of them to expect a TS complaint. It documents intent for peers too, as type specs are a tough read. - Run compile-time type specification tests in the `kibana` root with: - - yarn typespec - Test "cases" expecting to pass TS checks are not annotated, while ones we want TS to complain about are prepended with the comment - - // typings:expect-error + + // @ts-expect-error The test "suite" and "cases" are wrapped in IIFEs to prevent linters from complaining about the unused binding. It can be structured internally as desired. - */ -((): void => { - /** - * TYPE TEST SUITE - */ +describe('vector array creation', () => { + it('passes typechecking', () => { + let vec2d: Vector2d = UNIT2D; + let vec3d: Vector3d = UNIT3D; + + use(vec2d, vec3d); - (function vectorArrayCreationTests(vec2d: Vector2d, vec3d: Vector3d): void { // 2D vector OK vec2d = [0, 0, 0] as Vector2d; // OK vec2d = [-0, NaN, -Infinity] as Vector2d; // IEEE 754 values are OK @@ -57,30 +55,35 @@ import { // 2D vector not OK - // typings:expect-error + // @ts-expect-error vec2d = 3; // not even an array - // typings:expect-error + // @ts-expect-error vec2d = [] as Vector2d; // no elements - // typings:expect-error + // @ts-expect-error vec2d = [0, 0] as Vector2d; // too few elements - // typings:expect-error + // @ts-expect-error vec2d = [0, 0, 0, 0] as Vector2d; // too many elements // 3D vector not OK - // typings:expect-error + // @ts-expect-error vec3d = 3; // not even an array - // typings:expect-error + // @ts-expect-error vec3d = [] as Vector3d; // no elements - // typings:expect-error + // @ts-expect-error vec3d = [0, 0, 0] as Vector3d; // too few elements - // typings:expect-error + // @ts-expect-error vec3d = [0, 0, 0, 0, 0] as Vector3d; // too many elements + }); +}); + +describe('matrix array creation', () => { + it('passes typechecking', () => { + let mat2d: TransformMatrix2d = UNITMATRIX2D; + let mat3d: TransformMatrix3d = NANMATRIX3D; - return; // arrayCreationTests - })(UNIT2D, UNIT3D); + use(mat2d, mat3d); - (function matrixArrayCreationTests(mat2d: TransformMatrix2d, mat3d: TransformMatrix3d): void { // 2D matrix OK mat2d = [0, 1, 2, 3, 4, 5, 6, 7, 8] as TransformMatrix2d; // OK mat2d = [-0, NaN, -Infinity, 3, 4, 5, 6, 7, 8] as TransformMatrix2d; // IEEE 754 values are OK @@ -91,80 +94,87 @@ import { // 2D matrix not OK - // typings:expect-error + // @ts-expect-error mat2d = 3; // not even an array - // typings:expect-error + // @ts-expect-error mat2d = [] as TransformMatrix2d; // no elements - // typings:expect-error + // @ts-expect-error mat2d = [0, 1, 2, 3, 4, 5, 6, 7] as TransformMatrix2d; // too few elements - // typings:expect-error + // @ts-expect-error mat2d = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] as TransformMatrix2d; // too many elements // 3D vector not OK - // typings:expect-error + // @ts-expect-error mat3d = 3; // not even an array - // typings:expect-error + // @ts-expect-error mat3d = [] as TransformMatrix3d; // no elements - // typings:expect-error + // @ts-expect-error mat3d = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] as TransformMatrix3d; // too few elements - // typings:expect-error + // @ts-expect-error mat3d = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] as TransformMatrix3d; // too many elements - // Matrix modification should NOT be OK - mat3d[3] = 100; // too bad the ReadOnly part appears not to be enforced so can't precede it with typings:expect-error + // @ts-expect-error + mat3d[3] = 100; // Matrix modification is NOT OK + }); +}); - return; // arrayCreationTests - })(UNITMATRIX2D, NANMATRIX3D); +describe('matrix addition', () => { + it('passes typecheck', () => { + const mat2d: TransformMatrix2d = UNITMATRIX2D; + const mat3d: TransformMatrix3d = NANMATRIX3D; - (function matrixMatrixAdditionTests(mat2d: TransformMatrix2d, mat3d: TransformMatrix3d): void { add2d(mat2d, mat2d); // OK add3d(mat3d, mat3d); // OK - // typings:expect-error + // @ts-expect-error add2d(mat2d, mat3d); // at least one arg doesn't comply - // typings:expect-error + // @ts-expect-error add2d(mat3d, mat2d); // at least one arg doesn't comply - // typings:expect-error + // @ts-expect-error add2d(mat3d, mat3d); // at least one arg doesn't comply - // typings:expect-error + // @ts-expect-error add3d(mat2d, mat3d); // at least one arg doesn't comply - // typings:expect-error + // @ts-expect-error add3d(mat3d, mat2d); // at least one arg doesn't comply - // typings:expect-error + // @ts-expect-error add3d(mat2d, mat2d); // at least one arg doesn't comply + }); +}); - return; // matrixMatrixAdditionTests - })(UNITMATRIX2D, NANMATRIX3D); +describe('matric vector multiplication', () => { + it('passes typecheck', () => { + const vec2d: Vector2d = UNIT2D; + const mat2d: TransformMatrix2d = UNITMATRIX2D; + const vec3d: Vector3d = UNIT3D; + const mat3d: TransformMatrix3d = NANMATRIX3D; - (function matrixVectorMultiplicationTests( - vec2d: Vector2d, - mat2d: TransformMatrix2d, - vec3d: Vector3d, - mat3d: TransformMatrix3d - ): void { mult2d(mat2d, vec2d); // OK mult3d(mat3d, vec3d); // OK - // typings:expect-error + // @ts-expect-error mult3d(mat2d, vec2d); // trying to use a 3d fun for 2d args - // typings:expect-error + // @ts-expect-error mult2d(mat3d, vec3d); // trying to use a 2d fun for 3d args - // typings:expect-error + // @ts-expect-error mult2d(mat3d, vec2d); // 1st arg is a mismatch - // typings:expect-error + // @ts-expect-error mult2d(mat2d, vec3d); // 2nd arg is a mismatch - // typings:expect-error + // @ts-expect-error mult3d(mat2d, vec3d); // 1st arg is a mismatch - // typings:expect-error + // @ts-expect-error mult3d(mat3d, vec2d); // 2nd arg is a mismatch + }); +}); - return; // matrixVectorTests - })(UNIT2D, UNITMATRIX2D, UNIT3D, NANMATRIX3D); +describe('json', () => { + it('passes typecheck', () => { + let plain: Json = null; + + use(plain); - (function jsonTests(plain: Json): void { // numbers are OK plain = 1; plain = NaN; @@ -182,37 +192,37 @@ import { plain = [0, null, false, NaN, 3.14, 'one more']; plain = { a: { b: 5, c: { d: [1, 'a', -Infinity, null], e: -1 }, f: 'b' }, g: false }; - // typings:expect-error + // @ts-expect-error plain = undefined; // it's undefined - // typings:expect-error + // @ts-expect-error plain = (a) => a; // it's a function - // typings:expect-error + // @ts-expect-error plain = [new Date()]; // it's a time - // typings:expect-error + // @ts-expect-error plain = { a: Symbol('haha') }; // symbol isn't permitted either - // typings:expect-error + // @ts-expect-error plain = window || void 0; - // typings:expect-error + // @ts-expect-error plain = { a: { b: 5, c: { d: [1, 'a', undefined, null] } } }; // going deep into the structure + }); +}); - return; // jsonTests - })(null); +describe('select', () => { + it('passes typecheck', () => { + let selector: Selector; - (function selectTests(selector: Selector): void { selector = select((a: Json) => a); // one arg selector = select((a: Json, b: Json): Json => `${a} and ${b}`); // more args selector = select(() => 1); // zero arg selector = select((...args: Json[]) => args); // variadic - // typings:expect-error + // @ts-expect-error selector = (a: Json) => a; // not a selector - // typings:expect-error + // @ts-expect-error selector = select(() => {}); // should yield a JSON value, but it returns void - // typings:expect-error + // @ts-expect-error selector = select((x: Json) => ({ a: x, b: undefined })); // should return a Json - return; // selectTests - })(select((a: Json) => a)); - - return; // test suite -})(); + use(selector); + }); +}); diff --git a/x-pack/plugins/security_solution/scripts/optimize_tsconfig/tsconfig.json b/x-pack/plugins/security_solution/scripts/optimize_tsconfig/tsconfig.json index ea7a11b89dab2b..ac56a6af31c72c 100644 --- a/x-pack/plugins/security_solution/scripts/optimize_tsconfig/tsconfig.json +++ b/x-pack/plugins/security_solution/scripts/optimize_tsconfig/tsconfig.json @@ -10,7 +10,6 @@ "exclude": [ "test/**/*", "**/__fixtures__/**/*", - "plugins/security_solution/cypress/**/*", - "**/typespec_tests.ts" + "plugins/security_solution/cypress/**/*" ] } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 35e1800c6fbd19..7c6210bb9ce191 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -14,8 +14,7 @@ "test/**/*", "plugins/security_solution/cypress/**/*", "plugins/apm/e2e/cypress/**/*", - "plugins/apm/scripts/**/*", - "**/typespec_tests.ts" + "plugins/apm/scripts/**/*" ], "compilerOptions": { "outDir": ".", diff --git a/yarn.lock b/yarn.lock index c0c2305609f580..3be2599c64201d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9035,7 +9035,7 @@ comma-separated-tokens@^1.0.1: resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== -commander@2, commander@2.19.0, commander@^2.11.0, commander@^2.12.2: +commander@2, commander@2.19.0, commander@^2.11.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== @@ -28423,13 +28423,6 @@ typescript@4.0.2, typescript@^3.0.1, typescript@^3.0.3, typescript@^3.2.2, types resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2" integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== -typings-tester@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/typings-tester/-/typings-tester-0.3.2.tgz#04cc499d15ab1d8b2d14dd48415a13d01333bc5b" - integrity sha512-HjGoAM2UoGhmSKKy23TYEKkxlphdJFdix5VvqWFLzH1BJVnnwG38tpC6SXPgqhfFGfHY77RlN1K8ts0dbWBQ7A== - dependencies: - commander "^2.12.2" - ua-parser-js@^0.7.18: version "0.7.21" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777" From ff7e7effeda65065339f737ac845ad89323a11c6 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 1 Sep 2020 10:42:28 -0700 Subject: [PATCH 151/216] [ML] Remove "Are you sure" from data frame analytics jobs (#76214) --- .../components/action_delete/delete_action_modal.tsx | 10 +--------- .../components/action_start/start_action_modal.tsx | 4 ++-- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx index 5ffa5e304b9968..5db8446dec32f7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx @@ -14,7 +14,6 @@ import { EuiFlexItem, EUI_MODAL_CONFIRM_BUTTON, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { DeleteAction } from './use_delete_action'; @@ -40,7 +39,7 @@ export const DeleteActionModal: FC = ({ = ({ defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} buttonColor="danger" > -

- -

- {userCanDeleteIndex && ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_action_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_action_modal.tsx index fa559e807f5ea5..2048d1144952d7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_action_modal.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_action_modal.tsx @@ -17,7 +17,7 @@ export const StartActionModal: FC = ({ closeModal, item, startAndCl = ({ closeModal, item, startAndCl

{i18n.translate('xpack.ml.dataframe.analyticsList.startModalBody', { defaultMessage: - 'A data frame analytics job will increase search and indexing load in your cluster. Please stop the analytics job if excessive load is experienced. Are you sure you want to start this analytics job?', + 'A data frame analytics job increases search and indexing load in your cluster. If excessive load occurs, stop the job.', })}

diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b70ab0bb255610..df78975d21b075 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11113,7 +11113,6 @@ "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexPatternSuccessMessage": "インデックスパターン{destinationIndex}を削除する要求が確認されました。", "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexSuccessMessage": "ディスティネーションインデックス{destinationIndex}を削除する要求が確認されました。", "xpack.ml.dataframe.analyticsList.deleteDestinationIndexTitle": "ディスティネーションインデックス{indexName}を削除", - "xpack.ml.dataframe.analyticsList.deleteModalBody": "この分析ジョブを削除してよろしいですか?", "xpack.ml.dataframe.analyticsList.deleteModalCancelButton": "キャンセル", "xpack.ml.dataframe.analyticsList.deleteModalDeleteButton": "削除", "xpack.ml.dataframe.analyticsList.deleteModalTitle": "{analyticsId}の削除", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 72010e00dc820f..cfee565a1da933 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11116,7 +11116,6 @@ "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexPatternSuccessMessage": "删除索引模式 {destinationIndex} 的请求已确认。", "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexSuccessMessage": "删除目标索引 {destinationIndex} 的请求已确认。", "xpack.ml.dataframe.analyticsList.deleteDestinationIndexTitle": "删除目标索引 {indexName}", - "xpack.ml.dataframe.analyticsList.deleteModalBody": "是否确定要删除此分析作业?", "xpack.ml.dataframe.analyticsList.deleteModalCancelButton": "取消", "xpack.ml.dataframe.analyticsList.deleteModalDeleteButton": "删除", "xpack.ml.dataframe.analyticsList.deleteModalTitle": "删除 {analyticsId}", From 981691d378cf6bce738d8d30f568f2f407468e29 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Tue, 1 Sep 2020 19:42:41 +0200 Subject: [PATCH 152/216] Add setHeaderActionMenu API to AppMountParameters (#75422) * add `setHeaderActionMenu` to AppMountParameters * allow to remove the current menu by calling handler with undefined * update generated doc * updating snapshots * fix legacy tests * call renderApp with params * rename toMountPoint component file for consistency * add the MountPointPortal utility component * adapt TopNavMenu to add optional `setMenuMountPoint` prop * add kibanaReact as required bundle. * use innerHTML instead of textContent for portal tests * add error boundaries to portal component * improve renderLayout readability * duplicate wrapper in portal mode to avoid altering styles Co-authored-by: Elastic Machine --- ...a-plugin-core-public.appmountparameters.md | 1 + ....appmountparameters.setheaderactionmenu.md | 39 ++++ .../application_service.test.ts.snap | 1 + .../application/application_service.mock.ts | 2 + .../application/application_service.tsx | 42 +++- .../application_service.test.tsx | 187 ++++++++++++++++ .../integration_tests/router.test.tsx | 1 + src/core/public/application/types.ts | 40 ++++ .../application/ui/app_container.test.tsx | 4 + .../public/application/ui/app_container.tsx | 17 +- src/core/public/application/ui/app_router.tsx | 7 +- .../header/__snapshots__/header.test.tsx.snap | 36 +++ src/core/public/mocks.ts | 1 + src/core/public/public.api.md | 1 + src/plugins/dev_tools/public/application.tsx | 1 + src/plugins/kibana_react/public/index.ts | 2 +- src/plugins/kibana_react/public/util/index.ts | 3 +- .../public/util/mount_point_portal.test.tsx | 210 ++++++++++++++++++ .../public/util/mount_point_portal.tsx | 88 ++++++++ .../{react_mount.tsx => to_mount_point.tsx} | 0 src/plugins/navigation/kibana.json | 5 +- .../public/top_nav_menu/top_nav_menu.test.tsx | 60 ++++- .../public/top_nav_menu/top_nav_menu.tsx | 53 ++++- x-pack/plugins/ml/public/plugin.ts | 7 +- .../account_management_app.test.ts | 1 + .../access_agreement_app.test.ts | 1 + .../capture_url/capture_url_app.test.ts | 1 + .../logged_out/logged_out_app.test.ts | 1 + .../authentication/login/login_app.test.ts | 1 + .../authentication/logout/logout_app.test.ts | 1 + .../overwritten_session_app.test.ts | 1 + 31 files changed, 785 insertions(+), 30 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.appmountparameters.setheaderactionmenu.md create mode 100644 src/plugins/kibana_react/public/util/mount_point_portal.test.tsx create mode 100644 src/plugins/kibana_react/public/util/mount_point_portal.tsx rename src/plugins/kibana_react/public/util/{react_mount.tsx => to_mount_point.tsx} (100%) diff --git a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.md b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.md index de79fc8281c45f..f6c57603beddec 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.md +++ b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.md @@ -19,4 +19,5 @@ export interface AppMountParameters | [element](./kibana-plugin-core-public.appmountparameters.element.md) | HTMLElement | The container element to render the application into. | | [history](./kibana-plugin-core-public.appmountparameters.history.md) | ScopedHistory<HistoryLocationState> | A scoped history instance for your application. Should be used to wire up your applications Router. | | [onAppLeave](./kibana-plugin-core-public.appmountparameters.onappleave.md) | (handler: AppLeaveHandler) => void | A function that can be used to register a handler that will be called when the user is leaving the current application, allowing to prompt a confirmation message before actually changing the page.This will be called either when the user goes to another application, or when trying to close the tab or manually changing the url. | +| [setHeaderActionMenu](./kibana-plugin-core-public.appmountparameters.setheaderactionmenu.md) | (menuMount: MountPoint | undefined) => void | A function that can be used to set the mount point used to populate the application action container in the chrome header.Calling the handler multiple time will erase the current content of the action menu with the mount from the latest call. Calling the handler with undefined will unmount the current mount point. Calling the handler after the application has been unmounted will have no effect. | diff --git a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.setheaderactionmenu.md b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.setheaderactionmenu.md new file mode 100644 index 00000000000000..ca9cee64bb1f92 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.setheaderactionmenu.md @@ -0,0 +1,39 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) > [setHeaderActionMenu](./kibana-plugin-core-public.appmountparameters.setheaderactionmenu.md) + +## AppMountParameters.setHeaderActionMenu property + +A function that can be used to set the mount point used to populate the application action container in the chrome header. + +Calling the handler multiple time will erase the current content of the action menu with the mount from the latest call. Calling the handler with `undefined` will unmount the current mount point. Calling the handler after the application has been unmounted will have no effect. + +Signature: + +```typescript +setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; +``` + +## Example + + +```ts +// application.tsx +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter, Route } from 'react-router-dom'; + +import { CoreStart, AppMountParameters } from 'src/core/public'; +import { MyPluginDepsStart } from './plugin'; + +export renderApp = ({ element, history, setHeaderActionMenu }: AppMountParameters) => { + const { renderApp } = await import('./application'); + const { renderActionMenu } = await import('./action_menu'); + setHeaderActionMenu((element) => { + return renderActionMenu(element); + }) + return renderApp({ element, history }); +} + +``` + diff --git a/src/core/public/application/__snapshots__/application_service.test.ts.snap b/src/core/public/application/__snapshots__/application_service.test.ts.snap index c63a22170c4f61..a6c9eb27e338a5 100644 --- a/src/core/public/application/__snapshots__/application_service.test.ts.snap +++ b/src/core/public/application/__snapshots__/application_service.test.ts.snap @@ -80,6 +80,7 @@ exports[`#start() getComponent returns renderable JSX tree 1`] = ` } } mounters={Map {}} + setAppActionMenu={[Function]} setAppLeaveHandler={[Function]} setIsMounting={[Function]} /> diff --git a/src/core/public/application/application_service.mock.ts b/src/core/public/application/application_service.mock.ts index 47a8a01d917eb0..2bdf56ee342114 100644 --- a/src/core/public/application/application_service.mock.ts +++ b/src/core/public/application/application_service.mock.ts @@ -20,6 +20,7 @@ import { History } from 'history'; import { BehaviorSubject, Subject } from 'rxjs'; +import type { MountPoint } from '../types'; import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; import { ApplicationSetup, @@ -87,6 +88,7 @@ const createInternalStartContractMock = (): jest.Mocked>(new Map()), capabilities: capabilitiesServiceMock.createStartContract().capabilities, currentAppId$: currentAppId$.asObservable(), + currentActionMenu$: new BehaviorSubject(undefined), getComponent: jest.fn(), getUrlForApp: jest.fn(), navigateToApp: jest.fn().mockImplementation((appId) => currentAppId$.next(appId)), diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index d7f15decb255dd..df0f74c1914e9a 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -22,6 +22,7 @@ import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; import { map, shareReplay, takeUntil, distinctUntilChanged, filter } from 'rxjs/operators'; import { createBrowserHistory, History } from 'history'; +import { MountPoint } from '../types'; import { InjectedMetadataSetup } from '../injected_metadata'; import { HttpSetup, HttpStart } from '../http'; import { OverlayStart } from '../overlays'; @@ -90,6 +91,11 @@ interface AppUpdaterWrapper { updater: AppUpdater; } +interface AppInternalState { + leaveHandler?: AppLeaveHandler; + actionMenu?: MountPoint; +} + /** * Service that is responsible for registering new applications. * @internal @@ -98,8 +104,9 @@ export class ApplicationService { private readonly apps = new Map | LegacyApp>(); private readonly mounters = new Map(); private readonly capabilities = new CapabilitiesService(); - private readonly appLeaveHandlers = new Map(); + private readonly appInternalStates = new Map(); private currentAppId$ = new BehaviorSubject(undefined); + private currentActionMenu$ = new BehaviorSubject(undefined); private readonly statusUpdaters$ = new BehaviorSubject>(new Map()); private readonly subscriptions: Subscription[] = []; private stop$ = new Subject(); @@ -293,12 +300,14 @@ export class ApplicationService { if (path === undefined) { path = applications$.value.get(appId)?.defaultPath; } - this.appLeaveHandlers.delete(this.currentAppId$.value!); + this.appInternalStates.delete(this.currentAppId$.value!); this.navigate!(getAppUrl(availableMounters, appId, path), state, replace); this.currentAppId$.next(appId); } }; + this.currentAppId$.subscribe(() => this.refreshCurrentActionMenu()); + return { applications$: applications$.pipe( map((apps) => new Map([...apps.entries()].map(([id, app]) => [id, getAppInfo(app)]))), @@ -310,6 +319,10 @@ export class ApplicationService { distinctUntilChanged(), takeUntil(this.stop$) ), + currentActionMenu$: this.currentActionMenu$.pipe( + distinctUntilChanged(), + takeUntil(this.stop$) + ), history: this.history, registerMountContext: this.mountContext.registerContext, getUrlForApp: ( @@ -338,6 +351,7 @@ export class ApplicationService { mounters={availableMounters} appStatuses$={applicationStatuses$} setAppLeaveHandler={this.setAppLeaveHandler} + setAppActionMenu={this.setAppActionMenu} setIsMounting={(isMounting) => httpLoadingCount$.next(isMounting ? 1 : 0)} /> ); @@ -346,7 +360,24 @@ export class ApplicationService { } private setAppLeaveHandler = (appId: string, handler: AppLeaveHandler) => { - this.appLeaveHandlers.set(appId, handler); + this.appInternalStates.set(appId, { + ...(this.appInternalStates.get(appId) ?? {}), + leaveHandler: handler, + }); + }; + + private setAppActionMenu = (appId: string, mount: MountPoint | undefined) => { + this.appInternalStates.set(appId, { + ...(this.appInternalStates.get(appId) ?? {}), + actionMenu: mount, + }); + this.refreshCurrentActionMenu(); + }; + + private refreshCurrentActionMenu = () => { + const appId = this.currentAppId$.getValue(); + const currentActionMenu = appId ? this.appInternalStates.get(appId)?.actionMenu : undefined; + this.currentActionMenu$.next(currentActionMenu); }; private async shouldNavigate(overlays: OverlayStart): Promise { @@ -354,7 +385,7 @@ export class ApplicationService { if (currentAppId === undefined) { return true; } - const action = getLeaveAction(this.appLeaveHandlers.get(currentAppId)); + const action = getLeaveAction(this.appInternalStates.get(currentAppId)?.leaveHandler); if (isConfirmAction(action)) { const confirmed = await overlays.openConfirm(action.text, { title: action.title, @@ -372,7 +403,7 @@ export class ApplicationService { if (currentAppId === undefined) { return; } - const action = getLeaveAction(this.appLeaveHandlers.get(currentAppId)); + const action = getLeaveAction(this.appInternalStates.get(currentAppId)?.leaveHandler); if (isConfirmAction(action)) { event.preventDefault(); // some browsers accept a string return value being the message displayed @@ -383,6 +414,7 @@ export class ApplicationService { public stop() { this.stop$.next(); this.currentAppId$.complete(); + this.currentActionMenu$.complete(); this.statusUpdaters$.complete(); this.subscriptions.forEach((sub) => sub.unsubscribe()); window.removeEventListener('beforeunload', this.onBeforeUnload); diff --git a/src/core/public/application/integration_tests/application_service.test.tsx b/src/core/public/application/integration_tests/application_service.test.tsx index b0419d276dfa18..9eafddd6a61fed 100644 --- a/src/core/public/application/integration_tests/application_service.test.tsx +++ b/src/core/public/application/integration_tests/application_service.test.tsx @@ -30,6 +30,8 @@ import { MockLifecycle } from '../test_types'; import { overlayServiceMock } from '../../overlays/overlay_service.mock'; import { AppMountParameters } from '../types'; import { ScopedHistory } from '../scoped_history'; +import { Observable } from 'rxjs'; +import { MountPoint } from 'kibana/public'; const flushPromises = () => new Promise((resolve) => setImmediate(resolve)); @@ -309,4 +311,189 @@ describe('ApplicationService', () => { expect(history.entries[1].pathname).toEqual('/app/app1'); }); }); + + describe('registering action menus', () => { + const getValue = (obs: Observable): Promise => { + return obs.pipe(take(1)).toPromise(); + }; + + const mounter1: MountPoint = () => () => undefined; + const mounter2: MountPoint = () => () => undefined; + + it('updates the observable value when an application is mounted', async () => { + const { register } = service.setup(setupDeps); + + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: async ({ setHeaderActionMenu }: AppMountParameters) => { + setHeaderActionMenu(mounter1); + return () => undefined; + }, + }); + + const { navigateToApp, getComponent, currentActionMenu$ } = await service.start(startDeps); + update = createRenderer(getComponent()); + + expect(await getValue(currentActionMenu$)).toBeUndefined(); + + await act(async () => { + await navigateToApp('app1'); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBe(mounter1); + }); + + it('updates the observable value when switching application', async () => { + const { register } = service.setup(setupDeps); + + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: async ({ setHeaderActionMenu }: AppMountParameters) => { + setHeaderActionMenu(mounter1); + return () => undefined; + }, + }); + register(Symbol(), { + id: 'app2', + title: 'App2', + mount: async ({ setHeaderActionMenu }: AppMountParameters) => { + setHeaderActionMenu(mounter2); + return () => undefined; + }, + }); + + const { navigateToApp, getComponent, currentActionMenu$ } = await service.start(startDeps); + update = createRenderer(getComponent()); + + await act(async () => { + await navigateToApp('app1'); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBe(mounter1); + + await act(async () => { + await navigateToApp('app2'); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBe(mounter2); + }); + + it('updates the observable value to undefined when switching to an application without action menu', async () => { + const { register } = service.setup(setupDeps); + + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: async ({ setHeaderActionMenu }: AppMountParameters) => { + setHeaderActionMenu(mounter1); + return () => undefined; + }, + }); + register(Symbol(), { + id: 'app2', + title: 'App2', + mount: async ({}: AppMountParameters) => { + return () => undefined; + }, + }); + + const { navigateToApp, getComponent, currentActionMenu$ } = await service.start(startDeps); + update = createRenderer(getComponent()); + + await act(async () => { + await navigateToApp('app1'); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBe(mounter1); + + await act(async () => { + await navigateToApp('app2'); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBeUndefined(); + }); + + it('allow applications to call `setHeaderActionMenu` multiple times', async () => { + const { register } = service.setup(setupDeps); + + let resolveMount: () => void; + const promise = new Promise((resolve) => { + resolveMount = resolve; + }); + + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: async ({ setHeaderActionMenu }: AppMountParameters) => { + setHeaderActionMenu(mounter1); + promise.then(() => { + setHeaderActionMenu(mounter2); + }); + return () => undefined; + }, + }); + + const { navigateToApp, getComponent, currentActionMenu$ } = await service.start(startDeps); + update = createRenderer(getComponent()); + + await act(async () => { + await navigateToApp('app1'); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBe(mounter1); + + await act(async () => { + resolveMount(); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBe(mounter2); + }); + + it('allow applications to unset the current menu', async () => { + const { register } = service.setup(setupDeps); + + let resolveMount: () => void; + const promise = new Promise((resolve) => { + resolveMount = resolve; + }); + + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: async ({ setHeaderActionMenu }: AppMountParameters) => { + setHeaderActionMenu(mounter1); + promise.then(() => { + setHeaderActionMenu(undefined); + }); + return () => undefined; + }, + }); + + const { navigateToApp, getComponent, currentActionMenu$ } = await service.start(startDeps); + update = createRenderer(getComponent()); + + await act(async () => { + await navigateToApp('app1'); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBe(mounter1); + + await act(async () => { + resolveMount(); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBeUndefined(); + }); + }); }); diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx index f992e121437a96..6408b8123365e4 100644 --- a/src/core/public/application/integration_tests/router.test.tsx +++ b/src/core/public/application/integration_tests/router.test.tsx @@ -59,6 +59,7 @@ describe('AppRouter', () => { mounters={mockMountersToMounters()} appStatuses$={mountersToAppStatus$()} setAppLeaveHandler={noop} + setAppActionMenu={noop} setIsMounting={noop} /> ); diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 0fe97431b15690..320416a8c2379a 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -21,6 +21,7 @@ import { Observable } from 'rxjs'; import { History } from 'history'; import { RecursiveReadonly } from '@kbn/utility-types'; +import { MountPoint } from '../types'; import { Capabilities } from './capabilities'; import { ChromeStart } from '../chrome'; import { IContextProvider } from '../context'; @@ -495,6 +496,37 @@ export interface AppMountParameters { * ``` */ onAppLeave: (handler: AppLeaveHandler) => void; + + /** + * A function that can be used to set the mount point used to populate the application action container + * in the chrome header. + * + * Calling the handler multiple time will erase the current content of the action menu with the mount from the latest call. + * Calling the handler with `undefined` will unmount the current mount point. + * Calling the handler after the application has been unmounted will have no effect. + * + * @example + * + * ```ts + * // application.tsx + * import React from 'react'; + * import ReactDOM from 'react-dom'; + * import { BrowserRouter, Route } from 'react-router-dom'; + * + * import { CoreStart, AppMountParameters } from 'src/core/public'; + * import { MyPluginDepsStart } from './plugin'; + * + * export renderApp = ({ element, history, setHeaderActionMenu }: AppMountParameters) => { + * const { renderApp } = await import('./application'); + * const { renderActionMenu } = await import('./action_menu'); + * setHeaderActionMenu((element) => { + * return renderActionMenu(element); + * }) + * return renderApp({ element, history }); + * } + * ``` + */ + setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; } /** @@ -820,6 +852,14 @@ export interface InternalApplicationStart extends Omit; + /** * The global history instance, exposed only to Core. Undefined when rendering a legacy application. * @internal diff --git a/src/core/public/application/ui/app_container.test.tsx b/src/core/public/application/ui/app_container.test.tsx index a94313dd53abbc..e26fe7e59fd040 100644 --- a/src/core/public/application/ui/app_container.test.tsx +++ b/src/core/public/application/ui/app_container.test.tsx @@ -29,6 +29,7 @@ import { ScopedHistory } from '../scoped_history'; describe('AppContainer', () => { const appId = 'someApp'; const setAppLeaveHandler = jest.fn(); + const setAppActionMenu = jest.fn(); const setIsMounting = jest.fn(); beforeEach(() => { @@ -76,6 +77,7 @@ describe('AppContainer', () => { appStatus={AppStatus.inaccessible} mounter={mounter} setAppLeaveHandler={setAppLeaveHandler} + setAppActionMenu={setAppActionMenu} setIsMounting={setIsMounting} createScopedHistory={(appPath: string) => // Create a history using the appPath as the current location @@ -116,6 +118,7 @@ describe('AppContainer', () => { appStatus={AppStatus.accessible} mounter={mounter} setAppLeaveHandler={setAppLeaveHandler} + setAppActionMenu={setAppActionMenu} setIsMounting={setIsMounting} createScopedHistory={(appPath: string) => // Create a history using the appPath as the current location @@ -158,6 +161,7 @@ describe('AppContainer', () => { appStatus={AppStatus.accessible} mounter={mounter} setAppLeaveHandler={setAppLeaveHandler} + setAppActionMenu={setAppActionMenu} setIsMounting={setIsMounting} createScopedHistory={(appPath: string) => // Create a history using the appPath as the current location diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx index 332c31c64b6ba1..f668cf851da55f 100644 --- a/src/core/public/application/ui/app_container.tsx +++ b/src/core/public/application/ui/app_container.tsx @@ -25,8 +25,9 @@ import React, { useState, MutableRefObject, } from 'react'; - import { EuiLoadingSpinner } from '@elastic/eui'; + +import type { MountPoint } from '../../types'; import { AppLeaveHandler, AppStatus, AppUnmount, Mounter } from '../types'; import { AppNotFound } from './app_not_found_screen'; import { ScopedHistory } from '../scoped_history'; @@ -39,6 +40,7 @@ interface Props { mounter?: Mounter; appStatus: AppStatus; setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void; + setAppActionMenu: (appId: string, mount: MountPoint | undefined) => void; createScopedHistory: (appUrl: string) => ScopedHistory; setIsMounting: (isMounting: boolean) => void; } @@ -48,6 +50,7 @@ export const AppContainer: FunctionComponent = ({ appId, appPath, setAppLeaveHandler, + setAppActionMenu, createScopedHistory, appStatus, setIsMounting, @@ -84,6 +87,7 @@ export const AppContainer: FunctionComponent = ({ history: createScopedHistory(appPath), element: elementRef.current!, onAppLeave: (handler) => setAppLeaveHandler(appId, handler), + setHeaderActionMenu: (menuMount) => setAppActionMenu(appId, menuMount), })) || null; } catch (e) { // TODO: add error UI @@ -98,7 +102,16 @@ export const AppContainer: FunctionComponent = ({ mount(); return unmount; - }, [appId, appStatus, mounter, createScopedHistory, setAppLeaveHandler, appPath, setIsMounting]); + }, [ + appId, + appStatus, + mounter, + createScopedHistory, + setAppLeaveHandler, + setAppActionMenu, + appPath, + setIsMounting, + ]); return ( diff --git a/src/core/public/application/ui/app_router.tsx b/src/core/public/application/ui/app_router.tsx index f1f22237c32db0..5021dd3ae765ab 100644 --- a/src/core/public/application/ui/app_router.tsx +++ b/src/core/public/application/ui/app_router.tsx @@ -23,6 +23,7 @@ import { History } from 'history'; import { Observable } from 'rxjs'; import useObservable from 'react-use/lib/useObservable'; +import type { MountPoint } from '../../types'; import { AppLeaveHandler, AppStatus, Mounter } from '../types'; import { AppContainer } from './app_container'; import { ScopedHistory } from '../scoped_history'; @@ -32,6 +33,7 @@ interface Props { history: History; appStatuses$: Observable>; setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void; + setAppActionMenu: (appId: string, mount: MountPoint | undefined) => void; setIsMounting: (isMounting: boolean) => void; } @@ -43,6 +45,7 @@ export const AppRouter: FunctionComponent = ({ history, mounters, setAppLeaveHandler, + setAppActionMenu, appStatuses$, setIsMounting, }) => { @@ -69,7 +72,7 @@ export const AppRouter: FunctionComponent = ({ appPath={path} appStatus={appStatuses.get(appId) ?? AppStatus.inaccessible} createScopedHistory={createScopedHistory} - {...{ appId, mounter, setAppLeaveHandler, setIsMounting }} + {...{ appId, mounter, setAppLeaveHandler, setAppActionMenu, setIsMounting }} /> )} /> @@ -94,7 +97,7 @@ export const AppRouter: FunctionComponent = ({ appId={id} appStatus={appStatuses.get(id) ?? AppStatus.inaccessible} createScopedHistory={createScopedHistory} - {...{ mounter, setAppLeaveHandler, setIsMounting }} + {...{ mounter, setAppLeaveHandler, setAppActionMenu, setIsMounting }} /> ); }} diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 3aabd2a1127dcd..5ec7a4773967b2 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -29,6 +29,15 @@ exports[`Header renders 1`] = ` "management": Object {}, "navLinks": Object {}, }, + "currentActionMenu$": BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, "currentAppId$": Observable { "_isScalar": false, "source": Subject { @@ -641,6 +650,15 @@ exports[`Header renders 2`] = ` "management": Object {}, "navLinks": Object {}, }, + "currentActionMenu$": BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, "currentAppId$": Observable { "_isScalar": false, "source": Subject { @@ -4854,6 +4872,15 @@ exports[`Header renders 3`] = ` "management": Object {}, "navLinks": Object {}, }, + "currentActionMenu$": BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, "currentAppId$": Observable { "_isScalar": false, "source": Subject { @@ -9708,6 +9735,15 @@ exports[`Header renders 4`] = ` "management": Object {}, "navLinks": Object {}, }, + "currentActionMenu$": BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, "currentAppId$": Observable { "_isScalar": false, "source": Subject { diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 2f7f6fae944369..aefcb830d40bf0 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -166,6 +166,7 @@ function createAppMountParametersMock(appBasePath = '') { element: document.createElement('div'), history, onAppLeave: jest.fn(), + setHeaderActionMenu: jest.fn(), }; return params; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 6f25f46c76fb91..570732fa6e5d66 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -165,6 +165,7 @@ export interface AppMountParameters { element: HTMLElement; history: ScopedHistory; onAppLeave: (handler: AppLeaveHandler) => void; + setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; } // @public diff --git a/src/plugins/dev_tools/public/application.tsx b/src/plugins/dev_tools/public/application.tsx index 46f09a8ebb8796..d3a54627b02407 100644 --- a/src/plugins/dev_tools/public/application.tsx +++ b/src/plugins/dev_tools/public/application.tsx @@ -90,6 +90,7 @@ function DevToolsWrapper({ devTools, activeDevTool, updateRoute }: DevToolsWrapp element, appBasePath: '', onAppLeave: () => undefined, + setHeaderActionMenu: () => undefined, // TODO: adapt to use Core's ScopedHistory history: {} as any, }; diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts index 34140703fd8aee..9a9486da892e44 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -32,7 +32,7 @@ export * from './notifications'; export { Markdown, MarkdownSimple } from './markdown'; export { reactToUiComponent, uiToReactComponent } from './adapters'; export { useUrlTracker } from './use_url_tracker'; -export { toMountPoint } from './util'; +export { toMountPoint, MountPointPortal } from './util'; export { RedirectAppLinks } from './app_links'; /** dummy plugin, we just want kibanaReact to have its own bundle */ diff --git a/src/plugins/kibana_react/public/util/index.ts b/src/plugins/kibana_react/public/util/index.ts index 71a281dbdaad3c..a6f3f87535f468 100644 --- a/src/plugins/kibana_react/public/util/index.ts +++ b/src/plugins/kibana_react/public/util/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export * from './react_mount'; +export { toMountPoint } from './to_mount_point'; +export { MountPointPortal } from './mount_point_portal'; diff --git a/src/plugins/kibana_react/public/util/mount_point_portal.test.tsx b/src/plugins/kibana_react/public/util/mount_point_portal.test.tsx new file mode 100644 index 00000000000000..c13b8eae262213 --- /dev/null +++ b/src/plugins/kibana_react/public/util/mount_point_portal.test.tsx @@ -0,0 +1,210 @@ +/* + * 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 React, { FC } from 'react'; +import { mount, ReactWrapper } from 'enzyme'; +import { MountPoint, UnmountCallback } from 'kibana/public'; +import { MountPointPortal } from './mount_point_portal'; +import { act } from 'react-dom/test-utils'; + +describe('MountPointPortal', () => { + let portalTarget: HTMLElement; + let mountPoint: MountPoint; + let setMountPoint: jest.Mock<(mountPoint: MountPoint) => void>; + let dom: ReactWrapper; + + const refresh = () => { + new Promise(async (resolve) => { + if (dom) { + act(() => { + dom.update(); + }); + } + setImmediate(() => resolve(dom)); // flushes any pending promises + }); + }; + + beforeEach(() => { + portalTarget = document.createElement('div'); + document.body.append(portalTarget); + setMountPoint = jest.fn().mockImplementation((mp) => (mountPoint = mp)); + }); + + afterEach(() => { + if (portalTarget) { + portalTarget.remove(); + } + }); + + it('calls the provided `setMountPoint` during render', async () => { + dom = mount( + + portal content + + ); + + await refresh(); + + expect(setMountPoint).toHaveBeenCalledTimes(1); + }); + + it('renders the portal content when calling the mountPoint ', async () => { + dom = mount( + + portal content + + ); + + await refresh(); + + expect(mountPoint).toBeDefined(); + + act(() => { + mountPoint(portalTarget); + }); + + await refresh(); + + expect(portalTarget.innerHTML).toBe('portal content'); + }); + + it('cleanup the portal content when the component is unmounted', async () => { + dom = mount( + + portal content + + ); + + act(() => { + mountPoint(portalTarget); + }); + + await refresh(); + + expect(portalTarget.innerHTML).toBe('portal content'); + + dom.unmount(); + + await refresh(); + + expect(portalTarget.innerHTML).toBe(''); + }); + + it('cleanup the portal content when unmounting the MountPoint from outside', async () => { + dom = mount( + + portal content + + ); + + let unmount: UnmountCallback; + act(() => { + unmount = mountPoint(portalTarget); + }); + + await refresh(); + + expect(portalTarget.innerHTML).toBe('portal content'); + + act(() => { + unmount(); + }); + + await refresh(); + + expect(portalTarget.innerHTML).toBe(''); + }); + + it('updates the content of the portal element when the content of MountPointPortal changes', async () => { + const Wrapper: FC<{ + setMount: (mountPoint: MountPoint) => void; + portalContent: string; + }> = ({ setMount, portalContent }) => { + return ( + +
{portalContent}
+
+ ); + }; + + dom = mount(); + + act(() => { + mountPoint(portalTarget); + }); + + await refresh(); + + expect(portalTarget.innerHTML).toBe('
before
'); + + dom.setProps({ + portalContent: 'after', + }); + + await refresh(); + + expect(portalTarget.innerHTML).toBe('
after
'); + }); + + it('cleanup the previous portal content when setMountPoint changes', async () => { + dom = mount( + + portal content + + ); + + act(() => { + mountPoint(portalTarget); + }); + + await refresh(); + + expect(portalTarget.innerHTML).toBe('portal content'); + + const newSetMountPoint = jest.fn(); + + dom.setProps({ + setMountPoint: newSetMountPoint, + }); + + await refresh(); + + expect(portalTarget.innerHTML).toBe(''); + }); + + it('intercepts errors and display an error message', async () => { + const CrashTest = () => { + throw new Error('crash'); + }; + + dom = mount( + + + + ); + + act(() => { + mountPoint(portalTarget); + }); + + await refresh(); + + expect(portalTarget.innerHTML).toBe('

Error rendering portal content

'); + }); +}); diff --git a/src/plugins/kibana_react/public/util/mount_point_portal.tsx b/src/plugins/kibana_react/public/util/mount_point_portal.tsx new file mode 100644 index 00000000000000..b762fba88791e8 --- /dev/null +++ b/src/plugins/kibana_react/public/util/mount_point_portal.tsx @@ -0,0 +1,88 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { useRef, useEffect, useState, Component } from 'react'; +import ReactDOM from 'react-dom'; +import { MountPoint } from 'kibana/public'; + +interface MountPointPortalProps { + setMountPoint: (mountPoint: MountPoint) => void; +} + +/** + * Utility component to portal a part of a react application into the provided `MountPoint`. + */ +export const MountPointPortal: React.FC = ({ children, setMountPoint }) => { + // state used to force re-renders when the element changes + const [shouldRender, setShouldRender] = useState(false); + const el = useRef(); + + useEffect(() => { + setMountPoint((element) => { + el.current = element; + setShouldRender(true); + return () => { + setShouldRender(false); + el.current = undefined; + }; + }); + + return () => { + setShouldRender(false); + el.current = undefined; + }; + }, [setMountPoint]); + + if (shouldRender && el.current) { + return ReactDOM.createPortal( + {children}, + el.current + ); + } else { + return null; + } +}; + +class MountPointPortalErrorBoundary extends Component<{}, { error?: any }> { + state = { + error: undefined, + }; + + static getDerivedStateFromError(error: any) { + return { error }; + } + + componentDidCatch() { + // nothing, will just rerender to display the error message + } + + render() { + if (this.state.error) { + return ( +

+ {i18n.translate('kibana-react.mountPointPortal.errorMessage', { + defaultMessage: 'Error rendering portal content', + })} +

+ ); + } + return this.props.children; + } +} diff --git a/src/plugins/kibana_react/public/util/react_mount.tsx b/src/plugins/kibana_react/public/util/to_mount_point.tsx similarity index 100% rename from src/plugins/kibana_react/public/util/react_mount.tsx rename to src/plugins/kibana_react/public/util/to_mount_point.tsx diff --git a/src/plugins/navigation/kibana.json b/src/plugins/navigation/kibana.json index 000d5acf2635f7..85d2049a34be0b 100644 --- a/src/plugins/navigation/kibana.json +++ b/src/plugins/navigation/kibana.json @@ -3,5 +3,6 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["data"] -} \ No newline at end of file + "requiredPlugins": ["data"], + "requiredBundles": ["kibanaReact"] +} diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx index 46384fb3f27d5b..f21e5680e8f611 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx @@ -18,9 +18,12 @@ */ import React from 'react'; +import { ReactWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { MountPoint } from 'kibana/public'; import { TopNavMenu } from './top_nav_menu'; import { TopNavMenuData } from './top_nav_menu_data'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; const dataShim = { ui: { @@ -109,4 +112,59 @@ describe('TopNavMenu', () => { expect(component.find('.kbnTopNavMenu').length).toBe(1); expect(component.find('.myCoolClass').length).toBeTruthy(); }); + + describe('when setMenuMountPoint is provided', () => { + let portalTarget: HTMLElement; + let mountPoint: MountPoint; + let setMountPoint: jest.Mock<(mountPoint: MountPoint) => void>; + let dom: ReactWrapper; + + const refresh = () => { + new Promise(async (resolve) => { + if (dom) { + act(() => { + dom.update(); + }); + } + setImmediate(() => resolve(dom)); // flushes any pending promises + }); + }; + + beforeEach(() => { + portalTarget = document.createElement('div'); + document.body.append(portalTarget); + setMountPoint = jest.fn().mockImplementation((mp) => (mountPoint = mp)); + }); + + afterEach(() => { + if (portalTarget) { + portalTarget.remove(); + } + }); + + it('mounts the menu inside the provided mountPoint', async () => { + const component = mountWithIntl( + + ); + + act(() => { + mountPoint(portalTarget); + }); + + await refresh(); + + expect(component.find(WRAPPER_SELECTOR).length).toBe(1); + expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(1); + + // menu is rendered outside of the component + expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0); + expect(portalTarget.getElementsByTagName('BUTTON').length).toBe(menuItems.length); + }); + }); }); diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index 2cfca332effb0d..a1a40b49cc8f06 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -18,13 +18,14 @@ */ import React, { ReactElement } from 'react'; - import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; - import classNames from 'classnames'; + +import { MountPoint } from '../../../../core/public'; +import { MountPointPortal } from '../../../kibana_react/public'; +import { StatefulSearchBarProps, DataPublicPluginStart } from '../../../data/public'; import { TopNavMenuData } from './top_nav_menu_data'; import { TopNavMenuItem } from './top_nav_menu_item'; -import { StatefulSearchBarProps, DataPublicPluginStart } from '../../../data/public'; export type TopNavMenuProps = StatefulSearchBarProps & { config?: TopNavMenuData[]; @@ -35,6 +36,25 @@ export type TopNavMenuProps = StatefulSearchBarProps & { showFilterBar?: boolean; data?: DataPublicPluginStart; className?: string; + /** + * If provided, the menu part of the component will be rendered as a portal inside the given mount point. + * + * This is meant to be used with the `setHeaderActionMenu` core API. + * + * @example + * ```ts + * export renderApp = ({ element, history, setHeaderActionMenu }: AppMountParameters) => { + * const topNavConfig = ...; // TopNavMenuProps + * return ( + * + * + * + * + * ) + * } + * ``` + */ + setMenuMountPoint?: (menuMount: MountPoint | undefined) => void; }; /* @@ -92,13 +112,26 @@ export function TopNavMenu(props: TopNavMenuProps): ReactElement | null { } function renderLayout() { - const className = classNames('kbnTopNavMenu', props.className); - return ( - - {renderMenu(className)} - {renderSearchBar()} - - ); + const { setMenuMountPoint } = props; + const menuClassName = classNames('kbnTopNavMenu', props.className); + const wrapperClassName = 'kbnTopNavMenu__wrapper'; + if (setMenuMountPoint) { + return ( + <> + + {renderMenu(menuClassName)} + + {renderSearchBar()} + + ); + } else { + return ( + + {renderMenu(menuClassName)} + {renderSearchBar()} + + ); + } } return renderLayout(); diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 4f8ceb8effe982..214b393a0fda92 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -96,12 +96,7 @@ export class MlPlugin implements Plugin { uiActions: pluginsStart.uiActions, kibanaVersion, }, - { - element: params.element, - appBasePath: params.appBasePath, - onAppLeave: params.onAppLeave, - history: params.history, - } + params ); }, }); diff --git a/x-pack/plugins/security/public/account_management/account_management_app.test.ts b/x-pack/plugins/security/public/account_management/account_management_app.test.ts index 37b97a84723104..c41bd43872beed 100644 --- a/x-pack/plugins/security/public/account_management/account_management_app.test.ts +++ b/x-pack/plugins/security/public/account_management/account_management_app.test.ts @@ -54,6 +54,7 @@ describe('accountManagementApp', () => { element: containerMock, appBasePath: '', onAppLeave: jest.fn(), + setHeaderActionMenu: jest.fn(), history: scopedHistoryMock.create(), }); diff --git a/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.test.ts b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.test.ts index 0e262e9089842b..eafad74d2f0d83 100644 --- a/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.test.ts +++ b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.test.ts @@ -48,6 +48,7 @@ describe('accessAgreementApp', () => { element: containerMock, appBasePath: '', onAppLeave: jest.fn(), + setHeaderActionMenu: jest.fn(), history: scopedHistoryMock.create(), }); diff --git a/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts b/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts index c5b9245414630a..e6723085460f89 100644 --- a/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts +++ b/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts @@ -54,6 +54,7 @@ describe('captureURLApp', () => { element: document.createElement('div'), appBasePath: '', onAppLeave: jest.fn(), + setHeaderActionMenu: jest.fn(), history: (scopedHistoryMock.create() as unknown) as ScopedHistory, }); diff --git a/x-pack/plugins/security/public/authentication/logged_out/logged_out_app.test.ts b/x-pack/plugins/security/public/authentication/logged_out/logged_out_app.test.ts index 15d55136b405dc..86a5d21f1b233b 100644 --- a/x-pack/plugins/security/public/authentication/logged_out/logged_out_app.test.ts +++ b/x-pack/plugins/security/public/authentication/logged_out/logged_out_app.test.ts @@ -46,6 +46,7 @@ describe('loggedOutApp', () => { element: containerMock, appBasePath: '', onAppLeave: jest.fn(), + setHeaderActionMenu: jest.fn(), history: scopedHistoryMock.create(), }); diff --git a/x-pack/plugins/security/public/authentication/login/login_app.test.ts b/x-pack/plugins/security/public/authentication/login/login_app.test.ts index a6e5a321ef6ec2..5ae8afab9de23b 100644 --- a/x-pack/plugins/security/public/authentication/login/login_app.test.ts +++ b/x-pack/plugins/security/public/authentication/login/login_app.test.ts @@ -51,6 +51,7 @@ describe('loginApp', () => { element: containerMock, appBasePath: '', onAppLeave: jest.fn(), + setHeaderActionMenu: jest.fn(), history: scopedHistoryMock.create(), }); diff --git a/x-pack/plugins/security/public/authentication/logout/logout_app.test.ts b/x-pack/plugins/security/public/authentication/logout/logout_app.test.ts index 46b1083a2ed14a..b7bfdf492305e5 100644 --- a/x-pack/plugins/security/public/authentication/logout/logout_app.test.ts +++ b/x-pack/plugins/security/public/authentication/logout/logout_app.test.ts @@ -52,6 +52,7 @@ describe('logoutApp', () => { element: containerMock, appBasePath: '', onAppLeave: jest.fn(), + setHeaderActionMenu: jest.fn(), history: scopedHistoryMock.create(), }); diff --git a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.test.ts b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.test.ts index 0eed1382c270b2..6e0e06dd3dc44f 100644 --- a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.test.ts +++ b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.test.ts @@ -53,6 +53,7 @@ describe('overwrittenSessionApp', () => { element: containerMock, appBasePath: '', onAppLeave: jest.fn(), + setHeaderActionMenu: jest.fn(), history: scopedHistoryMock.create(), }); From d7869dee6e386a5576343ef2d5f2c46abf449a02 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Tue, 1 Sep 2020 14:17:52 -0400 Subject: [PATCH 153/216] [Maps] Add mvt support for ES doc sources (#75698) --- x-pack/package.json | 2 + x-pack/plugins/maps/common/constants.ts | 7 + .../data_request_descriptor_types.ts | 1 - .../maps/common/elasticsearch_geo_utils.d.ts | 10 + .../public/classes/fields/es_doc_field.ts | 8 + .../tiled_vector_layer.test.tsx | 63 ++--- .../tiled_vector_layer/tiled_vector_layer.tsx | 24 +- .../layers/vector_layer/vector_layer.js | 49 ++++ .../es_geo_grid_source.test.ts | 18 +- .../__snapshots__/scaling_form.test.tsx.snap | 152 +++++++++++- .../update_source_editor.test.js.snap | 6 + .../es_search_source/create_source_editor.js | 5 + .../es_documents_layer_wizard.tsx | 11 +- .../es_search_source/es_search_source.d.ts | 15 +- .../es_search_source/es_search_source.js | 81 ++++++- .../es_search_source/es_search_source.test.ts | 155 ++++++++++++ .../es_search_source/scaling_form.test.tsx | 52 ++-- .../sources/es_search_source/scaling_form.tsx | 72 +++++- .../es_search_source/update_source_editor.js | 10 + .../classes/sources/es_source/es_source.js | 2 +- .../sources/vector_source/vector_source.d.ts | 7 +- .../categorical_field_meta_popover.tsx | 2 + .../field_meta/ordinal_field_meta_popover.tsx | 2 + .../dynamic_color_property.test.tsx.snap | 26 ++ .../dynamic_color_property.test.tsx | 36 +++ .../properties/dynamic_style_property.tsx | 4 + .../classes/util/mb_filter_expressions.ts | 40 +++- .../map/mb/tooltip_control/tooltip_control.js | 8 +- .../connected_components/map/mb/view.js | 14 +- .../plugins/maps/public/index_pattern_util.ts | 10 + .../mvt/__tests__/json/0_0_0_search.json | 1 + .../maps/server/mvt/__tests__/pbf/0_0_0.pbf | Bin 0 -> 155 bytes .../server/mvt/__tests__/tile_searches.ts | 28 +++ .../plugins/maps/server/mvt/get_tile.test.ts | 63 +++++ x-pack/plugins/maps/server/mvt/get_tile.ts | 226 ++++++++++++++++++ x-pack/plugins/maps/server/mvt/mvt_routes.ts | 73 ++++++ x-pack/plugins/maps/server/mvt/util.ts | 72 ++++++ x-pack/plugins/maps/server/routes.js | 28 ++- .../api_integration/apis/maps/get_tile.js | 29 +++ .../test/api_integration/apis/maps/index.js | 1 + x-pack/test/functional/apps/maps/index.js | 1 + x-pack/test/functional/apps/maps/joins.js | 28 ++- .../functional/apps/maps/mapbox_styles.js | 36 +-- .../test/functional/apps/maps/mvt_scaling.js | 75 ++++++ .../es_archives/maps/kibana/data.json | 34 +++ 45 files changed, 1444 insertions(+), 143 deletions(-) create mode 100644 x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts create mode 100644 x-pack/plugins/maps/server/mvt/__tests__/json/0_0_0_search.json create mode 100644 x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0.pbf create mode 100644 x-pack/plugins/maps/server/mvt/__tests__/tile_searches.ts create mode 100644 x-pack/plugins/maps/server/mvt/get_tile.test.ts create mode 100644 x-pack/plugins/maps/server/mvt/get_tile.ts create mode 100644 x-pack/plugins/maps/server/mvt/mvt_routes.ts create mode 100644 x-pack/plugins/maps/server/mvt/util.ts create mode 100644 x-pack/test/api_integration/apis/maps/get_tile.js create mode 100644 x-pack/test/functional/apps/maps/mvt_scaling.js diff --git a/x-pack/package.json b/x-pack/package.json index f25fe7e3418ae6..0e9aef37aa2532 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -313,6 +313,7 @@ "file-type": "^10.9.0", "font-awesome": "4.7.0", "fp-ts": "^2.3.1", + "geojson-vt": "^3.2.1", "get-port": "^5.0.0", "getos": "^3.1.0", "git-url-parse": "11.1.2", @@ -384,6 +385,7 @@ "ui-select": "0.19.8", "uuid": "3.3.2", "vscode-languageserver": "^5.2.1", + "vt-pbf": "^3.1.1", "webpack": "^4.41.5", "wellknown": "^0.5.0", "xml2js": "^0.4.22", diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 363122ac62212a..a4f20caedfc9bd 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -33,6 +33,12 @@ export const MAP_PATH = 'map'; export const GIS_API_PATH = `api/${APP_ID}`; export const INDEX_SETTINGS_API_PATH = `${GIS_API_PATH}/indexSettings`; export const FONTS_API_PATH = `${GIS_API_PATH}/fonts`; +export const API_ROOT_PATH = `/${GIS_API_PATH}`; + +export const MVT_GETTILE_API_PATH = 'mvt/getTile'; +export const MVT_SOURCE_LAYER_NAME = 'source_layer'; +export const KBN_TOO_MANY_FEATURES_PROPERTY = '__kbn_too_many_features__'; +export const KBN_TOO_MANY_FEATURES_IMAGE_ID = '__kbn_too_many_features_image_id__'; const MAP_BASE_URL = `/${MAPS_APP_PATH}/${MAP_PATH}`; export function getNewMapPath() { @@ -220,6 +226,7 @@ export enum SCALING_TYPES { LIMIT = 'LIMIT', CLUSTERS = 'CLUSTERS', TOP_HITS = 'TOP_HITS', + MVT = 'MVT', } export const RGBA_0000 = 'rgba(0,0,0,0)'; diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts index cd7d2d5d0f461f..f3521cca2e456f 100644 --- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts @@ -18,7 +18,6 @@ export type MapFilters = { refreshTimerLastTriggeredAt?: string; timeFilters: TimeRange; zoom: number; - geogridPrecision?: number; }; type ESSearchSourceSyncMeta = { diff --git a/x-pack/plugins/maps/common/elasticsearch_geo_utils.d.ts b/x-pack/plugins/maps/common/elasticsearch_geo_utils.d.ts index 44250360e9d00a..e57efca94d95e7 100644 --- a/x-pack/plugins/maps/common/elasticsearch_geo_utils.d.ts +++ b/x-pack/plugins/maps/common/elasticsearch_geo_utils.d.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { FeatureCollection, GeoJsonProperties } from 'geojson'; import { MapExtent } from './descriptor_types'; +import { ES_GEO_FIELD_TYPE } from './constants'; export function scaleBounds(bounds: MapExtent, scaleFactor: number): MapExtent; @@ -13,3 +15,11 @@ export function turfBboxToBounds(turfBbox: unknown): MapExtent; export function clampToLatBounds(lat: number): number; export function clampToLonBounds(lon: number): number; + +export function hitsToGeoJson( + hits: Array>, + flattenHit: (elasticSearchHit: Record) => GeoJsonProperties, + geoFieldName: string, + geoFieldType: ES_GEO_FIELD_TYPE, + epochMillisFields: string[] +): FeatureCollection; diff --git a/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts b/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts index 9faa33fae5a431..543dbf6d87039c 100644 --- a/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts @@ -15,18 +15,22 @@ import { IVectorSource } from '../sources/vector_source'; export class ESDocField extends AbstractField implements IField { private readonly _source: IESSource; + private readonly _canReadFromGeoJson: boolean; constructor({ fieldName, source, origin, + canReadFromGeoJson = true, }: { fieldName: string; source: IESSource; origin: FIELD_ORIGIN; + canReadFromGeoJson?: boolean; }) { super({ fieldName, origin }); this._source = source; + this._canReadFromGeoJson = canReadFromGeoJson; } canValueBeFormatted(): boolean { @@ -60,6 +64,10 @@ export class ESDocField extends AbstractField implements IField { return true; } + canReadFromGeoJson(): boolean { + return this._canReadFromGeoJson; + } + async getOrdinalFieldMetaRequest(): Promise { const indexPatternField = await this._getIndexPatternField(); diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx index faae26cac08e71..822b78aa0deffa 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx @@ -128,32 +128,41 @@ describe('syncData', () => { sinon.assert.notCalled(syncContext2.stopLoading); }); - it('Should resync when changes to source params', async () => { - const layer1: TiledVectorLayer = createLayer({}, {}); - const syncContext1 = new MockSyncContext({ dataFilters: {} }); - - await layer1.syncData(syncContext1); - - const dataRequestDescriptor: DataRequestDescriptor = { - data: defaultConfig, - dataId: 'source', - }; - const layer2: TiledVectorLayer = createLayer( - { - __dataRequests: [dataRequestDescriptor], - }, - { layerName: 'barfoo' } - ); - const syncContext2 = new MockSyncContext({ dataFilters: {} }); - await layer2.syncData(syncContext2); - - // @ts-expect-error - sinon.assert.calledOnce(syncContext2.startLoading); - // @ts-expect-error - sinon.assert.calledOnce(syncContext2.stopLoading); - - // @ts-expect-error - const call = syncContext2.stopLoading.getCall(0); - expect(call.args[2]).toEqual({ ...defaultConfig, layerName: 'barfoo' }); + describe('Should resync when changes to source params: ', () => { + [ + { layerName: 'barfoo' }, + { urlTemplate: 'https://sub.example.com/{z}/{x}/{y}.pbf' }, + { minSourceZoom: 1 }, + { maxSourceZoom: 12 }, + ].forEach((changes) => { + it(`change in ${Object.keys(changes).join(',')}`, async () => { + const layer1: TiledVectorLayer = createLayer({}, {}); + const syncContext1 = new MockSyncContext({ dataFilters: {} }); + + await layer1.syncData(syncContext1); + + const dataRequestDescriptor: DataRequestDescriptor = { + data: defaultConfig, + dataId: 'source', + }; + const layer2: TiledVectorLayer = createLayer( + { + __dataRequests: [dataRequestDescriptor], + }, + changes + ); + const syncContext2 = new MockSyncContext({ dataFilters: {} }); + await layer2.syncData(syncContext2); + + // @ts-expect-error + sinon.assert.calledOnce(syncContext2.startLoading); + // @ts-expect-error + sinon.assert.calledOnce(syncContext2.stopLoading); + + // @ts-expect-error + const call = syncContext2.stopLoading.getCall(0); + expect(call.args[2]).toEqual({ ...defaultConfig, ...changes }); + }); + }); }); }); diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx index c9ae1c805fa306..70bf8ea3883b72 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx @@ -63,21 +63,24 @@ export class TiledVectorLayer extends VectorLayer { ); const prevDataRequest = this.getSourceDataRequest(); + const templateWithMeta = await this._source.getUrlTemplateWithMeta(searchFilters); if (prevDataRequest) { const data: MVTSingleLayerVectorSourceConfig = prevDataRequest.getData() as MVTSingleLayerVectorSourceConfig; - const canSkipBecauseNoChanges = - data.layerName === this._source.getLayerName() && - data.minSourceZoom === this._source.getMinZoom() && - data.maxSourceZoom === this._source.getMaxZoom(); - - if (canSkipBecauseNoChanges) { - return null; + if (data) { + const canSkipBecauseNoChanges = + data.layerName === this._source.getLayerName() && + data.minSourceZoom === this._source.getMinZoom() && + data.maxSourceZoom === this._source.getMaxZoom() && + data.urlTemplate === templateWithMeta.urlTemplate; + + if (canSkipBecauseNoChanges) { + return null; + } } } startLoading(SOURCE_DATA_REQUEST_ID, requestToken, searchFilters); try { - const templateWithMeta = await this._source.getUrlTemplateWithMeta(); stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, templateWithMeta, {}); } catch (error) { onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error.message); @@ -160,6 +163,11 @@ export class TiledVectorLayer extends VectorLayer { return false; } + if (!mbTileSource.tiles) { + // Expected source is not compatible, so remove. + return true; + } + const isSourceDifferent = mbTileSource.tiles[0] !== tiledSourceMeta.urlTemplate || mbTileSource.minzoom !== tiledSourceMeta.minSourceZoom || diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js index 2ba7f750e9b409..c49d0044e6ad60 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js @@ -15,9 +15,11 @@ import { SOURCE_BOUNDS_DATA_REQUEST_ID, FEATURE_VISIBLE_PROPERTY_NAME, EMPTY_FEATURE_COLLECTION, + KBN_TOO_MANY_FEATURES_PROPERTY, LAYER_TYPE, FIELD_ORIGIN, LAYER_STYLE_TYPE, + KBN_TOO_MANY_FEATURES_IMAGE_ID, } from '../../../../common/constants'; import _ from 'lodash'; import { JoinTooltipProperty } from '../../tooltips/join_tooltip_property'; @@ -777,6 +779,8 @@ export class VectorLayer extends AbstractLayer { const sourceId = this.getId(); const fillLayerId = this._getMbPolygonLayerId(); const lineLayerId = this._getMbLineLayerId(); + const tooManyFeaturesLayerId = this._getMbTooManyFeaturesLayerId(); + const hasJoins = this.hasJoins(); if (!mbMap.getLayer(fillLayerId)) { const mbLayer = { @@ -802,6 +806,30 @@ export class VectorLayer extends AbstractLayer { } mbMap.addLayer(mbLayer); } + if (!mbMap.getLayer(tooManyFeaturesLayerId)) { + const mbLayer = { + id: tooManyFeaturesLayerId, + type: 'fill', + source: sourceId, + paint: {}, + }; + if (mvtSourceLayer) { + mbLayer['source-layer'] = mvtSourceLayer; + } + mbMap.addLayer(mbLayer); + mbMap.setFilter(tooManyFeaturesLayerId, [ + '==', + ['get', KBN_TOO_MANY_FEATURES_PROPERTY], + true, + ]); + mbMap.setPaintProperty( + tooManyFeaturesLayerId, + 'fill-pattern', + KBN_TOO_MANY_FEATURES_IMAGE_ID + ); + mbMap.setPaintProperty(tooManyFeaturesLayerId, 'fill-opacity', this.getAlpha()); + } + this.getCurrentStyle().setMBPaintProperties({ alpha: this.getAlpha(), mbMap, @@ -822,6 +850,9 @@ export class VectorLayer extends AbstractLayer { if (lineFilterExpr !== mbMap.getFilter(lineLayerId)) { mbMap.setFilter(lineLayerId, lineFilterExpr); } + + this.syncVisibilityWithMb(mbMap, tooManyFeaturesLayerId); + mbMap.setLayerZoomRange(tooManyFeaturesLayerId, this.getMinZoom(), this.getMaxZoom()); } _syncStylePropertiesWithMb(mbMap) { @@ -836,6 +867,19 @@ export class VectorLayer extends AbstractLayer { type: 'geojson', data: EMPTY_FEATURE_COLLECTION, }); + } else if (mbSource.type !== 'geojson') { + // Recreate source when existing source is not geojson. This can occur when layer changes from tile layer to vector layer. + this.getMbLayerIds().forEach((mbLayerId) => { + if (mbMap.getLayer(mbLayerId)) { + mbMap.removeLayer(mbLayerId); + } + }); + + mbMap.removeSource(this._getMbSourceId()); + mbMap.addSource(this._getMbSourceId(), { + type: 'geojson', + data: EMPTY_FEATURE_COLLECTION, + }); } } @@ -865,6 +909,10 @@ export class VectorLayer extends AbstractLayer { return this.makeMbLayerId('fill'); } + _getMbTooManyFeaturesLayerId() { + return this.makeMbLayerId('toomanyfeatures'); + } + getMbLayerIds() { return [ this._getMbPointLayerId(), @@ -872,6 +920,7 @@ export class VectorLayer extends AbstractLayer { this._getMbSymbolLayerId(), this._getMbLineLayerId(), this._getMbPolygonLayerId(), + this._getMbTooManyFeaturesLayerId(), ]; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts index 43bfb74bf54b68..2e0ba7cf3efee0 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { MapExtent, MapFilters } from '../../../../common/descriptor_types'; +import { MapExtent, VectorSourceRequestMeta } from '../../../../common/descriptor_types'; jest.mock('../../../kibana_services'); @@ -19,6 +19,7 @@ import { SearchSource } from '../../../../../../../src/plugins/data/public/searc export class MockSearchSource { setField = jest.fn(); + setParent() {} } describe('ESGeoGridSource', () => { @@ -104,6 +105,9 @@ describe('ESGeoGridSource', () => { async create() { return mockSearchSource as SearchSource; }, + createEmpty() { + return mockSearchSource as SearchSource; + }, }, }; @@ -120,7 +124,7 @@ describe('ESGeoGridSource', () => { maxLat: 80, }; - const mapFilters: MapFilters = { + const mapFilters: VectorSourceRequestMeta = { geogridPrecision: 4, filters: [], timeFilters: { @@ -128,8 +132,16 @@ describe('ESGeoGridSource', () => { to: '15m', mode: 'relative', }, - // extent, + extent, + applyGlobalQuery: true, + fieldNames: [], buffer: extent, + sourceQuery: { + query: '', + language: 'KQL', + queryLastTriggeredAt: '2019-04-25T20:53:22.331Z', + }, + sourceMeta: null, zoom: 0, }; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/__snapshots__/scaling_form.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/es_search_source/__snapshots__/scaling_form.test.tsx.snap index 8ebb389472f74c..dd62be11c679db 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/__snapshots__/scaling_form.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/__snapshots__/scaling_form.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should disable clusters option when clustering is not supported 1`] = ` +exports[`scaling form should disable clusters option when clustering is not supported 1`] = ` + + + + Use vector tiles for faster display of large datasets. + + } + delay="regular" + position="left" + > + + +
+ + + + + +`; + +exports[`scaling form should disable mvt option when mvt is not supported 1`] = ` + + +
+ +
+
+ + +
+ + + + + +
`; -exports[`should render 1`] = ` +exports[`scaling form should render 1`] = ` + + + + Use vector tiles for faster display of large datasets. + + } + delay="regular" + position="left" + > + +
`; -exports[`should render top hits form when scaling type is TOP_HITS 1`] = ` +exports[`scaling form should render top hits form when scaling type is TOP_HITS 1`] = ` + + + + Use vector tiles for faster display of large datasets. + + } + delay="regular" + position="left" + > + +
@@ -159,6 +162,8 @@ export class CreateSourceEditor extends Component { this.state.indexPattern, this.state.geoFieldName )} + supportsMvt={mvtSupported} + mvtDisabledReason={mvtSupported ? null : getMvtDisabledReason()} clusteringDisabledReason={ this.state.indexPattern ? getGeoTileAggNotSupportedReason( diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx index 1ec6d2a1ff6716..249b9a2454d7d5 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx @@ -14,13 +14,18 @@ import { ESSearchSource, sourceTitle } from './es_search_source'; import { BlendedVectorLayer } from '../../layers/blended_vector_layer/blended_vector_layer'; import { VectorLayer } from '../../layers/vector_layer/vector_layer'; import { LAYER_WIZARD_CATEGORY, SCALING_TYPES } from '../../../../common/constants'; +import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer'; export function createDefaultLayerDescriptor(sourceConfig: unknown, mapColors: string[]) { const sourceDescriptor = ESSearchSource.createDescriptor(sourceConfig); - return sourceDescriptor.scalingType === SCALING_TYPES.CLUSTERS - ? BlendedVectorLayer.createDescriptor({ sourceDescriptor }, mapColors) - : VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + if (sourceDescriptor.scalingType === SCALING_TYPES.CLUSTERS) { + return BlendedVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + } else if (sourceDescriptor.scalingType === SCALING_TYPES.MVT) { + return TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + } else { + return VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + } } export const esDocumentsLayerWizardConfig: LayerWizard = { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.d.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.d.ts index 23e3c759d73c39..67d68dc065b006 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.d.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.d.ts @@ -5,11 +5,22 @@ */ import { AbstractESSource } from '../es_source'; -import { ESSearchSourceDescriptor } from '../../../../common/descriptor_types'; +import { ESSearchSourceDescriptor, MapFilters } from '../../../../common/descriptor_types'; +import { ITiledSingleLayerVectorSource } from '../vector_source'; -export class ESSearchSource extends AbstractESSource { +export class ESSearchSource extends AbstractESSource implements ITiledSingleLayerVectorSource { static createDescriptor(sourceConfig: unknown): ESSearchSourceDescriptor; constructor(sourceDescriptor: Partial, inspectorAdapters: unknown); getFieldNames(): string[]; + + getUrlTemplateWithMeta( + searchFilters: MapFilters + ): Promise<{ + layerName: string; + urlTemplate: string; + minSourceZoom: number; + maxSourceZoom: number; + }>; + getLayerName(): string; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js index 6d61c4a7455b21..7ac2738eaeb51f 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js @@ -6,9 +6,10 @@ import _ from 'lodash'; import React from 'react'; +import rison from 'rison-node'; import { AbstractESSource } from '../es_source'; -import { getSearchService } from '../../../kibana_services'; +import { getSearchService, getHttp } from '../../../kibana_services'; import { hitsToGeoJson } from '../../../../common/elasticsearch_geo_utils'; import { UpdateSourceEditor } from './update_source_editor'; import { @@ -18,6 +19,9 @@ import { SORT_ORDER, SCALING_TYPES, VECTOR_SHAPE_TYPE, + MVT_SOURCE_LAYER_NAME, + GIS_API_PATH, + MVT_GETTILE_API_PATH, } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; @@ -96,6 +100,7 @@ export class ESSearchSource extends AbstractESSource { return new ESDocField({ fieldName, source: this, + canReadFromGeoJson: this._descriptor.scalingType !== SCALING_TYPES.MVT, }); } @@ -448,9 +453,13 @@ export class ESSearchSource extends AbstractESSource { } isFilterByMapBounds() { - return this._descriptor.scalingType === SCALING_TYPES.CLUSTER - ? true - : this._descriptor.filterByMapBounds; + if (this._descriptor.scalingType === SCALING_TYPES.CLUSTER) { + return true; + } else if (this._descriptor.scalingType === SCALING_TYPES.MVT) { + return false; + } else { + return this._descriptor.filterByMapBounds; + } } async getLeftJoinFields() { @@ -553,11 +562,65 @@ export class ESSearchSource extends AbstractESSource { } getJoinsDisabledReason() { - return this._descriptor.scalingType === SCALING_TYPES.CLUSTERS - ? i18n.translate('xpack.maps.source.esSearch.joinsDisabledReason', { - defaultMessage: 'Joins are not supported when scaling by clusters', - }) - : null; + let reason; + if (this._descriptor.scalingType === SCALING_TYPES.CLUSTERS) { + reason = i18n.translate('xpack.maps.source.esSearch.joinsDisabledReason', { + defaultMessage: 'Joins are not supported when scaling by clusters', + }); + } else if (this._descriptor.scalingType === SCALING_TYPES.MVT) { + reason = i18n.translate('xpack.maps.source.esSearch.joinsDisabledReasonMvt', { + defaultMessage: 'Joins are not supported when scaling by mvt vector tiles', + }); + } else { + reason = null; + } + return reason; + } + + getLayerName() { + return MVT_SOURCE_LAYER_NAME; + } + + async getUrlTemplateWithMeta(searchFilters) { + const indexPattern = await this.getIndexPattern(); + const indexSettings = await loadIndexSettings(indexPattern.title); + + const { docValueFields, sourceOnlyFields } = getDocValueAndSourceFields( + indexPattern, + searchFilters.fieldNames + ); + + const initialSearchContext = { docvalue_fields: docValueFields }; // Request fields in docvalue_fields insted of _source + + const searchSource = await this.makeSearchSource( + searchFilters, + indexSettings.maxResultWindow, + initialSearchContext + ); + searchSource.setField('fields', searchFilters.fieldNames); // Setting "fields" filters out unused scripted fields + if (sourceOnlyFields.length === 0) { + searchSource.setField('source', false); // do not need anything from _source + } else { + searchSource.setField('source', sourceOnlyFields); + } + if (this._hasSort()) { + searchSource.setField('sort', this._buildEsSort()); + } + + const dsl = await searchSource.getSearchRequestBody(); + const risonDsl = rison.encode(dsl); + + const mvtUrlServicePath = getHttp().basePath.prepend( + `/${GIS_API_PATH}/${MVT_GETTILE_API_PATH}` + ); + + const urlTemplate = `${mvtUrlServicePath}?x={x}&y={y}&z={z}&geometryFieldName=${this._descriptor.geoField}&index=${indexPattern.title}&requestBody=${risonDsl}`; + return { + layerName: this.getLayerName(), + minSourceZoom: this.getMinZoom(), + maxSourceZoom: this.getMaxZoom(), + urlTemplate: urlTemplate, + }; } } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts new file mode 100644 index 00000000000000..3223d0c94178ff --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts @@ -0,0 +1,155 @@ +/* + * 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 { ES_GEO_FIELD_TYPE, SCALING_TYPES } from '../../../../common/constants'; + +jest.mock('../../../kibana_services'); +jest.mock('./load_index_settings'); + +import { getIndexPatternService, getSearchService, getHttp } from '../../../kibana_services'; +import { SearchSource } from '../../../../../../../src/plugins/data/public/search/search_source'; + +// @ts-expect-error +import { loadIndexSettings } from './load_index_settings'; + +import { ESSearchSource } from './es_search_source'; +import { VectorSourceRequestMeta } from '../../../../common/descriptor_types'; + +describe('ESSearchSource', () => { + it('constructor', () => { + const esSearchSource = new ESSearchSource({}, null); + expect(esSearchSource instanceof ESSearchSource).toBe(true); + }); + + describe('ITiledSingleLayerVectorSource', () => { + it('mb-source params', () => { + const esSearchSource = new ESSearchSource({}, null); + expect(esSearchSource.getMinZoom()).toBe(0); + expect(esSearchSource.getMaxZoom()).toBe(24); + expect(esSearchSource.getLayerName()).toBe('source_layer'); + }); + + describe('getUrlTemplateWithMeta', () => { + const geoFieldName = 'bar'; + const mockIndexPatternService = { + get() { + return { + title: 'foobar-title-*', + fields: { + getByName() { + return { + name: geoFieldName, + type: ES_GEO_FIELD_TYPE.GEO_SHAPE, + }; + }, + }, + }; + }, + }; + + beforeEach(async () => { + const mockSearchSource = { + setField: jest.fn(), + getSearchRequestBody() { + return { foobar: 'ES_DSL_PLACEHOLDER', params: this.setField.mock.calls }; + }, + setParent() {}, + }; + const mockSearchService = { + searchSource: { + async create() { + return (mockSearchSource as unknown) as SearchSource; + }, + createEmpty() { + return (mockSearchSource as unknown) as SearchSource; + }, + }, + }; + + // @ts-expect-error + getIndexPatternService.mockReturnValue(mockIndexPatternService); + // @ts-expect-error + getSearchService.mockReturnValue(mockSearchService); + loadIndexSettings.mockReturnValue({ + maxResultWindow: 1000, + }); + // @ts-expect-error + getHttp.mockReturnValue({ + basePath: { + prepend(path: string) { + return `rootdir${path};`; + }, + }, + }); + }); + + const searchFilters: VectorSourceRequestMeta = { + filters: [], + zoom: 0, + fieldNames: ['tooltipField', 'styleField'], + timeFilters: { + from: 'now', + to: '15m', + mode: 'relative', + }, + sourceQuery: { + query: 'tooltipField: foobar', + language: 'KQL', + queryLastTriggeredAt: '2019-04-25T20:53:22.331Z', + }, + sourceMeta: null, + applyGlobalQuery: true, + }; + + it('Should only include required props', async () => { + const esSearchSource = new ESSearchSource( + { geoField: geoFieldName, indexPatternId: 'ipId' }, + null + ); + const urlTemplateWithMeta = await esSearchSource.getUrlTemplateWithMeta(searchFilters); + expect(urlTemplateWithMeta.urlTemplate).toBe( + `rootdir/api/maps/mvt/getTile;?x={x}&y={y}&z={z}&geometryFieldName=bar&index=foobar-title-*&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:(),title:'foobar-title-*')),'1':('0':size,'1':1000),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:(),title:'foobar-title-*')),'5':('0':query,'1':(language:KQL,query:'tooltipField: foobar',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':fields,'1':!(tooltipField,styleField)),'7':('0':source,'1':!(tooltipField,styleField))))` + ); + }); + }); + }); + + describe('isFilterByMapBounds', () => { + it('default', () => { + const esSearchSource = new ESSearchSource({}, null); + expect(esSearchSource.isFilterByMapBounds()).toBe(true); + }); + it('mvt', () => { + const esSearchSource = new ESSearchSource({ scalingType: SCALING_TYPES.MVT }, null); + expect(esSearchSource.isFilterByMapBounds()).toBe(false); + }); + }); + + describe('getJoinsDisabledReason', () => { + it('default', () => { + const esSearchSource = new ESSearchSource({}, null); + expect(esSearchSource.getJoinsDisabledReason()).toBe(null); + }); + it('mvt', () => { + const esSearchSource = new ESSearchSource({ scalingType: SCALING_TYPES.MVT }, null); + expect(esSearchSource.getJoinsDisabledReason()).toBe( + 'Joins are not supported when scaling by mvt vector tiles' + ); + }); + }); + + describe('getFields', () => { + it('default', () => { + const esSearchSource = new ESSearchSource({}, null); + const docField = esSearchSource.createField({ fieldName: 'prop1' }); + expect(docField.canReadFromGeoJson()).toBe(true); + }); + it('mvt', () => { + const esSearchSource = new ESSearchSource({ scalingType: SCALING_TYPES.MVT }, null); + const docField = esSearchSource.createField({ fieldName: 'prop1' }); + expect(docField.canReadFromGeoJson()).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.test.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.test.tsx index 6e56c179b4ead2..f57335db14c629 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.test.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.test.tsx @@ -27,28 +27,46 @@ const defaultProps = { termFields: [], topHitsSplitField: null, topHitsSize: 1, + supportsMvt: true, + mvtDisabledReason: null, }; -test('should render', async () => { - const component = shallow(); +describe('scaling form', () => { + test('should render', async () => { + const component = shallow(); - expect(component).toMatchSnapshot(); -}); + expect(component).toMatchSnapshot(); + }); -test('should disable clusters option when clustering is not supported', async () => { - const component = shallow( - - ); + test('should disable clusters option when clustering is not supported', async () => { + const component = shallow( + + ); - expect(component).toMatchSnapshot(); -}); + expect(component).toMatchSnapshot(); + }); + + test('should render top hits form when scaling type is TOP_HITS', async () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); -test('should render top hits form when scaling type is TOP_HITS', async () => { - const component = shallow(); + test('should disable mvt option when mvt is not supported', async () => { + const component = shallow( + + ); - expect(component).toMatchSnapshot(); + expect(component).toMatchSnapshot(); + }); }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.tsx index 816db6a98d593b..cc2d4d059a3a8f 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.tsx @@ -4,16 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, Component } from 'react'; +import React, { Component, Fragment } from 'react'; import { EuiFormRow, + EuiHorizontalRule, + EuiRadio, + EuiSpacer, EuiSwitch, EuiSwitchEvent, EuiTitle, - EuiSpacer, - EuiHorizontalRule, - EuiRadio, EuiToolTip, + EuiBetaBadge, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -24,8 +25,8 @@ import { ValidatedRange } from '../../../components/validated_range'; import { DEFAULT_MAX_INNER_RESULT_WINDOW, DEFAULT_MAX_RESULT_WINDOW, - SCALING_TYPES, LAYER_TYPE, + SCALING_TYPES, } from '../../../../common/constants'; // @ts-ignore import { loadIndexSettings } from './load_index_settings'; @@ -38,7 +39,9 @@ interface Props { onChange: (args: OnSourceChangeArgs) => void; scalingType: SCALING_TYPES; supportsClustering: boolean; + supportsMvt: boolean; clusteringDisabledReason?: string | null; + mvtDisabledReason?: string | null; termFields: IFieldType[]; topHitsSplitField: string | null; topHitsSize: number; @@ -80,8 +83,15 @@ export class ScalingForm extends Component { } _onScalingTypeChange = (optionId: string): void => { - const layerType = - optionId === SCALING_TYPES.CLUSTERS ? LAYER_TYPE.BLENDED_VECTOR : LAYER_TYPE.VECTOR; + let layerType; + if (optionId === SCALING_TYPES.CLUSTERS) { + layerType = LAYER_TYPE.BLENDED_VECTOR; + } else if (optionId === SCALING_TYPES.MVT) { + layerType = LAYER_TYPE.TILED_VECTOR; + } else { + layerType = LAYER_TYPE.VECTOR; + } + this.props.onChange({ propName: 'scalingType', value: optionId, newLayerType: layerType }); }; @@ -177,9 +187,47 @@ export class ScalingForm extends Component { ); } + _renderMVTRadio() { + const labelText = i18n.translate('xpack.maps.source.esSearch.useMVTVectorTiles', { + defaultMessage: 'Use vector tiles', + }); + const mvtRadio = ( + this._onScalingTypeChange(SCALING_TYPES.MVT)} + disabled={!this.props.supportsMvt} + /> + ); + + const enabledInfo = ( + <> + + + {i18n.translate('xpack.maps.source.esSearch.mvtDescription', { + defaultMessage: 'Use vector tiles for faster display of large datasets.', + })} + + ); + + return !this.props.supportsMvt ? ( + + {mvtRadio} + + ) : ( + + {mvtRadio} + + ); + } + render() { let filterByBoundsSwitch; - if (this.props.scalingType !== SCALING_TYPES.CLUSTERS) { + if ( + this.props.scalingType === SCALING_TYPES.TOP_HITS || + this.props.scalingType === SCALING_TYPES.LIMIT + ) { filterByBoundsSwitch = ( { ); } - let scalingForm = null; + let topHitsOptionsForm = null; if (this.props.scalingType === SCALING_TYPES.TOP_HITS) { - scalingForm = ( + topHitsOptionsForm = ( {this._renderTopHitsForm()} @@ -234,12 +282,12 @@ export class ScalingForm extends Component { onChange={() => this._onScalingTypeChange(SCALING_TYPES.TOP_HITS)} /> {this._renderClusteringRadio()} + {this._renderMVTRadio()}
{filterByBoundsSwitch} - - {scalingForm} + {topHitsOptionsForm} ); } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js index 0701dbbaecdd56..c123c307c4895b 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js @@ -17,6 +17,8 @@ import { getTermsFields, getSourceFields, supportsGeoTileAgg, + supportsMvt, + getMvtDisabledReason, } from '../../../index_pattern_util'; import { SORT_ORDER } from '../../../../common/constants'; import { ESDocField } from '../../fields/es_doc_field'; @@ -42,6 +44,9 @@ export class UpdateSourceEditor extends Component { termFields: null, sortFields: null, supportsClustering: false, + supportsMvt: false, + mvtDisabledReason: null, + clusteringDisabledReason: null, }; componentDidMount() { @@ -94,9 +99,12 @@ export class UpdateSourceEditor extends Component { }); }); + const mvtSupported = supportsMvt(indexPattern, geoField.name); this.setState({ supportsClustering: supportsGeoTileAgg(geoField), + supportsMvt: mvtSupported, clusteringDisabledReason: getGeoTileAggNotSupportedReason(geoField), + mvtDisabledReason: mvtSupported ? null : getMvtDisabledReason(), sourceFields: sourceFields, termFields: getTermsFields(indexPattern.fields), //todo change term fields to use fields sortFields: indexPattern.fields.filter( @@ -207,7 +215,9 @@ export class UpdateSourceEditor extends Component { onChange={this.props.onChange} scalingType={this.props.scalingType} supportsClustering={this.state.supportsClustering} + supportsMvt={this.state.supportsMvt} clusteringDisabledReason={this.state.clusteringDisabledReason} + mvtDisabledReason={this.state.mvtDisabledReason} termFields={this.state.termFields} topHitsSplitField={this.props.topHitsSplitField} topHitsSize={this.props.topHitsSize} diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js index 8cc2aa018979b5..56b830e9ff0983 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js @@ -184,7 +184,7 @@ export class AbstractESSource extends AbstractVectorSource { const minLon = esBounds.top_left.lon; const maxLon = esBounds.bottom_right.lon; return { - minLon: minLon > maxLon ? minLon - 360 : minLon, + minLon: minLon > maxLon ? minLon - 360 : minLon, //fixes an ES bbox to straddle dateline maxLon, minLat: esBounds.bottom_right.lat, maxLat: esBounds.top_left.lat, diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts index 271505010f36a8..fd9c1792754448 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts @@ -14,6 +14,7 @@ import { MapExtent, MapFilters, MapQuery, + VectorSourceRequestMeta, VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; import { VECTOR_SHAPE_TYPE } from '../../../../common/constants'; @@ -64,7 +65,7 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc ): MapExtent | null; getGeoJsonWithMeta( layerName: string, - searchFilters: MapFilters, + searchFilters: VectorSourceRequestMeta, registerCancelCallback: (callback: () => void) => void ): Promise; @@ -79,7 +80,9 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc } export interface ITiledSingleLayerVectorSource extends IVectorSource { - getUrlTemplateWithMeta(): Promise<{ + getUrlTemplateWithMeta( + searchFilters: VectorSourceRequestMeta + ): Promise<{ layerName: string; urlTemplate: string; minSourceZoom: number; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/categorical_field_meta_popover.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/categorical_field_meta_popover.tsx index e49c15c68b8db9..2a544b94d760ab 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/categorical_field_meta_popover.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/categorical_field_meta_popover.tsx @@ -14,6 +14,7 @@ import { FieldMetaOptions } from '../../../../../../common/descriptor_types'; type Props = { fieldMetaOptions: FieldMetaOptions; onChange: (fieldMetaOptions: FieldMetaOptions) => void; + switchDisabled: boolean; }; export function CategoricalFieldMetaPopover(props: Props) { @@ -34,6 +35,7 @@ export function CategoricalFieldMetaPopover(props: Props) { checked={props.fieldMetaOptions.isEnabled} onChange={onIsEnabledChange} compressed + disabled={props.switchDisabled} /> diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/ordinal_field_meta_popover.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/ordinal_field_meta_popover.tsx index 9086c4df315967..09be9d72af9704 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/ordinal_field_meta_popover.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/ordinal_field_meta_popover.tsx @@ -40,6 +40,7 @@ type Props = { fieldMetaOptions: FieldMetaOptions; styleName: VECTOR_STYLES; onChange: (fieldMetaOptions: FieldMetaOptions) => void; + switchDisabled: boolean; }; export function OrdinalFieldMetaPopover(props: Props) { @@ -66,6 +67,7 @@ export function OrdinalFieldMetaPopover(props: Props) { checked={props.fieldMetaOptions.isEnabled} onChange={onIsEnabledChange} compressed + disabled={props.switchDisabled} /> diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_color_property.test.tsx.snap b/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_color_property.test.tsx.snap index c722e86512e528..34d2d7fb0cbbf6 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_color_property.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_color_property.test.tsx.snap @@ -325,3 +325,29 @@ exports[`ordinal Should render ordinal legend as bands 1`] = `
`; + +exports[`renderFieldMetaPopover Should disable toggle when field is not backed by geojson source 1`] = ` + +`; + +exports[`renderFieldMetaPopover Should enable toggle when field is backed by geojson-source 1`] = ` + +`; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx index c3610cbc78e15f..de8f3b5c09175f 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx @@ -577,3 +577,39 @@ test('Should read out ordinal type correctly', async () => { expect(ordinalColorStyle2.isOrdinal()).toEqual(true); expect(ordinalColorStyle2.isCategorical()).toEqual(false); }); + +describe('renderFieldMetaPopover', () => { + test('Should enable toggle when field is backed by geojson-source', () => { + const colorStyle = makeProperty( + { + color: 'Blues', + type: undefined, + fieldMetaOptions, + }, + undefined, + mockField + ); + + const legendRow = colorStyle.renderFieldMetaPopover(() => {}); + expect(legendRow).toMatchSnapshot(); + }); + + test('Should disable toggle when field is not backed by geojson source', () => { + const nonGeoJsonField = Object.create(mockField); + nonGeoJsonField.canReadFromGeoJson = () => { + return false; + }; + const colorStyle = makeProperty( + { + color: 'Blues', + type: undefined, + fieldMetaOptions, + }, + undefined, + nonGeoJsonField + ); + + const legendRow = colorStyle.renderFieldMetaPopover(() => {}); + expect(legendRow).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx index b16755e69f92de..f6ab052497723e 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx @@ -328,16 +328,20 @@ export class DynamicStyleProperty return null; } + const switchDisabled = !!this._field && !this._field.canReadFromGeoJson(); + return this.isCategorical() ? ( ) : ( ); } diff --git a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts index 8da6fa2318de96..0da6f632eb4a84 100644 --- a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts +++ b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts @@ -4,32 +4,48 @@ * you may not use this file except in compliance with the Elastic License. */ -import { GEO_JSON_TYPE, FEATURE_VISIBLE_PROPERTY_NAME } from '../../../common/constants'; +import { + GEO_JSON_TYPE, + FEATURE_VISIBLE_PROPERTY_NAME, + KBN_TOO_MANY_FEATURES_PROPERTY, +} from '../../../common/constants'; + +export const EXCLUDE_TOO_MANY_FEATURES_BOX = ['!=', ['get', KBN_TOO_MANY_FEATURES_PROPERTY], true]; const VISIBILITY_FILTER_CLAUSE = ['all', ['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]]; +const TOO_MANY_FEATURES_FILTER = ['all', EXCLUDE_TOO_MANY_FEATURES_BOX]; const CLOSED_SHAPE_MB_FILTER = [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], + ...TOO_MANY_FEATURES_FILTER, + [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], + ], ]; const VISIBLE_CLOSED_SHAPE_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, CLOSED_SHAPE_MB_FILTER]; const ALL_SHAPE_MB_FILTER = [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING], + ...TOO_MANY_FEATURES_FILTER, + [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING], + ], ]; const VISIBLE_ALL_SHAPE_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, ALL_SHAPE_MB_FILTER]; const POINT_MB_FILTER = [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POINT], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT], + ...TOO_MANY_FEATURES_FILTER, + [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POINT], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT], + ], ]; const VISIBLE_POINT_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, POINT_MB_FILTER]; diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js index 87d6f8e1d8e716..edfeb3c76b1046 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js @@ -8,6 +8,7 @@ import _ from 'lodash'; import React from 'react'; import { FEATURE_ID_PROPERTY_NAME, LON_INDEX } from '../../../../../common/constants'; import { TooltipPopover } from './tooltip_popover'; +import { EXCLUDE_TOO_MANY_FEATURES_BOX } from '../../../../classes/util/mb_filter_expressions'; function justifyAnchorLocation(mbLngLat, targetFeature) { let popupAnchorLocation = [mbLngLat.lng, mbLngLat.lat]; // default popup location to mouse location @@ -79,7 +80,7 @@ export class TooltipControl extends React.Component { // - As empty object literal // To avoid ambiguity, normalize properties to empty object literal. const mbProperties = mbFeature.properties ? mbFeature.properties : {}; - //This keeps track of first properties (assuming these will be identical for features in different tiles + //This keeps track of first properties (assuming these will be identical for features in different tiles) uniqueFeatures.push({ id: featureId, layerId: layerId, @@ -175,7 +176,10 @@ export class TooltipControl extends React.Component { y: mbLngLatPoint.y + PADDING, }, ]; - return this.props.mbMap.queryRenderedFeatures(mbBbox, { layers: mbLayerIds }); + return this.props.mbMap.queryRenderedFeatures(mbBbox, { + layers: mbLayerIds, + filter: EXCLUDE_TOO_MANY_FEATURES_BOX, + }); } render() { diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/plugins/maps/public/connected_components/map/mb/view.js index 22c374aceedd5d..eede1edf40cc4f 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/view.js @@ -10,7 +10,11 @@ import { ResizeChecker } from '../../../../../../../src/plugins/kibana_utils/pub import { removeOrphanedSourcesAndLayers, addSpritesheetToMap } from './utils'; import { syncLayerOrder } from './sort_layers'; import { getGlyphUrl, isRetina } from '../../../meta'; -import { DECIMAL_DEGREES_PRECISION, ZOOM_PRECISION } from '../../../../common/constants'; +import { + DECIMAL_DEGREES_PRECISION, + KBN_TOO_MANY_FEATURES_IMAGE_ID, + ZOOM_PRECISION, +} from '../../../../common/constants'; import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker'; import mbRtlPlugin from '!!file-loader!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js'; @@ -143,6 +147,14 @@ export class MBMap extends React.Component { mbMap.addControl(new mapboxgl.NavigationControl({ showCompass: false }), 'top-left'); } + const tooManyFeaturesImageSrc = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAA7DgAAOw4BzLahgwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAARLSURBVHic7ZnPbxRVAMe/7735sWO3293ZlUItJsivCxEE0oTYRgu1FqTQoFSwKTYx8SAH/wHjj4vRozGGi56sMcW2UfqTEuOhppE0KJc2GIuKQFDY7qzdtrudX88D3YTUdFuQN8+k87ltZt7uZz958/bNLAGwBWsYKltANmEA2QKyCQPIFpBNGEC2gGzCALIFZBMGkC0gmzCAbAHZhAFkC8gmDCBbQDZhANkCslnzARQZH6oDpNs0D5UDSUIInePcOpPLfdfnODNBuwQWIAWwNOABwHZN0x8npE6hNLJ4DPWRyFSf40wE5VOEQPBjcR0g3YlE4ybGmtK+/1NzJtOZA/xSYwZMs3nG962T2ez3It2AANaA/kSidYuivOQBs5WM1fUnk6f0u+GXJUqIuUtVXx00zRbRfkIDfBqL7a1WlIYbjvNtTTr99jXXHVpH6dMjK0R4cXq6c9rzxjcx9sKX8XitSEdhAToMI7VP10/97fsTh7PZrgWAN1lW72KE2vOm2b5chDTgtWQyn93x/bEEIetEOQIC14CxVOr1CkKefH929t0v8vn0vcdGEoljGxXl4C3PGz2YyXy+AHARDqtByAxoUdWKBKV70r4/vvTLA0CjZfX+5nkDGxirKzUTgkBIgNaysh3gnF627R+XO+dQJvP1ddcdrmSsbtA020pF+CAW21qrqmUiXIUEqGRsIwD0FQq/lzqv0bJ6rrvucBVjzwyb5ivLRTiiaW+8VV7eIEBVTAANiIIQd9RxZlc6t9Gyem647vn1jD07ZJonl4sQASoevqmgABzwwHnJzc69PGdZ3X+47sgGxuqHTPPE0ggeVtg5/QeEBMhxPg1Aa1DV2GrHPG9ZXy1G2D+wNALn9jyQEeHKAJgP+033Kgrdqij7AFwZtu3bqx3XWShMHtV1o1pRGo4YxiNd+fyEB2DKdX/4aG5u0hbwcylkBryTy/3scT6zW9Nq7ndso2Wdvea6Q1WUHuiPx1/WAXLBcWZXun94UMRcAoD/p+ddTFK6u8MwUvc7vsmyem+67oVqVT0wkEgcF+FYRNhW+L25uX6f84XThtHxIBudE5bVY/t++jFVrU/dvVSFICzAqG3PX/S8rihj2/61qK1AOUB7ksl2jdLUL7Z9rvgcQQRCFsEi5wqFmw26XnhCUQ63GcZmCly95Lrzpca0G0byk3j8tEnpU1c975tmyxoU5QcE8EAEAM5WVOzfoarHAeC2749dcpzxMwsLv07Ztg0AOzVNf03Ttu/S9T2PMlbjc25fdpyutmx2TLRbIAEA4M1otKo1EjmaoHQn4ZwBgA/kAVAK6MXXdzxv/ONcrq/HcbJBeAUWoEizqsaORaPbKglZrxMSZZyrM76f/ovzWx/m85PFWREUgQf4v7Hm/xcIA8gWkE0YQLaAbMIAsgVkEwaQLSCbMIBsAdmEAWQLyCYMIFtANmEA2QKyCQPIFpDNmg/wD3OFdEybUvJjAAAAAElFTkSuQmCC'; + const tooManyFeaturesImage = new Image(); + tooManyFeaturesImage.onload = () => { + mbMap.addImage(KBN_TOO_MANY_FEATURES_IMAGE_ID, tooManyFeaturesImage); + }; + tooManyFeaturesImage.src = tooManyFeaturesImageSrc; + let emptyImage; mbMap.on('styleimagemissing', (e) => { if (emptyImage) { diff --git a/x-pack/plugins/maps/public/index_pattern_util.ts b/x-pack/plugins/maps/public/index_pattern_util.ts index 4b4bfb41990b92..bd2a14619ac419 100644 --- a/x-pack/plugins/maps/public/index_pattern_util.ts +++ b/x-pack/plugins/maps/public/index_pattern_util.ts @@ -81,6 +81,16 @@ export function supportsGeoTileAgg(field?: IFieldType): boolean { ); } +export function supportsMvt(indexPattern: IndexPattern, geoFieldName: string): boolean { + const field = indexPattern.fields.getByName(geoFieldName); + return !!field && field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE; +} + +export function getMvtDisabledReason() { + return i18n.translate('xpack.maps.mbt.disabled', { + defaultMessage: 'Display as vector tiles is only supported for geo_shape field-types.', + }); +} // Returns filtered fields list containing only fields that exist in _source. export function getSourceFields(fields: IFieldType[]): IFieldType[] { return fields.filter((field) => { diff --git a/x-pack/plugins/maps/server/mvt/__tests__/json/0_0_0_search.json b/x-pack/plugins/maps/server/mvt/__tests__/json/0_0_0_search.json new file mode 100644 index 00000000000000..0fc99ffd811f71 --- /dev/null +++ b/x-pack/plugins/maps/server/mvt/__tests__/json/0_0_0_search.json @@ -0,0 +1 @@ +{"took":0,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":1,"relation":"eq"},"max_score":0,"hits":[{"_index":"poly","_id":"G7PRMXQBgyyZ-h5iYibj","_score":0,"_source":{"coordinates":{"coordinates":[[[-106.171875,36.59788913307022],[-50.625,-22.91792293614603],[4.921875,42.8115217450979],[-33.046875,63.54855223203644],[-66.796875,63.860035895395306],[-106.171875,36.59788913307022]]],"type":"polygon"}}}]}} diff --git a/x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0.pbf b/x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0.pbf new file mode 100644 index 0000000000000000000000000000000000000000..9a9296e2ece3f94ac726f6a83f2958ba2e22074b GIT binary patch literal 155 zcmb1|!C1k>#Z#PLT9lj`pOaXbTBOmSAfzP3#=yYH$iyVUtR%)cfww_Yse%1Hdjp%m z1GW`x?>R5<@Jq49XXd4(R!A|&XQoIA$H!+U<;BORr6!h?7Nr7(;^URrxL6AEb1Id@ lxJ2B|1A=@b0-e$;E2DHXOfw@hld_a#xuikzR@fx13;_42EcXBa literal 0 HcmV?d00001 diff --git a/x-pack/plugins/maps/server/mvt/__tests__/tile_searches.ts b/x-pack/plugins/maps/server/mvt/__tests__/tile_searches.ts new file mode 100644 index 00000000000000..317d6434cf81ef --- /dev/null +++ b/x-pack/plugins/maps/server/mvt/__tests__/tile_searches.ts @@ -0,0 +1,28 @@ +/* + * 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 * as path from 'path'; +import * as fs from 'fs'; + +const search000path = path.resolve(__dirname, './json/0_0_0_search.json'); +const search000raw = fs.readFileSync(search000path); +const search000json = JSON.parse((search000raw as unknown) as string); + +export const TILE_SEARCHES = { + '0.0.0': { + countResponse: { + count: 1, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + }, + searchResponse: search000json, + }, + '1.1.0': {}, +}; diff --git a/x-pack/plugins/maps/server/mvt/get_tile.test.ts b/x-pack/plugins/maps/server/mvt/get_tile.test.ts new file mode 100644 index 00000000000000..b9c928d594539c --- /dev/null +++ b/x-pack/plugins/maps/server/mvt/get_tile.test.ts @@ -0,0 +1,63 @@ +/* + * 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 { getTile } from './get_tile'; +import { TILE_SEARCHES } from './__tests__/tile_searches'; +import { Logger } from 'src/core/server'; +import * as path from 'path'; +import * as fs from 'fs'; + +describe('getTile', () => { + const mockCallElasticsearch = jest.fn(); + + const requestBody = { + _source: { excludes: [] }, + docvalue_fields: [], + query: { bool: { filter: [{ match_all: {} }], must: [], must_not: [], should: [] } }, + script_fields: {}, + size: 10000, + stored_fields: ['*'], + }; + const geometryFieldName = 'coordinates'; + + beforeEach(() => { + mockCallElasticsearch.mockReset(); + }); + + test('0.0.0 - under limit', async () => { + mockCallElasticsearch.mockImplementation((type) => { + if (type === 'count') { + return TILE_SEARCHES['0.0.0'].countResponse; + } else if (type === 'search') { + return TILE_SEARCHES['0.0.0'].searchResponse; + } else { + throw new Error(`${type} not recognized`); + } + }); + + const tile = await getTile({ + x: 0, + y: 0, + z: 0, + index: 'world_countries', + requestBody, + geometryFieldName, + logger: ({ + info: () => {}, + } as unknown) as Logger, + callElasticsearch: mockCallElasticsearch, + }); + + if (tile === null) { + throw new Error('Tile should be created'); + } + + const expectedPath = path.resolve(__dirname, './__tests__/pbf/0_0_0.pbf'); + const expectedBin = fs.readFileSync(expectedPath, 'binary'); + const expectedTile = Buffer.from(expectedBin, 'binary'); + expect(expectedTile.equals(tile)).toBe(true); + }); +}); diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts new file mode 100644 index 00000000000000..9621f7f174a306 --- /dev/null +++ b/x-pack/plugins/maps/server/mvt/get_tile.ts @@ -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. + */ + +// @ts-expect-error +import geojsonvt from 'geojson-vt'; +// @ts-expect-error +import vtpbf from 'vt-pbf'; +import { Logger } from 'src/core/server'; +import { Feature, FeatureCollection, Polygon } from 'geojson'; +import { + ES_GEO_FIELD_TYPE, + FEATURE_ID_PROPERTY_NAME, + KBN_TOO_MANY_FEATURES_PROPERTY, + MVT_SOURCE_LAYER_NAME, +} from '../../common/constants'; + +import { hitsToGeoJson } from '../../common/elasticsearch_geo_utils'; +import { flattenHit } from './util'; + +interface ESBounds { + top_left: { + lon: number; + lat: number; + }; + bottom_right: { + lon: number; + lat: number; + }; +} + +export async function getTile({ + logger, + callElasticsearch, + index, + geometryFieldName, + x, + y, + z, + requestBody = {}, +}: { + x: number; + y: number; + z: number; + geometryFieldName: string; + index: string; + callElasticsearch: (type: string, ...args: any[]) => Promise; + logger: Logger; + requestBody: any; +}): Promise { + const geojsonBbox = tileToGeoJsonPolygon(x, y, z); + + let resultFeatures: Feature[]; + try { + let result; + try { + const geoShapeFilter = { + geo_shape: { + [geometryFieldName]: { + shape: geojsonBbox, + relation: 'INTERSECTS', + }, + }, + }; + requestBody.query.bool.filter.push(geoShapeFilter); + + const esSearchQuery = { + index, + body: requestBody, + }; + + const esCountQuery = { + index, + body: { + query: requestBody.query, + }, + }; + + const countResult = await callElasticsearch('count', esCountQuery); + + // @ts-expect-error + if (countResult.count > requestBody.size) { + // Generate "too many features"-bounds + const bboxAggName = 'data_bounds'; + const bboxQuery = { + index, + body: { + size: 0, + query: requestBody.query, + aggs: { + [bboxAggName]: { + geo_bounds: { + field: geometryFieldName, + }, + }, + }, + }, + }; + + const bboxResult = await callElasticsearch('search', bboxQuery); + + // @ts-expect-error + const bboxForData = esBboxToGeoJsonPolygon(bboxResult.aggregations[bboxAggName].bounds); + + resultFeatures = [ + { + type: 'Feature', + properties: { + [KBN_TOO_MANY_FEATURES_PROPERTY]: true, + }, + geometry: bboxForData, + }, + ]; + } else { + // Perform actual search + result = await callElasticsearch('search', esSearchQuery); + + // Todo: pass in epochMillies-fields + const featureCollection = hitsToGeoJson( + // @ts-expect-error + result.hits.hits, + (hit: Record) => { + return flattenHit(geometryFieldName, hit); + }, + geometryFieldName, + ES_GEO_FIELD_TYPE.GEO_SHAPE, + [] + ); + + resultFeatures = featureCollection.features; + + // Correct system-fields. + for (let i = 0; i < resultFeatures.length; i++) { + const props = resultFeatures[i].properties; + if (props !== null) { + props[FEATURE_ID_PROPERTY_NAME] = resultFeatures[i].id; + } + } + } + } catch (e) { + logger.warn(e.message); + throw e; + } + + const featureCollection: FeatureCollection = { + features: resultFeatures, + type: 'FeatureCollection', + }; + + const tileIndex = geojsonvt(featureCollection, { + maxZoom: 24, // max zoom to preserve detail on; can't be higher than 24 + tolerance: 3, // simplification tolerance (higher means simpler) + extent: 4096, // tile extent (both width and height) + buffer: 64, // tile buffer on each side + debug: 0, // logging level (0 to disable, 1 or 2) + lineMetrics: false, // whether to enable line metrics tracking for LineString/MultiLineString features + promoteId: null, // name of a feature property to promote to feature.id. Cannot be used with `generateId` + generateId: false, // whether to generate feature ids. Cannot be used with `promoteId` + indexMaxZoom: 5, // max zoom in the initial tile index + indexMaxPoints: 100000, // max number of points per tile in the index + }); + const tile = tileIndex.getTile(z, x, y); + + if (tile) { + const pbf = vtpbf.fromGeojsonVt({ [MVT_SOURCE_LAYER_NAME]: tile }, { version: 2 }); + return Buffer.from(pbf); + } else { + return null; + } + } catch (e) { + logger.warn(`Cannot generate tile for ${z}/${x}/${y}: ${e.message}`); + return null; + } +} + +function tileToGeoJsonPolygon(x: number, y: number, z: number): Polygon { + const wLon = tile2long(x, z); + const sLat = tile2lat(y + 1, z); + const eLon = tile2long(x + 1, z); + const nLat = tile2lat(y, z); + + return { + type: 'Polygon', + coordinates: [ + [ + [wLon, sLat], + [wLon, nLat], + [eLon, nLat], + [eLon, sLat], + [wLon, sLat], + ], + ], + }; +} + +function tile2long(x: number, z: number): number { + return (x / Math.pow(2, z)) * 360 - 180; +} + +function tile2lat(y: number, z: number): number { + const n = Math.PI - (2 * Math.PI * y) / Math.pow(2, z); + return (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))); +} + +function esBboxToGeoJsonPolygon(esBounds: ESBounds): Polygon { + let minLon = esBounds.top_left.lon; + const maxLon = esBounds.bottom_right.lon; + minLon = minLon > maxLon ? minLon - 360 : minLon; // fixes an ES bbox to straddle dateline + const minLat = esBounds.bottom_right.lat; + const maxLat = esBounds.top_left.lat; + + return { + type: 'Polygon', + coordinates: [ + [ + [minLon, minLat], + [minLon, maxLat], + [maxLon, maxLat], + [maxLon, minLat], + [minLon, minLat], + ], + ], + }; +} diff --git a/x-pack/plugins/maps/server/mvt/mvt_routes.ts b/x-pack/plugins/maps/server/mvt/mvt_routes.ts new file mode 100644 index 00000000000000..32c14a355ba2a1 --- /dev/null +++ b/x-pack/plugins/maps/server/mvt/mvt_routes.ts @@ -0,0 +1,73 @@ +/* + * 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 rison from 'rison-node'; +import { schema } from '@kbn/config-schema'; +import { Logger } from 'src/core/server'; +import { IRouter } from 'src/core/server'; +import { MVT_GETTILE_API_PATH, API_ROOT_PATH } from '../../common/constants'; +import { getTile } from './get_tile'; + +const CACHE_TIMEOUT = 0; // Todo. determine good value. Unsure about full-implications (e.g. wrt. time-based data). + +export function initMVTRoutes({ router, logger }: { logger: Logger; router: IRouter }) { + router.get( + { + path: `${API_ROOT_PATH}/${MVT_GETTILE_API_PATH}`, + validate: { + query: schema.object({ + x: schema.number(), + y: schema.number(), + z: schema.number(), + geometryFieldName: schema.string(), + requestBody: schema.string(), + index: schema.string(), + }), + }, + }, + async (context, request, response) => { + const { query } = request; + + const callElasticsearch = async (type: string, ...args: any[]): Promise => { + return await context.core.elasticsearch.legacy.client.callAsCurrentUser(type, ...args); + }; + + const requestBodyDSL = rison.decode(query.requestBody); + + const tile = await getTile({ + logger, + callElasticsearch, + geometryFieldName: query.geometryFieldName, + x: query.x, + y: query.y, + z: query.z, + index: query.index, + requestBody: requestBodyDSL, + }); + + if (tile) { + return response.ok({ + body: tile, + headers: { + 'content-disposition': 'inline', + 'content-length': `${tile.length}`, + 'Content-Type': 'application/x-protobuf', + 'Cache-Control': `max-age=${CACHE_TIMEOUT}`, + }, + }); + } else { + return response.ok({ + headers: { + 'content-disposition': 'inline', + 'content-length': '0', + 'Content-Type': 'application/x-protobuf', + 'Cache-Control': `max-age=${CACHE_TIMEOUT}`, + }, + }); + } + } + ); +} diff --git a/x-pack/plugins/maps/server/mvt/util.ts b/x-pack/plugins/maps/server/mvt/util.ts new file mode 100644 index 00000000000000..eb85468dd770dc --- /dev/null +++ b/x-pack/plugins/maps/server/mvt/util.ts @@ -0,0 +1,72 @@ +/* + * 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. + */ + +// This implementation: +// - does not include meta-fields +// - does not validate the schema against the index-pattern (e.g. nested fields) +// In the context of .mvt this is sufficient: +// - only fields from the response are packed in the tile (more efficient) +// - query-dsl submitted from the client, which was generated by the IndexPattern +// todo: Ideally, this should adapt/reuse from https://github.com/elastic/kibana/blob/52b42a81faa9dd5c102b9fbb9a645748c3623121/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts#L26 +import { GeoJsonProperties } from 'geojson'; + +export function flattenHit(geometryField: string, hit: Record): GeoJsonProperties { + const flat: GeoJsonProperties = {}; + if (hit) { + flattenSource(flat, '', hit._source as Record, geometryField); + if (hit.fields) { + flattenFields(flat, hit.fields as Array>); + } + + // Attach meta fields + flat._index = hit._index; + flat._id = hit._id; + } + return flat; +} + +function flattenSource( + accum: GeoJsonProperties, + path: string, + properties: Record = {}, + geometryField: string +): GeoJsonProperties { + accum = accum || {}; + for (const key in properties) { + if (properties.hasOwnProperty(key)) { + const newKey = path ? path + '.' + key : key; + let value; + if (geometryField === newKey) { + value = properties[key]; // do not deep-copy the geometry + } else if (properties[key] !== null && typeof value === 'object' && !Array.isArray(value)) { + value = flattenSource( + accum, + newKey, + properties[key] as Record, + geometryField + ); + } else { + value = properties[key]; + } + accum[newKey] = value; + } + } + return accum; +} + +function flattenFields(accum: GeoJsonProperties = {}, fields: Array>) { + accum = accum || {}; + for (const key in fields) { + if (fields.hasOwnProperty(key)) { + const value = fields[key]; + if (Array.isArray(value)) { + accum[key] = value[0]; + } else { + accum[key] = value; + } + } + } +} diff --git a/x-pack/plugins/maps/server/routes.js b/x-pack/plugins/maps/server/routes.js index 1876c0de19c560..6b19103b597228 100644 --- a/x-pack/plugins/maps/server/routes.js +++ b/x-pack/plugins/maps/server/routes.js @@ -22,6 +22,7 @@ import { EMS_SPRITES_PATH, INDEX_SETTINGS_API_PATH, FONTS_API_PATH, + API_ROOT_PATH, } from '../common/constants'; import { EMSClient } from '@elastic/ems-client'; import fetch from 'node-fetch'; @@ -30,8 +31,7 @@ import { getIndexPatternSettings } from './lib/get_index_pattern_settings'; import { schema } from '@kbn/config-schema'; import fs from 'fs'; import path from 'path'; - -const ROOT = `/${GIS_API_PATH}`; +import { initMVTRoutes } from './mvt/mvt_routes'; export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { let emsClient; @@ -69,7 +69,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_FILES_API_PATH}/${EMS_FILES_DEFAULT_JSON_PATH}`, + path: `${API_ROOT_PATH}/${EMS_FILES_API_PATH}/${EMS_FILES_DEFAULT_JSON_PATH}`, validate: { query: schema.object({ id: schema.maybe(schema.string()), @@ -109,7 +109,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_RASTER_TILE_PATH}`, + path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_TILES_RASTER_TILE_PATH}`, validate: false, }, async (context, request, response) => { @@ -145,7 +145,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_CATALOGUE_PATH}`, + path: `${API_ROOT_PATH}/${EMS_CATALOGUE_PATH}`, validate: false, }, async (context, request, { ok, badRequest }) => { @@ -181,7 +181,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_FILES_CATALOGUE_PATH}/{emsVersion}/manifest`, + path: `${API_ROOT_PATH}/${EMS_FILES_CATALOGUE_PATH}/{emsVersion}/manifest`, validate: false, }, async (context, request, { ok, badRequest }) => { @@ -213,7 +213,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_TILES_CATALOGUE_PATH}/{emsVersion}/manifest`, + path: `${API_ROOT_PATH}/${EMS_TILES_CATALOGUE_PATH}/{emsVersion}/manifest`, validate: false, }, async (context, request, { ok, badRequest }) => { @@ -257,7 +257,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_RASTER_STYLE_PATH}`, + path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_TILES_RASTER_STYLE_PATH}`, validate: { query: schema.object({ id: schema.maybe(schema.string()), @@ -293,7 +293,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_STYLE_PATH}`, + path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_STYLE_PATH}`, validate: { query: schema.object({ id: schema.string(), @@ -341,7 +341,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_SOURCE_PATH}`, + path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_SOURCE_PATH}`, validate: { query: schema.object({ id: schema.string(), @@ -379,7 +379,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_TILE_PATH}`, + path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_TILE_PATH}`, validate: { query: schema.object({ id: schema.string(), @@ -417,7 +417,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_GLYPHS_PATH}/{fontstack}/{range}`, + path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_GLYPHS_PATH}/{fontstack}/{range}`, validate: { params: schema.object({ fontstack: schema.string(), @@ -439,7 +439,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_SPRITES_PATH}/{id}/sprite{scaling?}.{extension}`, + path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_SPRITES_PATH}/{id}/sprite{scaling?}.{extension}`, validate: { query: schema.object({ elastic_tile_service_tos: schema.maybe(schema.string()), @@ -591,4 +591,6 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { return response.badRequest(`Cannot connect to EMS`); } } + + initMVTRoutes({ router, logger }); } diff --git a/x-pack/test/api_integration/apis/maps/get_tile.js b/x-pack/test/api_integration/apis/maps/get_tile.js new file mode 100644 index 00000000000000..7219fc858e059c --- /dev/null +++ b/x-pack/test/api_integration/apis/maps/get_tile.js @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function ({ getService }) { + const supertest = getService('supertest'); + + describe('getTile', () => { + it('should validate params', async () => { + await supertest + .get( + `/api/maps/mvt/getTile?x=15&y=11&z=5&geometryFieldName=coordinates&index=logstash*&requestBody=(_source:(includes:!(coordinates)),docvalue_fields:!(),query:(bool:(filter:!((match_all:())),must:!(),must_not:!(),should:!())),script_fields:(),size:10000,stored_fields:!(coordinates))` + ) + .set('kbn-xsrf', 'kibana') + .expect(200); + }); + + it('should not validate when required params are missing', async () => { + await supertest + .get( + `/api/maps/mvt/getTile?&index=logstash*&requestBody=(_source:(includes:!(coordinates)),docvalue_fields:!(),query:(bool:(filter:!((match_all:())),must:!(),must_not:!(),should:!())),script_fields:(),size:10000,stored_fields:!(coordinates))` + ) + .set('kbn-xsrf', 'kibana') + .expect(400); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/maps/index.js b/x-pack/test/api_integration/apis/maps/index.js index f9dff19229645a..6c213380dd64e1 100644 --- a/x-pack/test/api_integration/apis/maps/index.js +++ b/x-pack/test/api_integration/apis/maps/index.js @@ -16,6 +16,7 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./fonts_api')); loadTestFile(require.resolve('./index_settings')); loadTestFile(require.resolve('./migrations')); + loadTestFile(require.resolve('./get_tile')); }); }); } diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index 4bbe38367d0a21..ef8b4ad4c0f190 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -46,6 +46,7 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./es_geo_grid_source')); loadTestFile(require.resolve('./es_pew_pew_source')); loadTestFile(require.resolve('./joins')); + loadTestFile(require.resolve('./mvt_scaling')); loadTestFile(require.resolve('./add_layer_panel')); loadTestFile(require.resolve('./import_geojson')); loadTestFile(require.resolve('./layer_errors')); diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index e447996a08dfe1..1139ae204aefd2 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -5,7 +5,6 @@ */ import expect from '@kbn/expect'; -import { set } from '@elastic/safer-lodash-set'; import { MAPBOX_STYLES } from './mapbox_styles'; @@ -21,6 +20,7 @@ const VECTOR_SOURCE_ID = 'n1t6f'; const CIRCLE_STYLE_LAYER_INDEX = 0; const FILL_STYLE_LAYER_INDEX = 2; const LINE_STYLE_LAYER_INDEX = 3; +const TOO_MANY_FEATURES_LAYER_INDEX = 4; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); @@ -87,28 +87,32 @@ export default function ({ getPageObjects, getService }) { }); }); - it('should style fills, points and lines independently', async () => { + it('should style fills, points, lines, and bounding-boxes independently', async () => { const mapboxStyle = await PageObjects.maps.getMapboxStyle(); const layersForVectorSource = mapboxStyle.layers.filter((mbLayer) => { return mbLayer.id.startsWith(VECTOR_SOURCE_ID); }); - // Color is dynamically obtained from eui source lib - const dynamicColor = - layersForVectorSource[CIRCLE_STYLE_LAYER_INDEX].paint['circle-stroke-color']; - //circle layer for points - expect(layersForVectorSource[CIRCLE_STYLE_LAYER_INDEX]).to.eql( - set(MAPBOX_STYLES.POINT_LAYER, 'paint.circle-stroke-color', dynamicColor) - ); + expect(layersForVectorSource[CIRCLE_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.POINT_LAYER); //fill layer expect(layersForVectorSource[FILL_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.FILL_LAYER); //line layer for borders - expect(layersForVectorSource[LINE_STYLE_LAYER_INDEX]).to.eql( - set(MAPBOX_STYLES.LINE_LAYER, 'paint.line-color', dynamicColor) - ); + expect(layersForVectorSource[LINE_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.LINE_LAYER); + + //Too many features layer (this is a static style config) + expect(layersForVectorSource[TOO_MANY_FEATURES_LAYER_INDEX]).to.eql({ + id: 'n1t6f_toomanyfeatures', + type: 'fill', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: ['==', ['get', '__kbn_too_many_features__'], true], + layout: { visibility: 'visible' }, + paint: { 'fill-pattern': '__kbn_too_many_features_image_id__', 'fill-opacity': 0.75 }, + }); }); it('should flag only the joined features as visible', async () => { diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/mapbox_styles.js index 744eb4ac74bf65..78720fa1689ec6 100644 --- a/x-pack/test/functional/apps/maps/mapbox_styles.js +++ b/x-pack/test/functional/apps/maps/mapbox_styles.js @@ -14,7 +14,11 @@ export const MAPBOX_STYLES = { filter: [ 'all', ['==', ['get', '__kbn_isvisibleduetojoin__'], true], - ['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']], + [ + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + ['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']], + ], ], layout: { visibility: 'visible' }, paint: { @@ -84,7 +88,11 @@ export const MAPBOX_STYLES = { filter: [ 'all', ['==', ['get', '__kbn_isvisibleduetojoin__'], true], - ['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']], + [ + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + ['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']], + ], ], layout: { visibility: 'visible' }, paint: { @@ -151,20 +159,18 @@ export const MAPBOX_STYLES = { 'all', ['==', ['get', '__kbn_isvisibleduetojoin__'], true], [ - 'any', - ['==', ['geometry-type'], 'Polygon'], - ['==', ['geometry-type'], 'MultiPolygon'], - ['==', ['geometry-type'], 'LineString'], - ['==', ['geometry-type'], 'MultiLineString'], + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + [ + 'any', + ['==', ['geometry-type'], 'Polygon'], + ['==', ['geometry-type'], 'MultiPolygon'], + ['==', ['geometry-type'], 'LineString'], + ['==', ['geometry-type'], 'MultiLineString'], + ], ], ], - layout: { - visibility: 'visible', - }, - paint: { - /* 'line-color': '' */ // Obtained dynamically - 'line-opacity': 0.75, - 'line-width': 1, - }, + layout: { visibility: 'visible' }, + paint: { 'line-color': '#41937c', 'line-opacity': 0.75, 'line-width': 1 }, }, }; diff --git a/x-pack/test/functional/apps/maps/mvt_scaling.js b/x-pack/test/functional/apps/maps/mvt_scaling.js new file mode 100644 index 00000000000000..e50b72658fb435 --- /dev/null +++ b/x-pack/test/functional/apps/maps/mvt_scaling.js @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +const VECTOR_SOURCE_ID = 'caffa63a-ebfb-466d-8ff6-d797975b88ab'; + +export default function ({ getPageObjects, getService }) { + const PageObjects = getPageObjects(['maps']); + const inspector = getService('inspector'); + + describe('mvt geoshape layer', () => { + before(async () => { + await PageObjects.maps.loadSavedMap('geo_shape_mvt'); + }); + + after(async () => { + await inspector.close(); + }); + + it('should render with mvt-source', async () => { + const mapboxStyle = await PageObjects.maps.getMapboxStyle(); + + //Source should be correct + expect(mapboxStyle.sources[VECTOR_SOURCE_ID].tiles[0]).to.equal( + '/api/maps/mvt/getTile?x={x}&y={y}&z={z}&geometryFieldName=geometry&index=geo_shapes*&requestBody=(_source:(includes:!(geometry,prop1)),docvalue_fields:!(prop1),query:(bool:(filter:!((match_all:())),must:!(),must_not:!(),should:!())),script_fields:(),size:10000,stored_fields:!(geometry,prop1))' + ); + + //Should correctly load meta for style-rule (sigma is set to 1, opacity to 1) + const fillLayer = mapboxStyle.layers.find((layer) => layer.id === VECTOR_SOURCE_ID + '_fill'); + expect(fillLayer.paint).to.eql({ + 'fill-color': [ + 'interpolate', + ['linear'], + [ + 'coalesce', + [ + 'case', + ['==', ['get', 'prop1'], null], + 0.3819660112501051, + [ + 'max', + ['min', ['to-number', ['get', 'prop1']], 3.618033988749895], + 1.381966011250105, + ], + ], + 0.3819660112501051, + ], + 0.3819660112501051, + 'rgba(0,0,0,0)', + 1.381966011250105, + '#ecf1f7', + 1.6614745084375788, + '#d9e3ef', + 1.9409830056250525, + '#c5d5e7', + 2.2204915028125263, + '#b2c7df', + 2.5, + '#9eb9d8', + 2.7795084971874737, + '#8bacd0', + 3.0590169943749475, + '#769fc8', + 3.338525491562421, + '#6092c0', + ], + 'fill-opacity': 1, + }); + }); + }); +} diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index 0f1fd3c09d7065..f756d73484198f 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -1008,6 +1008,40 @@ } } +{ + "type": "doc", + "value": { + "id": "map:bff99716-e3dc-11ea-87d0-0242ac130003", + "index": ".kibana", + "source": { + "map" : { + "description":"shapes with mvt scaling", + "layerListJSON":"[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"76b9fc1d-1e8a-4d2f-9f9e-6ba2b19f24bb\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"},{\"sourceDescriptor\":{\"geoField\":\"geometry\",\"filterByMapBounds\":true,\"scalingType\":\"MVT\",\"topHitsSize\":1,\"id\":\"97f8555e-8db0-4bd8-8b18-22e32f468667\",\"type\":\"ES_SEARCH\",\"tooltipProperties\":[],\"sortField\":\"\",\"sortOrder\":\"desc\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"id\":\"caffa63a-ebfb-466d-8ff6-d797975b88ab\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"colorCategory\":\"palette_0\",\"field\":{\"name\":\"prop1\",\"origin\":\"source\"},\"fieldMetaOptions\":{\"isEnabled\":true,\"sigma\":1},\"type\":\"ORDINAL\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}}},\"isTimeAware\":true},\"type\":\"TILED_VECTOR\",\"joins\":[]}]", + "mapStateJSON":"{\"zoom\":3.75,\"center\":{\"lon\":80.01106,\"lat\":3.65009},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", + "title":"geo_shape_mvt", + "uiStateJSON":"{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" + }, + "type" : "map", + "references" : [ + { + "id":"561253e0-f731-11e8-8487-11b9dd924f96", + "name":"layer_1_source_index_pattern", + "type":"index-pattern" + } + ], + "migrationVersion" : { + "map" : "7.9.0" + }, + "updated_at" : "2020-08-10T18:27:39.805Z" + } + } +} + + + + + + { "type": "doc", "value": { From 1ae2a145fbf58fe07ecac030df727447f1377f4e Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Tue, 1 Sep 2020 14:34:45 -0400 Subject: [PATCH 154/216] [Dashboard First] Library Notification (#76122) Added a notification to show when an embeddable is saved to the Visualize Library --- .../public/application/actions/index.ts | 5 + .../library_notification_action.test.tsx | 100 ++++++++++++++++++ .../actions/library_notification_action.tsx | 89 ++++++++++++++++ src/plugins/dashboard/public/plugin.tsx | 17 ++- .../lib/panel/panel_header/panel_header.tsx | 5 +- 5 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx create mode 100644 src/plugins/dashboard/public/application/actions/library_notification_action.tsx diff --git a/src/plugins/dashboard/public/application/actions/index.ts b/src/plugins/dashboard/public/application/actions/index.ts index 4343a3409b6967..cd32c2025456f5 100644 --- a/src/plugins/dashboard/public/application/actions/index.ts +++ b/src/plugins/dashboard/public/application/actions/index.ts @@ -42,3 +42,8 @@ export { UnlinkFromLibraryActionContext, ACTION_UNLINK_FROM_LIBRARY, } from './unlink_from_library_action'; +export { + LibraryNotificationActionContext, + LibraryNotificationAction, + ACTION_LIBRARY_NOTIFICATION, +} from './library_notification_action'; diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx new file mode 100644 index 00000000000000..385f6f14ba94ce --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx @@ -0,0 +1,100 @@ +/* + * 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 { isErrorEmbeddable, ReferenceOrValueEmbeddable } from '../../embeddable_plugin'; +import { DashboardContainer } from '../embeddable'; +import { getSampleDashboardInput } from '../test_helpers'; +import { + CONTACT_CARD_EMBEDDABLE, + ContactCardEmbeddableFactory, + ContactCardEmbeddable, + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, +} from '../../embeddable_plugin_test_samples'; +import { coreMock } from '../../../../../core/public/mocks'; +import { CoreStart } from 'kibana/public'; +import { LibraryNotificationAction } from '.'; +import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; +import { ViewMode } from '../../../../embeddable/public'; + +const { setup, doStart } = embeddablePluginMock.createInstance(); +setup.registerEmbeddableFactory( + CONTACT_CARD_EMBEDDABLE, + new ContactCardEmbeddableFactory((() => null) as any, {} as any) +); +const start = doStart(); + +let container: DashboardContainer; +let embeddable: ContactCardEmbeddable & ReferenceOrValueEmbeddable; +let coreStart: CoreStart; +beforeEach(async () => { + coreStart = coreMock.createStart(); + + const containerOptions = { + ExitFullScreenButton: () => null, + SavedObjectFinder: () => null, + application: {} as any, + embeddable: start, + inspector: {} as any, + notifications: {} as any, + overlays: coreStart.overlays, + savedObjectMetaData: {} as any, + uiActions: {} as any, + }; + + container = new DashboardContainer(getSampleDashboardInput(), containerOptions); + + const contactCardEmbeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Kibanana', + }); + + if (isErrorEmbeddable(contactCardEmbeddable)) { + throw new Error('Failed to create embeddable'); + } + embeddable = embeddablePluginMock.mockRefOrValEmbeddable< + ContactCardEmbeddable, + ContactCardEmbeddableInput + >(contactCardEmbeddable, { + mockedByReferenceInput: { savedObjectId: 'testSavedObjectId', id: contactCardEmbeddable.id }, + mockedByValueInput: { firstName: 'Kibanana', id: contactCardEmbeddable.id }, + }); + embeddable.updateInput({ viewMode: ViewMode.EDIT }); +}); + +test('Notification is shown when embeddable on dashboard has reference type input', async () => { + const action = new LibraryNotificationAction(); + embeddable.updateInput(await embeddable.getInputAsRefType()); + expect(await action.isCompatible({ embeddable })).toBe(true); +}); + +test('Notification is not shown when embeddable input is by value', async () => { + const action = new LibraryNotificationAction(); + embeddable.updateInput(await embeddable.getInputAsValueType()); + expect(await action.isCompatible({ embeddable })).toBe(false); +}); + +test('Notification is not shown when view mode is set to view', async () => { + const action = new LibraryNotificationAction(); + embeddable.updateInput(await embeddable.getInputAsRefType()); + embeddable.updateInput({ viewMode: ViewMode.VIEW }); + expect(await action.isCompatible({ embeddable })).toBe(false); +}); diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx new file mode 100644 index 00000000000000..974b55275ccc1d --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx @@ -0,0 +1,89 @@ +/* + * 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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiBadge } from '@elastic/eui'; +import { IEmbeddable, ViewMode, isReferenceOrValueEmbeddable } from '../../embeddable_plugin'; +import { ActionByType, IncompatibleActionError } from '../../ui_actions_plugin'; +import { reactToUiComponent } from '../../../../kibana_react/public'; + +export const ACTION_LIBRARY_NOTIFICATION = 'ACTION_LIBRARY_NOTIFICATION'; + +export interface LibraryNotificationActionContext { + embeddable: IEmbeddable; +} + +export class LibraryNotificationAction implements ActionByType { + public readonly id = ACTION_LIBRARY_NOTIFICATION; + public readonly type = ACTION_LIBRARY_NOTIFICATION; + public readonly order = 1; + + private displayName = i18n.translate('dashboard.panel.LibraryNotification', { + defaultMessage: 'Library', + }); + + private icon = 'folderCheck'; + + public readonly MenuItem = reactToUiComponent(() => ( + + {this.displayName} + + )); + + public getDisplayName({ embeddable }: LibraryNotificationActionContext) { + if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { + throw new IncompatibleActionError(); + } + return this.displayName; + } + + public getIconType({ embeddable }: LibraryNotificationActionContext) { + if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { + throw new IncompatibleActionError(); + } + return this.icon; + } + + public getDisplayNameTooltip = ({ embeddable }: LibraryNotificationActionContext) => { + if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { + throw new IncompatibleActionError(); + } + return i18n.translate('dashboard.panel.libraryNotification.toolTip', { + defaultMessage: + 'This panel is linked to a Library item. Editing the panel might affect other dashboards.', + }); + }; + + public isCompatible = async ({ embeddable }: LibraryNotificationActionContext) => { + return ( + embeddable.getInput()?.viewMode !== ViewMode.VIEW && + isReferenceOrValueEmbeddable(embeddable) && + embeddable.inputIsRefType(embeddable.getInput()) + ); + }; + + public execute = async () => {}; +} diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 8b9b92faf90316..3df52f4e7a2054 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -40,6 +40,7 @@ import { EmbeddableStart, SavedObjectEmbeddableInput, EmbeddableInput, + PANEL_NOTIFICATION_TRIGGER, } from '../../embeddable/public'; import { DataPublicPluginSetup, DataPublicPluginStart, esFilters } from '../../data/public'; import { SharePluginSetup, SharePluginStart, UrlGeneratorContract } from '../../share/public'; @@ -83,6 +84,12 @@ import { ACTION_UNLINK_FROM_LIBRARY, UnlinkFromLibraryActionContext, UnlinkFromLibraryAction, + ACTION_ADD_TO_LIBRARY, + AddToLibraryActionContext, + AddToLibraryAction, + ACTION_LIBRARY_NOTIFICATION, + LibraryNotificationActionContext, + LibraryNotificationAction, } from './application'; import { createDashboardUrlGenerator, @@ -95,11 +102,6 @@ import { addEmbeddableToDashboardUrl } from './url_utils/url_helper'; import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; import { UrlGeneratorState } from '../../share/public'; import { AttributeService } from '.'; -import { - AddToLibraryAction, - ACTION_ADD_TO_LIBRARY, - AddToLibraryActionContext, -} from './application/actions/add_to_library_action'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -162,6 +164,7 @@ declare module '../../../plugins/ui_actions/public' { [ACTION_CLONE_PANEL]: ClonePanelActionContext; [ACTION_ADD_TO_LIBRARY]: AddToLibraryActionContext; [ACTION_UNLINK_FROM_LIBRARY]: UnlinkFromLibraryActionContext; + [ACTION_LIBRARY_NOTIFICATION]: LibraryNotificationActionContext; } } @@ -437,6 +440,10 @@ export class DashboardPlugin const unlinkFromLibraryAction = new UnlinkFromLibraryAction(); uiActions.registerAction(unlinkFromLibraryAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, unlinkFromLibraryAction.id); + + const libraryNotificationAction = new LibraryNotificationAction(); + uiActions.registerAction(libraryNotificationAction); + uiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, libraryNotificationAction.id); } const savedDashboardLoader = createSavedDashboardLoader({ diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx index 5d7daaa7217ed7..f3c4cae720193c 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx @@ -31,6 +31,7 @@ import { Action } from 'src/plugins/ui_actions/public'; import { PanelOptionsMenu } from './panel_options_menu'; import { IEmbeddable } from '../../embeddables'; import { EmbeddableContext, panelBadgeTrigger, panelNotificationTrigger } from '../../triggers'; +import { uiToReactComponent } from '../../../../../kibana_react/public'; export interface PanelHeaderProps { title?: string; @@ -65,7 +66,9 @@ function renderNotifications( return notifications.map((notification) => { const context = { embeddable }; - let badge = ( + let badge = notification.MenuItem ? ( + React.createElement(uiToReactComponent(notification.MenuItem)) + ) : ( Date: Tue, 1 Sep 2020 21:30:45 +0200 Subject: [PATCH 155/216] [Detections Engine] Add Alert actions to the Timeline (#73228) --- .../security_solution/public/app/app.tsx | 6 +- .../public/app/home/index.tsx | 7 +- .../components/alerts_viewer/alerts_table.tsx | 5 +- .../alerts_viewer/histogram_configs.ts | 4 +- .../common/components/alerts_viewer/index.tsx | 4 +- .../events_viewer/events_viewer.test.tsx | 42 -- .../events_viewer/events_viewer.tsx | 7 +- .../common/components/events_viewer/index.tsx | 7 +- .../exceptions/add_exception_modal/index.tsx | 6 +- .../components/matrix_histogram/types.ts | 2 +- .../common/components/url_state/helpers.ts | 3 +- .../histogram_configs.ts | 4 +- .../public/common/mock/timeline_results.ts | 4 +- .../components/alerts_table/actions.test.tsx | 14 +- .../components/alerts_table/actions.tsx | 16 +- .../alerts_table/default_config.tsx | 229 +-------- .../components/alerts_table/index.test.tsx | 2 - .../components/alerts_table/index.tsx | 204 +------- .../timeline_actions/alert_context_menu.tsx | 484 ++++++++++++++++++ .../investigate_in_timeline_action.tsx | 85 +++ .../components/alerts_table/translations.ts | 7 + .../components/alerts_table/types.ts | 1 - .../detections/components/user_info/index.tsx | 2 +- .../detection_engine.test.tsx | 4 +- .../detection_engine/detection_engine.tsx | 22 +- .../pages/detection_engine/index.test.tsx | 4 +- .../pages/detection_engine/index.tsx | 43 +- .../rules/create/index.test.tsx | 4 +- .../detection_engine/rules/create/index.tsx | 18 +- .../rules/details/index.test.tsx | 4 +- .../detection_engine/rules/details/index.tsx | 22 +- .../rules/edit/index.test.tsx | 4 +- .../detection_engine/rules/edit/index.tsx | 18 +- .../detection_engine/rules/index.test.tsx | 4 +- .../pages/detection_engine/rules/index.tsx | 20 +- .../public/detections/routes.tsx | 13 +- .../public/graphql/introspection.json | 8 + .../security_solution/public/graphql/types.ts | 4 + .../authentications_query_tab_body.tsx | 4 +- .../navigation/events_query_tab_body.tsx | 16 +- .../pages/navigation/dns_query_tab_body.tsx | 6 +- .../components/alerts_by_category/index.tsx | 4 +- .../components/events_by_dataset/index.tsx | 4 +- .../fields_browser/field_browser.tsx | 10 +- .../components/fields_browser/header.tsx | 10 +- .../components/fields_browser/index.tsx | 2 - .../components/manage_timeline/index.test.tsx | 39 +- .../components/manage_timeline/index.tsx | 56 +- .../components/open_timeline/helpers.test.ts | 26 +- .../components/open_timeline/helpers.ts | 13 +- .../components/open_timeline/index.tsx | 5 +- .../body/actions/action_icon_item.tsx | 55 ++ .../body/actions/add_note_icon_item.tsx | 59 +++ .../timeline/body/actions/index.test.tsx | 228 --------- .../timeline/body/actions/index.tsx | 227 +++----- .../body/actions/pin_event_action.tsx | 58 +++ .../timeline/body/column_headers/index.tsx | 1 - .../body/events/event_column_view.test.tsx | 117 +++++ .../body/events/event_column_view.tsx | 246 ++++----- .../components/timeline/body/events/index.tsx | 6 +- .../timeline/body/events/stateful_event.tsx | 14 +- .../timeline/body/{helpers.ts => helpers.tsx} | 69 +-- .../components/timeline/body/index.test.tsx | 3 +- .../components/timeline/body/index.tsx | 35 +- .../timeline/body/stateful_body.tsx | 10 +- .../timeline/properties/helpers.tsx | 35 +- .../properties/new_template_timeline.tsx | 4 +- .../components/timeline/timeline.tsx | 11 +- .../timelines/containers/index.gql_query.ts | 1 + .../public/timelines/pages/timelines_page.tsx | 4 +- .../timeline/epic_local_storage.test.tsx | 4 +- .../server/graphql/ecs/schema.gql.ts | 1 + .../security_solution/server/graphql/types.ts | 9 + .../server/lib/ecs_fields/index.ts | 1 + .../routes/__mocks__/import_timelines.ts | 20 +- 75 files changed, 1374 insertions(+), 1376 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx rename x-pack/plugins/security_solution/public/timelines/components/timeline/body/{helpers.ts => helpers.tsx} (67%) diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx index b5e952b0ffa8ee..b4e9ba3dd7a716 100644 --- a/x-pack/plugins/security_solution/public/app/app.tsx +++ b/x-pack/plugins/security_solution/public/app/app.tsx @@ -15,6 +15,7 @@ import { EuiErrorBoundary } from '@elastic/eui'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { ManageUserInfo } from '../detections/components/user_info'; import { DEFAULT_DARK_MODE, APP_NAME } from '../../common/constants'; import { ErrorToastDispatcher } from '../common/components/error_toast_dispatcher'; import { MlCapabilitiesProvider } from '../common/components/ml/permissions/ml_capabilities_provider'; @@ -28,6 +29,7 @@ import { ManageGlobalTimeline } from '../timelines/components/manage_timeline'; import { StartServices } from '../types'; import { PageRouter } from './routes'; import { ManageSource } from '../common/containers/sourcerer'; + interface StartAppComponent extends AppFrontendLibs { children: React.ReactNode; history: History; @@ -57,7 +59,9 @@ const StartAppComponent: FC = ({ children, apolloClient, hist - {children} + + {children} + diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 7c287646ba7ac5..b48ae4e6e2d75c 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -7,6 +7,7 @@ import React, { useMemo } from 'react'; import styled from 'styled-components'; +import { TimelineId } from '../../../common/types/timeline'; import { DragDropContextWrapper } from '../../common/components/drag_and_drop/drag_drop_context_wrapper'; import { Flyout } from '../../timelines/components/flyout'; import { HeaderGlobal } from '../../common/components/header_global'; @@ -17,6 +18,7 @@ import { useWithSource } from '../../common/containers/source'; import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline'; import { navTabs } from './home_navigations'; import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index'; +import { useUserInfo } from '../../detections/components/user_info'; const SecuritySolutionAppWrapper = styled.div` display: flex; @@ -52,6 +54,9 @@ const HomePageComponent: React.FC = ({ children }) => { const [showTimeline] = useShowTimeline(); const { browserFields, indexPattern, indicesExist } = useWithSource('default', indexToAdd); + // side effect: this will attempt to create the signals index if it doesn't exist + useUserInfo(); + return ( @@ -62,7 +67,7 @@ const HomePageComponent: React.FC = ({ children }) => { {indicesExist && showTimeline && ( <> - + )} diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index 841a1ef09ede6c..00879ace040b91 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -5,14 +5,12 @@ */ import React, { useEffect, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; import { Filter } from '../../../../../../../src/plugins/data/public'; import { TimelineIdLiteral } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../events_viewer'; import { alertsDefaultModel } from './default_headers'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; -import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers'; import * as i18n from './translations'; import { useKibana } from '../../lib/kibana'; @@ -68,7 +66,6 @@ const AlertsTableComponent: React.FC = ({ startDate, pageFilters = [], }) => { - const dispatch = useDispatch(); const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]); const { filterManager } = useKibana().services.data.query; const { initializeTimeline } = useManageTimeline(); @@ -80,12 +77,12 @@ const AlertsTableComponent: React.FC = ({ filterManager, defaultModel: alertsDefaultModel, footerText: i18n.TOTAL_COUNT_OF_ALERTS, - timelineRowActions: () => [getInvestigateInResolverAction({ dispatch, timelineId })], title: i18n.ALERTS_TABLE_TITLE, unit: i18n.UNIT, }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + return ( o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[1], errorMessage: i18n.ERROR_FETCHING_ALERTS_DATA, diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx index 633135d63ac33a..de9a8b32f1f90b 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx @@ -15,7 +15,7 @@ import * as i18n from './translations'; import { useUiSetting$ } from '../../lib/kibana'; import { MatrixHistogramContainer } from '../matrix_histogram'; import { histogramConfigs } from './histogram_configs'; -import { MatrixHisrogramConfigs } from '../matrix_histogram/types'; +import { MatrixHistogramConfigs } from '../matrix_histogram/types'; const ID = 'alertsOverTimeQuery'; export const AlertsView = ({ @@ -38,7 +38,7 @@ export const AlertsView = ({ [] ); const { globalFullScreen } = useFullScreen(); - const alertsHistogramConfigs: MatrixHisrogramConfigs = useMemo( + const alertsHistogramConfigs: MatrixHistogramConfigs = useMemo( () => ({ ...histogramConfigs, subtitle: getSubtitle, diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 6f77d15913d073..833688ae579932 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -50,10 +50,6 @@ const utilityBar = (refetch: inputsModel.Refetch, totalCount: number) => (
); -const exceptionsModal = (refetch: inputsModel.Refetch) => ( -
-); - const eventsViewerDefaultProps = { browserFields: {}, columns: [], @@ -464,42 +460,4 @@ describe('EventsViewer', () => { }); }); }); - - describe('exceptions modal', () => { - test('it renders exception modal if "exceptionsModal" callback exists', async () => { - const wrapper = mount( - - - - - - ); - - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="mock-exceptions-modal"]`).exists()).toBeTruthy(); - }); - }); - - test('it does not render exception modal if "exceptionModal" callback does not exist', async () => { - const wrapper = mount( - - - - - - ); - - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="mock-exceptions-modal"]`).exists()).toBeFalsy(); - }); - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index ebda64efabf65b..3d193856a8ae43 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -109,7 +109,6 @@ interface Props { utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; // If truthy, the graph viewer (Resolver) is showing graphEventId: string | undefined; - exceptionsModal?: (refetch: inputsModel.Refetch) => React.ReactNode; } const EventsViewerComponent: React.FC = ({ @@ -135,7 +134,6 @@ const EventsViewerComponent: React.FC = ({ toggleColumn, utilityBar, graphEventId, - exceptionsModal, }) => { const { globalFullScreen } = useFullScreen(); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; @@ -261,7 +259,6 @@ const EventsViewerComponent: React.FC = ({ )} - {exceptionsModal && exceptionsModal(refetch)} {utilityBar && !resolverIsShowing(graphEventId) && ( {utilityBar?.(refetch, totalCountMinusDeleted)} )} @@ -280,6 +277,7 @@ const EventsViewerComponent: React.FC = ({ docValueFields={docValueFields} id={id} isEventViewer={true} + refetch={refetch} sort={sort} toggleColumn={toggleColumn} /> @@ -338,6 +336,5 @@ export const EventsViewer = React.memo( prevProps.start === nextProps.start && prevProps.sort === nextProps.sort && prevProps.utilityBar === nextProps.utilityBar && - prevProps.graphEventId === nextProps.graphEventId && - prevProps.exceptionsModal === nextProps.exceptionsModal + prevProps.graphEventId === nextProps.graphEventId ); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index ec56a3a1bd8d3f..e4520dab4626a8 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -43,7 +43,6 @@ export interface OwnProps { headerFilterGroup?: React.ReactNode; pageFilters?: Filter[]; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; - exceptionsModal?: (refetch: inputsModel.Refetch) => React.ReactNode; } type Props = OwnProps & PropsFromRedux; @@ -75,7 +74,6 @@ const StatefulEventsViewerComponent: React.FC = ({ utilityBar, // If truthy, the graph viewer (Resolver) is showing graphEventId, - exceptionsModal, }) => { const [ { docValueFields, browserFields, indexPatterns, isLoading: isLoadingIndexPattern }, @@ -158,7 +156,6 @@ const StatefulEventsViewerComponent: React.FC = ({ toggleColumn={toggleColumn} utilityBar={utilityBar} graphEventId={graphEventId} - exceptionsModal={exceptionsModal} /> @@ -223,7 +220,6 @@ type PropsFromRedux = ConnectedProps; export const StatefulEventsViewer = connector( React.memo( StatefulEventsViewerComponent, - // eslint-disable-next-line complexity (prevProps, nextProps) => prevProps.id === nextProps.id && deepEqual(prevProps.columns, nextProps.columns) && @@ -244,7 +240,6 @@ export const StatefulEventsViewer = connector( prevProps.showCheckboxes === nextProps.showCheckboxes && prevProps.start === nextProps.start && prevProps.utilityBar === nextProps.utilityBar && - prevProps.graphEventId === nextProps.graphEventId && - prevProps.exceptionsModal === nextProps.exceptionsModal + prevProps.graphEventId === nextProps.graphEventId ) ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 21f82c6ab4c986..c46eb1b6b59cc8 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -63,7 +63,7 @@ export interface AddExceptionModalBaseProps { export interface AddExceptionModalProps extends AddExceptionModalBaseProps { onCancel: () => void; - onConfirm: (didCloseAlert: boolean) => void; + onConfirm: (didCloseAlert: boolean, didBulkCloseAlert: boolean) => void; onRuleChange?: () => void; alertStatus?: Status; } @@ -137,8 +137,8 @@ export const AddExceptionModal = memo(function AddExceptionModal({ ); const onSuccess = useCallback(() => { addSuccess(i18n.ADD_EXCEPTION_SUCCESS); - onConfirm(shouldCloseAlert); - }, [addSuccess, onConfirm, shouldCloseAlert]); + onConfirm(shouldCloseAlert, shouldBulkCloseAlert); + }, [addSuccess, onConfirm, shouldBulkCloseAlert, shouldCloseAlert]); const [{ isLoading: addExceptionIsLoading }, addOrUpdateExceptionItems] = useAddOrUpdateException( { diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index a859b0dd392310..d471b5ae9bed17 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -25,7 +25,7 @@ export interface MatrixHistogramOption { export type GetSubTitle = (count: number) => string; export type GetTitle = (matrixHistogramOption: MatrixHistogramOption) => string; -export interface MatrixHisrogramConfigs { +export interface MatrixHistogramConfigs { defaultStackByOption: MatrixHistogramOption; errorMessage: string; hideHistogramIfEmpty?: boolean; diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts index 5e40cd00fa69ef..6052913b4183b1 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts @@ -12,6 +12,7 @@ import * as H from 'history'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; import { url } from '../../../../../../../src/plugins/kibana_utils/public'; +import { TimelineId } from '../../../../common/types/timeline'; import { SecurityPageName } from '../../../app/types'; import { inputsSelectors, State } from '../../store'; import { UrlInputsModel } from '../../store/inputs/model'; @@ -122,7 +123,7 @@ export const makeMapStateToProps = () => { const { linkTo: globalLinkTo, timerange: globalTimerange } = inputState.global; const { linkTo: timelineLinkTo, timerange: timelineTimerange } = inputState.timeline; - const flyoutTimeline = getTimeline(state, 'timeline-1'); + const flyoutTimeline = getTimeline(state, TimelineId.active); const timeline = flyoutTimeline != null ? { diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts index b32919f4868dcc..6a05f97da2fefa 100644 --- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts +++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts @@ -6,7 +6,7 @@ import * as i18n from './translations'; import { MatrixHistogramOption, - MatrixHisrogramConfigs, + MatrixHistogramConfigs, } from '../../../components/matrix_histogram/types'; import { HistogramType } from '../../../../graphql/types'; @@ -19,7 +19,7 @@ export const anomaliesStackByOptions: MatrixHistogramOption[] = [ const DEFAULT_STACK_BY = i18n.ANOMALIES_STACK_BY_JOB_ID; -export const histogramConfigs: MatrixHisrogramConfigs = { +export const histogramConfigs: MatrixHistogramConfigs = { defaultStackByOption: anomaliesStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? anomaliesStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_ANOMALIES_DATA, diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index ab9f12a67fe897..26013915315afb 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -5,7 +5,7 @@ */ import { FilterStateStore } from '../../../../../../src/plugins/data/common/es_query/filters/meta_filter'; -import { TimelineType, TimelineStatus } from '../../../common/types/timeline'; +import { TimelineId, TimelineType, TimelineStatus } from '../../../common/types/timeline'; import { OpenTimelineResult } from '../../timelines/components/open_timeline/types'; import { @@ -2227,7 +2227,7 @@ export const defaultTimelineProps: CreateTimelineProps = { filters: [], highlightedDropAndProviderId: '', historyIds: [], - id: 'timeline-1', + id: TimelineId.active, isFavorite: false, isLive: false, isLoading: false, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index e8015f601cb184..3f95fd36b60106 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -18,7 +18,7 @@ import { } from '../../../common/mock/'; import { CreateTimeline, UpdateTimelineLoading } from './types'; import { Ecs } from '../../../graphql/types'; -import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; +import { TimelineId, TimelineType, TimelineStatus } from '../../../../common/types/timeline'; jest.mock('apollo-client'); @@ -67,7 +67,10 @@ describe('alert actions', () => { }); expect(updateTimelineIsLoading).toHaveBeenCalledTimes(1); - expect(updateTimelineIsLoading).toHaveBeenCalledWith({ id: 'timeline-1', isLoading: true }); + expect(updateTimelineIsLoading).toHaveBeenCalledWith({ + id: TimelineId.active, + isLoading: true, + }); }); test('it invokes createTimeline with designated timeline template if "timelineTemplate" exists', async () => { @@ -313,9 +316,12 @@ describe('alert actions', () => { updateTimelineIsLoading, }); - expect(updateTimelineIsLoading).toHaveBeenCalledWith({ id: 'timeline-1', isLoading: true }); expect(updateTimelineIsLoading).toHaveBeenCalledWith({ - id: 'timeline-1', + id: TimelineId.active, + isLoading: true, + }); + expect(updateTimelineIsLoading).toHaveBeenCalledWith({ + id: TimelineId.active, isLoading: false, }); expect(createTimeline).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index 34c0537a6d7d24..3545bfd91e553d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -10,6 +10,7 @@ import dateMath from '@elastic/datemath'; import { get, getOr, isEmpty, find } from 'lodash/fp'; import moment from 'moment'; +import { TimelineId } from '../../../../common/types/timeline'; import { updateAlertStatus } from '../../containers/detection_engine/alerts/api'; import { SendAlertToTimelineActionProps, UpdateAlertStatusActionProps } from './types'; import { @@ -67,7 +68,6 @@ export const getFilterAndRuleBounds = ( export const updateAlertStatusAction = async ({ query, alertIds, - status, selectedStatus, setEventsLoading, setEventsDeleted, @@ -126,7 +126,7 @@ export const getThresholdAggregationDataProvider = ( return [ { and: [], - id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-${aggregationFieldId}-${dataProviderValue}`, + id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-${aggregationFieldId}-${dataProviderValue}`, name: ecsData.signal?.rule?.threshold.field, enabled: true, excluded: false, @@ -155,7 +155,7 @@ export const sendAlertToTimelineAction = async ({ if (timelineId !== '' && apolloClient != null) { try { - updateTimelineIsLoading({ id: 'timeline-1', isLoading: true }); + updateTimelineIsLoading({ id: TimelineId.active, isLoading: true }); const [responseTimeline, eventDataResp] = await Promise.all([ apolloClient.query({ query: oneTimelineQuery, @@ -236,7 +236,7 @@ export const sendAlertToTimelineAction = async ({ } } catch { openAlertInBasicTimeline = true; - updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); + updateTimelineIsLoading({ id: TimelineId.active, isLoading: false }); } } @@ -253,7 +253,7 @@ export const sendAlertToTimelineAction = async ({ dataProviders: [ { and: [], - id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-alert-id-${ecsData._id}`, + id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-alert-id-${ecsData._id}`, name: ecsData._id, enabled: true, excluded: false, @@ -266,7 +266,7 @@ export const sendAlertToTimelineAction = async ({ }, ...getThresholdAggregationDataProvider(ecsData, nonEcsData), ], - id: 'timeline-1', + id: TimelineId.active, dateRange: { start: from, end: to, @@ -304,7 +304,7 @@ export const sendAlertToTimelineAction = async ({ dataProviders: [ { and: [], - id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-alert-id-${ecsData._id}`, + id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-alert-id-${ecsData._id}`, name: ecsData._id, enabled: true, excluded: false, @@ -316,7 +316,7 @@ export const sendAlertToTimelineAction = async ({ }, }, ], - id: 'timeline-1', + id: TimelineId.active, dateRange: { start: from, end: to, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index ca17d331c67e58..eebabc59d93244 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -4,44 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import ApolloClient from 'apollo-client'; -import { Dispatch } from 'redux'; - -import { EuiText } from '@elastic/eui'; -import { RuleType } from '../../../../common/detection_engine/types'; -import { isMlRule } from '../../../../common/machine_learning/helpers'; import { RowRendererId } from '../../../../common/types/timeline'; -import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; -import { - TimelineRowAction, - TimelineRowActionOnClick, -} from '../../../timelines/components/timeline/body/actions'; + import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; -import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers'; import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH, } from '../../../timelines/components/timeline/body/constants'; -import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../timelines/components/timeline/helpers'; import { ColumnHeaderOptions, SubsetTimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; -import { FILTER_OPEN, FILTER_CLOSED, FILTER_IN_PROGRESS } from './alerts_filter_group'; -import { sendAlertToTimelineAction, updateAlertStatusAction } from './actions'; import * as i18n from './translations'; -import { - CreateTimeline, - SetEventsDeletedProps, - SetEventsLoadingProps, - UpdateTimelineLoading, -} from './types'; -import { Ecs, TimelineNonEcsData } from '../../../graphql/types'; -import { AddExceptionModalBaseProps } from '../../../common/components/exceptions/add_exception_modal'; -import { getMappedNonEcsValue } from '../../../common/components/exceptions/helpers'; -import { isThresholdRule } from '../../../../common/detection_engine/utils'; export const buildAlertStatusFilter = (status: Status): Filter[] => [ { @@ -189,13 +164,16 @@ export const alertsDefaultModel: SubsetTimelineModel = { export const requiredFieldsForActions = [ '@timestamp', + 'signal.status', 'signal.original_time', 'signal.rule.filters', 'signal.rule.from', 'signal.rule.language', 'signal.rule.query', + 'signal.rule.name', 'signal.rule.to', 'signal.rule.id', + 'signal.rule.index', 'signal.rule.type', 'signal.original_event.kind', 'signal.original_event.module', @@ -208,202 +186,3 @@ export const requiredFieldsForActions = [ 'host.os.family', 'event.code', ]; - -interface AlertActionArgs { - apolloClient?: ApolloClient<{}>; - canUserCRUD: boolean; - createTimeline: CreateTimeline; - dispatch: Dispatch; - ecsRowData: Ecs; - nonEcsRowData: TimelineNonEcsData[]; - hasIndexWrite: boolean; - onAlertStatusUpdateFailure: (status: Status, error: Error) => void; - onAlertStatusUpdateSuccess: (count: number, status: Status) => void; - setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; - setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; - status: Status; - timelineId: string; - updateTimelineIsLoading: UpdateTimelineLoading; - openAddExceptionModal: ({ - exceptionListType, - alertData, - ruleName, - ruleId, - }: AddExceptionModalBaseProps) => void; -} - -export const getAlertActions = ({ - apolloClient, - canUserCRUD, - createTimeline, - dispatch, - ecsRowData, - nonEcsRowData, - hasIndexWrite, - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - status, - timelineId, - updateTimelineIsLoading, - openAddExceptionModal, -}: AlertActionArgs): TimelineRowAction[] => { - const openAlertActionComponent: TimelineRowAction = { - ariaLabel: 'Open alert', - content: {i18n.ACTION_OPEN_ALERT}, - dataTestSubj: 'open-alert-status', - displayType: 'contextMenu', - id: FILTER_OPEN, - isActionDisabled: () => !canUserCRUD || !hasIndexWrite, - onClick: ({ eventId }: TimelineRowActionOnClick) => - updateAlertStatusAction({ - alertIds: [eventId], - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - status, - selectedStatus: FILTER_OPEN, - }), - width: DEFAULT_ICON_BUTTON_WIDTH, - }; - - const closeAlertActionComponent: TimelineRowAction = { - ariaLabel: 'Close alert', - content: {i18n.ACTION_CLOSE_ALERT}, - dataTestSubj: 'close-alert-status', - displayType: 'contextMenu', - id: FILTER_CLOSED, - isActionDisabled: () => !canUserCRUD || !hasIndexWrite, - onClick: ({ eventId }: TimelineRowActionOnClick) => - updateAlertStatusAction({ - alertIds: [eventId], - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - status, - selectedStatus: FILTER_CLOSED, - }), - width: DEFAULT_ICON_BUTTON_WIDTH, - }; - - const inProgressAlertActionComponent: TimelineRowAction = { - ariaLabel: 'Mark alert in progress', - content: {i18n.ACTION_IN_PROGRESS_ALERT}, - dataTestSubj: 'in-progress-alert-status', - displayType: 'contextMenu', - id: FILTER_IN_PROGRESS, - isActionDisabled: () => !canUserCRUD || !hasIndexWrite, - onClick: ({ eventId }: TimelineRowActionOnClick) => - updateAlertStatusAction({ - alertIds: [eventId], - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - status, - selectedStatus: FILTER_IN_PROGRESS, - }), - width: DEFAULT_ICON_BUTTON_WIDTH, - }; - - const isEndpointAlert = () => { - const [module] = getMappedNonEcsValue({ - data: nonEcsRowData, - fieldName: 'signal.original_event.module', - }); - const [kind] = getMappedNonEcsValue({ - data: nonEcsRowData, - fieldName: 'signal.original_event.kind', - }); - return module === 'endpoint' && kind === 'alert'; - }; - - const exceptionsAreAllowed = () => { - const ruleTypes = getMappedNonEcsValue({ - data: nonEcsRowData, - fieldName: 'signal.rule.type', - }); - const [ruleType] = ruleTypes as RuleType[]; - return !isMlRule(ruleType) && !isThresholdRule(ruleType); - }; - - return [ - { - ...getInvestigateInResolverAction({ dispatch, timelineId }), - }, - { - ariaLabel: 'Send alert to timeline', - content: i18n.ACTION_INVESTIGATE_IN_TIMELINE, - dataTestSubj: 'send-alert-to-timeline', - displayType: 'icon', - iconType: 'timeline', - id: 'sendAlertToTimeline', - onClick: ({ ecsData, data }: TimelineRowActionOnClick) => - sendAlertToTimelineAction({ - apolloClient, - createTimeline, - ecsData, - nonEcsData: data, - updateTimelineIsLoading, - }), - width: DEFAULT_ICON_BUTTON_WIDTH, - }, - // Context menu items - ...(FILTER_OPEN !== status ? [openAlertActionComponent] : []), - ...(FILTER_CLOSED !== status ? [closeAlertActionComponent] : []), - ...(FILTER_IN_PROGRESS !== status ? [inProgressAlertActionComponent] : []), - { - onClick: ({ ecsData, data }: TimelineRowActionOnClick) => { - const [ruleName] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' }); - const [ruleId] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' }); - const ruleIndices = getMappedNonEcsValue({ data, fieldName: 'signal.rule.index' }); - if (ruleId !== undefined) { - openAddExceptionModal({ - ruleName: ruleName ?? '', - ruleId, - ruleIndices: ruleIndices.length > 0 ? ruleIndices : DEFAULT_INDEX_PATTERN, - exceptionListType: 'endpoint', - alertData: { - ecsData, - nonEcsData: data, - }, - }); - } - }, - id: 'addEndpointException', - isActionDisabled: () => !canUserCRUD || !hasIndexWrite || !isEndpointAlert(), - dataTestSubj: 'add-endpoint-exception-menu-item', - ariaLabel: 'Add Endpoint Exception', - content: {i18n.ACTION_ADD_ENDPOINT_EXCEPTION}, - displayType: 'contextMenu', - }, - { - onClick: ({ ecsData, data }: TimelineRowActionOnClick) => { - const [ruleName] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' }); - const [ruleId] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' }); - const ruleIndices = getMappedNonEcsValue({ data, fieldName: 'signal.rule.index' }); - if (ruleId !== undefined) { - openAddExceptionModal({ - ruleName: ruleName ?? '', - ruleId, - ruleIndices: ruleIndices.length > 0 ? ruleIndices : DEFAULT_INDEX_PATTERN, - exceptionListType: 'detection', - alertData: { - ecsData, - nonEcsData: data, - }, - }); - } - }, - id: 'addException', - isActionDisabled: () => !canUserCRUD || !hasIndexWrite || !exceptionsAreAllowed(), - dataTestSubj: 'add-exception-menu-item', - ariaLabel: 'Add Exception', - content: {i18n.ACTION_ADD_EXCEPTION}, - displayType: 'contextMenu', - }, - ]; -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx index d5688d84e97597..be249576020370 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx @@ -40,8 +40,6 @@ describe('AlertsTableComponent', () => { clearEventsDeleted={jest.fn()} showBuildingBlockAlerts={false} onShowBuildingBlockAlertsChanged={jest.fn()} - updateTimelineIsLoading={jest.fn()} - updateTimeline={jest.fn()} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 854565ace9b4bd..63e1c8aca9082b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -7,7 +7,7 @@ import { EuiPanel, EuiLoadingContent } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { connect, ConnectedProps, useDispatch } from 'react-redux'; +import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; @@ -22,15 +22,10 @@ import { inputsSelectors, State, inputsModel } from '../../../common/store'; import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline'; import { TimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; -import { - useManageTimeline, - TimelineRowActionArgs, -} from '../../../timelines/components/manage_timeline'; -import { useApolloClient } from '../../../common/utils/apollo_context'; +import { useManageTimeline } from '../../../timelines/components/manage_timeline'; import { updateAlertStatusAction } from './actions'; import { - getAlertActions, requiredFieldsForActions, alertsDefaultModel, buildAlertStatusFilter, @@ -39,23 +34,16 @@ import { FILTER_OPEN, AlertsTableFilterGroup } from './alerts_filter_group'; import { AlertsUtilityBar } from './alerts_utility_bar'; import * as i18n from './translations'; import { - CreateTimelineProps, SetEventsDeletedProps, SetEventsLoadingProps, UpdateAlertsStatusCallback, UpdateAlertsStatusProps, } from './types'; -import { dispatchUpdateTimeline } from '../../../timelines/components/open_timeline/helpers'; import { useStateToaster, displaySuccessToast, displayErrorToast, } from '../../../common/components/toasters'; -import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers'; -import { - AddExceptionModal, - AddExceptionModalBaseProps, -} from '../../../common/components/exceptions/add_exception_modal'; interface OwnProps { timelineId: TimelineIdLiteral; @@ -72,14 +60,6 @@ interface OwnProps { type AlertsTableComponentProps = OwnProps & PropsFromRedux; -const addExceptionModalInitialState: AddExceptionModalBaseProps = { - ruleName: '', - ruleId: '', - ruleIndices: [], - exceptionListType: 'detection', - alertData: undefined, -}; - export const AlertsTableComponent: React.FC = ({ timelineId, canUserCRUD, @@ -101,30 +81,16 @@ export const AlertsTableComponent: React.FC = ({ onShowBuildingBlockAlertsChanged, signalsIndex, to, - updateTimeline, - updateTimelineIsLoading, }) => { - const dispatch = useDispatch(); - const apolloClient = useApolloClient(); - const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); - const [shouldShowAddExceptionModal, setShouldShowAddExceptionModal] = useState(false); - const [addExceptionModalState, setAddExceptionModalState] = useState( - addExceptionModalInitialState - ); const [{ browserFields, indexPatterns, isLoading: indexPatternsLoading }] = useFetchIndexPatterns( signalsIndex !== '' ? [signalsIndex] : [], 'alerts_table' ); const kibana = useKibana(); const [, dispatchToaster] = useStateToaster(); - const { - initializeTimeline, - setSelectAll, - setTimelineRowActions, - setIndexToAdd, - } = useManageTimeline(); + const { initializeTimeline, setSelectAll, setIndexToAdd } = useManageTimeline(); const getGlobalQuery = useCallback( (customFilters: Filter[]) => { @@ -149,27 +115,6 @@ export const AlertsTableComponent: React.FC = ({ [browserFields, defaultFilters, globalFilters, globalQuery, indexPatterns, kibana, to, from] ); - // Callback for creating a new timeline -- utilized by row/batch actions - const createTimelineCallback = useCallback( - ({ from: fromTimeline, timeline, to: toTimeline, ruleNote, notes }: CreateTimelineProps) => { - updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); - updateTimeline({ - duplicate: true, - forceNotes: true, - from: fromTimeline, - id: 'timeline-1', - notes, - timeline: { - ...timeline, - show: true, - }, - to: toTimeline, - ruleNote, - })(); - }, - [updateTimeline, updateTimelineIsLoading] - ); - const setEventsLoadingCallback = useCallback( ({ eventIds, isLoading }: SetEventsLoadingProps) => { setEventsLoading!({ id: timelineId, eventIds, isLoading }); @@ -220,28 +165,6 @@ export const AlertsTableComponent: React.FC = ({ [dispatchToaster] ); - const openAddExceptionModalCallback = useCallback( - ({ - ruleName, - ruleIndices, - ruleId, - exceptionListType, - alertData, - }: AddExceptionModalBaseProps) => { - if (alertData != null) { - setShouldShowAddExceptionModal(true); - setAddExceptionModalState({ - ruleName, - ruleId, - ruleIndices, - exceptionListType, - alertData, - }); - } - }, - [setShouldShowAddExceptionModal, setAddExceptionModalState] - ); - // Catches state change isSelectAllChecked->false upon user selection change to reset utility bar useEffect(() => { if (isSelectAllChecked) { @@ -297,7 +220,6 @@ export const AlertsTableComponent: React.FC = ({ ? getGlobalQuery(currentStatusFilter)?.filterQuery : undefined, alertIds: Object.keys(selectedEventIds), - status, selectedStatus, setEventsDeleted: setEventsDeletedCallback, setEventsLoading: setEventsLoadingCallback, @@ -352,42 +274,6 @@ export const AlertsTableComponent: React.FC = ({ ] ); - // Send to Timeline / Update Alert Status Actions for each table row - const additionalActions = useMemo( - () => ({ ecsData, nonEcsData }: TimelineRowActionArgs) => - getAlertActions({ - apolloClient, - canUserCRUD, - createTimeline: createTimelineCallback, - ecsRowData: ecsData, - nonEcsRowData: nonEcsData, - dispatch, - hasIndexWrite, - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted: setEventsDeletedCallback, - setEventsLoading: setEventsLoadingCallback, - status: filterGroup, - timelineId, - updateTimelineIsLoading, - openAddExceptionModal: openAddExceptionModalCallback, - }), - [ - apolloClient, - canUserCRUD, - createTimelineCallback, - dispatch, - hasIndexWrite, - filterGroup, - setEventsLoadingCallback, - setEventsDeletedCallback, - timelineId, - updateTimelineIsLoading, - onAlertStatusUpdateSuccess, - onAlertStatusUpdateFailure, - openAddExceptionModalCallback, - ] - ); const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]); const defaultFiltersMemo = useMemo(() => { if (isEmpty(defaultFilters)) { @@ -408,21 +294,12 @@ export const AlertsTableComponent: React.FC = ({ indexToAdd: defaultIndices, loadingText: i18n.LOADING_ALERTS, selectAll: false, - timelineRowActions: () => [getInvestigateInResolverAction({ dispatch, timelineId })], + queryFields: requiredFieldsForActions, title: '', }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useEffect(() => { - setTimelineRowActions({ - id: timelineId, - queryFields: requiredFieldsForActions, - timelineRowActions: additionalActions, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [additionalActions]); - useEffect(() => { setIndexToAdd({ id: timelineId, indexToAdd: defaultIndices }); }, [timelineId, defaultIndices, setIndexToAdd]); @@ -432,53 +309,6 @@ export const AlertsTableComponent: React.FC = ({ [onFilterGroupChangedCallback] ); - const closeAddExceptionModal = useCallback(() => { - setShouldShowAddExceptionModal(false); - setAddExceptionModalState(addExceptionModalInitialState); - }, [setShouldShowAddExceptionModal, setAddExceptionModalState]); - - const onAddExceptionCancel = useCallback(() => { - closeAddExceptionModal(); - }, [closeAddExceptionModal]); - - const onAddExceptionConfirm = useCallback( - (refetch: inputsModel.Refetch) => (): void => { - refetch(); - closeAddExceptionModal(); - }, - [closeAddExceptionModal] - ); - - // Callback for creating the AddExceptionModal and allowing it - // access to the refetchQuery to update the page - const exceptionModalCallback = useCallback( - (refetchQuery: inputsModel.Refetch) => { - if (shouldShowAddExceptionModal) { - return ( - - ); - } else { - return <>; - } - }, - [ - addExceptionModalState, - filterGroup, - onAddExceptionCancel, - onAddExceptionConfirm, - shouldShowAddExceptionModal, - ] - ); - if (loading || indexPatternsLoading || isEmpty(signalsIndex)) { return ( @@ -489,19 +319,16 @@ export const AlertsTableComponent: React.FC = ({ } return ( - <> - - + ); }; @@ -551,9 +378,6 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ }) => dispatch(timelineActions.setEventsDeleted({ id, eventIds, isDeleted })), clearEventsDeleted: ({ id }: { id: string }) => dispatch(timelineActions.clearEventsDeleted({ id })), - updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => - dispatch(timelineActions.updateIsLoading({ id, isLoading })), - updateTimeline: dispatchUpdateTimeline(dispatch), }); const connector = connect(makeMapStateToProps, mapDispatchToProps); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx new file mode 100644 index 00000000000000..589116c901c303 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -0,0 +1,484 @@ +/* + * 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, { useCallback, useMemo, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { + EuiText, + EuiButtonIcon, + EuiContextMenuPanel, + EuiPopover, + EuiContextMenuItem, +} from '@elastic/eui'; +import styled from 'styled-components'; + +import { TimelineId } from '../../../../../common/types/timeline'; +import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; +import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { isThresholdRule } from '../../../../../common/detection_engine/utils'; +import { RuleType } from '../../../../../common/detection_engine/types'; +import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { timelineActions } from '../../../../timelines/store/timeline'; +import { EventsTd, EventsTdContent } from '../../../../timelines/components/timeline/styles'; +import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../../timelines/components/timeline/helpers'; +import { FILTER_OPEN, FILTER_CLOSED, FILTER_IN_PROGRESS } from '../alerts_filter_group'; +import { updateAlertStatusAction } from '../actions'; +import { SetEventsDeletedProps, SetEventsLoadingProps } from '../types'; +import { Ecs, TimelineNonEcsData } from '../../../../graphql/types'; +import { + AddExceptionModal as AddExceptionModalComponent, + AddExceptionModalBaseProps, +} from '../../../../common/components/exceptions/add_exception_modal'; +import { getMappedNonEcsValue } from '../../../../common/components/exceptions/helpers'; +import * as i18n from '../translations'; +import { + useStateToaster, + displaySuccessToast, + displayErrorToast, +} from '../../../../common/components/toasters'; +import { inputsModel } from '../../../../common/store'; +import { useUserData } from '../../user_info'; + +interface AlertContextMenuProps { + disabled: boolean; + ecsRowData: Ecs; + nonEcsRowData: TimelineNonEcsData[]; + refetch: inputsModel.Refetch; + timelineId: string; +} + +const addExceptionModalInitialState: AddExceptionModalBaseProps = { + ruleName: '', + ruleId: '', + ruleIndices: [], + exceptionListType: 'detection', + alertData: undefined, +}; + +const AlertContextMenuComponent: React.FC = ({ + disabled, + ecsRowData, + nonEcsRowData, + refetch, + timelineId, +}) => { + const dispatch = useDispatch(); + const [, dispatchToaster] = useStateToaster(); + const [isPopoverOpen, setPopover] = useState(false); + const [alertStatus, setAlertStatus] = useState( + (ecsRowData.signal?.status && (ecsRowData.signal.status[0] as Status)) ?? undefined + ); + const eventId = ecsRowData._id; + + const onButtonClick = useCallback(() => { + setPopover(!isPopoverOpen); + }, [isPopoverOpen]); + + const closePopover = useCallback(() => { + setPopover(false); + }, []); + const [shouldShowAddExceptionModal, setShouldShowAddExceptionModal] = useState(false); + const [addExceptionModalState, setAddExceptionModalState] = useState( + addExceptionModalInitialState + ); + const [{ canUserCRUD, hasIndexWrite }] = useUserData(); + + const isEndpointAlert = useMemo(() => { + if (!nonEcsRowData) { + return false; + } + + const [module] = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.original_event.module', + }); + const [kind] = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.original_event.kind', + }); + return module === 'endpoint' && kind === 'alert'; + }, [nonEcsRowData]); + + const closeAddExceptionModal = useCallback(() => { + setShouldShowAddExceptionModal(false); + setAddExceptionModalState(addExceptionModalInitialState); + }, [setShouldShowAddExceptionModal, setAddExceptionModalState]); + + const onAddExceptionCancel = useCallback(() => { + closeAddExceptionModal(); + }, [closeAddExceptionModal]); + + const onAddExceptionConfirm = useCallback( + (didCloseAlert: boolean, didBulkCloseAlert) => { + closeAddExceptionModal(); + if (didCloseAlert) { + setAlertStatus('closed'); + } + if (timelineId !== TimelineId.active || didBulkCloseAlert) { + refetch(); + } + }, + [closeAddExceptionModal, timelineId, refetch] + ); + + const onAlertStatusUpdateSuccess = useCallback( + (count: number, newStatus: Status) => { + let title: string; + switch (newStatus) { + case 'closed': + title = i18n.CLOSED_ALERT_SUCCESS_TOAST(count); + break; + case 'open': + title = i18n.OPENED_ALERT_SUCCESS_TOAST(count); + break; + case 'in-progress': + title = i18n.IN_PROGRESS_ALERT_SUCCESS_TOAST(count); + } + displaySuccessToast(title, dispatchToaster); + setAlertStatus(newStatus); + }, + [dispatchToaster] + ); + + const onAlertStatusUpdateFailure = useCallback( + (newStatus: Status, error: Error) => { + let title: string; + switch (newStatus) { + case 'closed': + title = i18n.CLOSED_ALERT_FAILED_TOAST; + break; + case 'open': + title = i18n.OPENED_ALERT_FAILED_TOAST; + break; + case 'in-progress': + title = i18n.IN_PROGRESS_ALERT_FAILED_TOAST; + } + displayErrorToast(title, [error.message], dispatchToaster); + }, + [dispatchToaster] + ); + + const setEventsLoading = useCallback( + ({ eventIds, isLoading }: SetEventsLoadingProps) => { + dispatch(timelineActions.setEventsLoading({ id: timelineId, eventIds, isLoading })); + }, + [dispatch, timelineId] + ); + + const setEventsDeleted = useCallback( + ({ eventIds, isDeleted }: SetEventsDeletedProps) => { + dispatch(timelineActions.setEventsDeleted({ id: timelineId, eventIds, isDeleted })); + }, + [dispatch, timelineId] + ); + + const openAlertActionOnClick = useCallback(() => { + updateAlertStatusAction({ + alertIds: [eventId], + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + selectedStatus: FILTER_OPEN, + }); + closePopover(); + }, [ + closePopover, + eventId, + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + ]); + + const openAlertActionComponent = ( + + {i18n.ACTION_OPEN_ALERT} + + ); + + const closeAlertActionClick = useCallback(() => { + updateAlertStatusAction({ + alertIds: [eventId], + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + selectedStatus: FILTER_CLOSED, + }); + closePopover(); + }, [ + closePopover, + eventId, + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + ]); + + const closeAlertActionComponent = ( + + {i18n.ACTION_CLOSE_ALERT} + + ); + + const inProgressAlertActionClick = useCallback(() => { + updateAlertStatusAction({ + alertIds: [eventId], + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + selectedStatus: FILTER_IN_PROGRESS, + }); + closePopover(); + }, [ + closePopover, + eventId, + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + ]); + + const inProgressAlertActionComponent = ( + + {i18n.ACTION_IN_PROGRESS_ALERT} + + ); + + const openAddExceptionModal = useCallback( + ({ + ruleName, + ruleIndices, + ruleId, + exceptionListType, + alertData, + }: AddExceptionModalBaseProps) => { + if (alertData !== null && alertData !== undefined) { + setShouldShowAddExceptionModal(true); + setAddExceptionModalState({ + ruleName, + ruleId, + ruleIndices, + exceptionListType, + alertData, + }); + } + }, + [setShouldShowAddExceptionModal, setAddExceptionModalState] + ); + + const AddExceptionModal = useCallback( + () => + shouldShowAddExceptionModal === true && addExceptionModalState.alertData !== null ? ( + + ) : null, + [ + shouldShowAddExceptionModal, + addExceptionModalState.alertData, + addExceptionModalState.ruleName, + addExceptionModalState.ruleId, + addExceptionModalState.ruleIndices, + addExceptionModalState.exceptionListType, + onAddExceptionCancel, + onAddExceptionConfirm, + alertStatus, + ] + ); + + const button = ( + + ); + + const handleAddEndpointExceptionClick = useCallback(() => { + const [ruleName] = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.name', + }); + const [ruleId] = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.id', + }); + const ruleIndices = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.index', + }); + + closePopover(); + + if (ruleId !== undefined) { + openAddExceptionModal({ + ruleName: ruleName ?? '', + ruleId, + ruleIndices: ruleIndices.length > 0 ? ruleIndices : DEFAULT_INDEX_PATTERN, + exceptionListType: 'endpoint', + alertData: { + ecsData: ecsRowData, + nonEcsData: nonEcsRowData, + }, + }); + } + }, [closePopover, ecsRowData, nonEcsRowData, openAddExceptionModal]); + + const addEndpointExceptionComponent = ( + + {i18n.ACTION_ADD_ENDPOINT_EXCEPTION} + + ); + + const handleAddExceptionClick = useCallback(() => { + const [ruleName] = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.name', + }); + const [ruleId] = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.id', + }); + const ruleIndices = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.index', + }); + + closePopover(); + + if (ruleId !== undefined) { + openAddExceptionModal({ + ruleName: ruleName ?? '', + ruleId, + ruleIndices: ruleIndices.length > 0 ? ruleIndices : DEFAULT_INDEX_PATTERN, + exceptionListType: 'detection', + alertData: { + ecsData: ecsRowData, + nonEcsData: nonEcsRowData, + }, + }); + } + }, [closePopover, ecsRowData, nonEcsRowData, openAddExceptionModal]); + + const areExceptionsAllowed = useMemo(() => { + const ruleTypes = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.type', + }); + const [ruleType] = ruleTypes as RuleType[]; + return !isMlRule(ruleType) && !isThresholdRule(ruleType); + }, [nonEcsRowData]); + + const addExceptionComponent = ( + + {i18n.ACTION_ADD_EXCEPTION} + + ); + + const statusFilters = useMemo(() => { + if (!alertStatus) { + return []; + } + + switch (alertStatus) { + case 'open': + return [inProgressAlertActionComponent, closeAlertActionComponent]; + case 'in-progress': + return [openAlertActionComponent, closeAlertActionComponent]; + case 'closed': + return [openAlertActionComponent, inProgressAlertActionComponent]; + default: + return []; + } + }, [ + alertStatus, + closeAlertActionComponent, + inProgressAlertActionComponent, + openAlertActionComponent, + ]); + + const items = useMemo( + () => [...statusFilters, addEndpointExceptionComponent, addExceptionComponent], + [addEndpointExceptionComponent, addExceptionComponent, statusFilters] + ); + + return ( + <> + + + + + + + + + + ); +}; + +const ContextMenuPanel = styled(EuiContextMenuPanel)` + font-size: ${({ theme }) => theme.eui.euiFontSizeS}; +`; + +ContextMenuPanel.displayName = 'ContextMenuPanel'; + +export const AlertContextMenu = React.memo(AlertContextMenuComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx new file mode 100644 index 00000000000000..f4080de5b4ba1f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.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 React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; + +import { TimelineId } from '../../../../../common/types/timeline'; +import { TimelineNonEcsData, Ecs } from '../../../../../public/graphql/types'; +import { timelineActions } from '../../../../timelines/store/timeline'; +import { useApolloClient } from '../../../../common/utils/apollo_context'; +import { sendAlertToTimelineAction } from '../actions'; +import { dispatchUpdateTimeline } from '../../../../timelines/components/open_timeline/helpers'; +import { ActionIconItem } from '../../../../timelines/components/timeline/body/actions/action_icon_item'; + +import { CreateTimelineProps } from '../types'; +import { + ACTION_INVESTIGATE_IN_TIMELINE, + ACTION_INVESTIGATE_IN_TIMELINE_ARIA_LABEL, +} from '../translations'; + +interface InvestigateInTimelineActionProps { + ecsRowData: Ecs; + nonEcsRowData: TimelineNonEcsData[]; +} + +const InvestigateInTimelineActionComponent: React.FC = ({ + ecsRowData, + nonEcsRowData, +}) => { + const dispatch = useDispatch(); + const apolloClient = useApolloClient(); + + const updateTimelineIsLoading = useCallback( + (payload) => dispatch(timelineActions.updateIsLoading(payload)), + [dispatch] + ); + + const createTimeline = useCallback( + ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => { + updateTimelineIsLoading({ id: TimelineId.active, isLoading: false }); + dispatchUpdateTimeline(dispatch)({ + duplicate: true, + from: fromTimeline, + id: TimelineId.active, + notes: [], + timeline: { + ...timeline, + show: true, + }, + to: toTimeline, + ruleNote, + })(); + }, + [dispatch, updateTimelineIsLoading] + ); + + const investigateInTimelineAlertClick = useCallback( + () => + sendAlertToTimelineAction({ + apolloClient, + createTimeline, + ecsData: ecsRowData, + nonEcsData: nonEcsRowData, + updateTimelineIsLoading, + }), + [apolloClient, createTimeline, ecsRowData, nonEcsRowData, updateTimelineIsLoading] + ); + + return ( + + ); +}; + +export const InvestigateInTimelineAction = React.memo(InvestigateInTimelineActionComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index 3d6c3dc0a7a8e5..b4da0267d2ea5d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -115,6 +115,13 @@ export const ACTION_INVESTIGATE_IN_TIMELINE = i18n.translate( } ); +export const ACTION_INVESTIGATE_IN_TIMELINE_ARIA_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.actions.investigateInTimelineAriaLabel', + { + defaultMessage: 'Send alert to timeline', + } +); + export const ACTION_ADD_EXCEPTION = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.actions.addException', { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts index 2e77e77f6b3d50..d8ba0ab2d40b9e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts @@ -41,7 +41,6 @@ export type UpdateAlertsStatus = ({ export interface UpdateAlertStatusActionProps { query?: string; alertIds: string[]; - status: Status; selectedStatus: Status; setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx index 50348578cb039a..e1a29c3575d95c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx @@ -121,7 +121,7 @@ export const userInfoReducer = (state: State, action: Action): State => { const StateUserInfoContext = createContext<[State, Dispatch]>([initialState, () => noop]); -const useUserData = () => useContext(StateUserInfoContext); +export const useUserData = () => useContext(StateUserInfoContext); interface ManageUserInfoProps { children: React.ReactNode; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index 982712cbe97975..8c21f6a1e8cb78 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -19,7 +19,7 @@ import { } from '../../../common/mock'; import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; import { DetectionEnginePageComponent } from './detection_engine'; -import { useUserInfo } from '../../components/user_info'; +import { useUserData } from '../../components/user_info'; import { useWithSource } from '../../../common/containers/source'; import { createStore, State } from '../../../common/store'; import { mockHistory, Router } from '../../../cases/components/__mock__/router'; @@ -73,7 +73,7 @@ const store = createStore( describe('DetectionEnginePageComponent', () => { beforeAll(() => { (useParams as jest.Mock).mockReturnValue({}); - (useUserInfo as jest.Mock).mockReturnValue({}); + (useUserData as jest.Mock).mockReturnValue([{}]); (useWithSource as jest.Mock).mockReturnValue({ indicesExist: true, indexPattern: {}, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index d76da592e1c811..3a3854f145db3d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -30,7 +30,7 @@ import { NoApiIntegrationKeyCallOut } from '../../components/no_api_integration_ import { NoWriteAlertsCallOut } from '../../components/no_write_alerts_callout'; import { AlertsHistogramPanel } from '../../components/alerts_histogram_panel'; import { alertsHistogramOptions } from '../../components/alerts_histogram_panel/config'; -import { useUserInfo } from '../../components/user_info'; +import { useUserData } from '../../components/user_info'; import { OverviewEmpty } from '../../../overview/components/overview_empty'; import { DetectionEngineNoIndex } from './detection_engine_no_index'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; @@ -55,15 +55,17 @@ export const DetectionEnginePageComponent: React.FC = ({ }) => { const { to, from, deleteQuery, setQuery } = useGlobalTime(); const { globalFullScreen } = useFullScreen(); - const { - loading: userInfoLoading, - isSignalIndexExists, - isAuthenticated: isUserAuthenticated, - hasEncryptionKey, - canUserCRUD, - signalIndexName, - hasIndexWrite, - } = useUserInfo(); + const [ + { + loading: userInfoLoading, + isSignalIndexExists, + isAuthenticated: isUserAuthenticated, + hasEncryptionKey, + canUserCRUD, + signalIndexName, + hasIndexWrite, + }, + ] = useUserData(); const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.test.tsx index d4e654321ef988..045e7d402fd2b3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.test.tsx @@ -12,8 +12,8 @@ import { DetectionEngineContainer } from './index'; describe('DetectionEngineContainer', () => { it('renders correctly', () => { - const wrapper = shallow(); + const wrapper = shallow(); - expect(wrapper.find('ManageUserInfo')).toHaveLength(1); + expect(wrapper.find('Switch')).toHaveLength(1); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.tsx index 914734aba4ec62..5f379f7dbb70e3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.tsx @@ -5,37 +5,32 @@ */ import React from 'react'; -import { Route, Switch, RouteComponentProps } from 'react-router-dom'; +import { Route, Switch } from 'react-router-dom'; -import { ManageUserInfo } from '../../components/user_info'; import { CreateRulePage } from './rules/create'; import { DetectionEnginePage } from './detection_engine'; import { EditRulePage } from './rules/edit'; import { RuleDetailsPage } from './rules/details'; import { RulesPage } from './rules'; -type Props = Partial> & { url: string }; - -const DetectionEngineContainerComponent: React.FC = () => ( - - - - - - - - - - - - - - - - - - - +const DetectionEngineContainerComponent: React.FC = () => ( + + + + + + + + + + + + + + + + + ); export const DetectionEngineContainer = React.memo(DetectionEngineContainerComponent); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx index 50407c5eb219b2..deffee5a56d46d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx @@ -10,7 +10,7 @@ import { shallow } from 'enzyme'; import '../../../../../common/mock/match_media'; import { TestProviders } from '../../../../../common/mock'; import { CreateRulePage } from './index'; -import { useUserInfo } from '../../../../components/user_info'; +import { useUserData } from '../../../../components/user_info'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -29,7 +29,7 @@ jest.mock('../../../../components/user_info'); describe('CreateRulePage', () => { it('renders correctly', () => { - (useUserInfo as jest.Mock).mockReturnValue({}); + (useUserData as jest.Mock).mockReturnValue([{}]); const wrapper = shallow(, { wrappingComponent: TestProviders }); expect(wrapper.find('[title="Create new rule"]')).toHaveLength(1); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx index 70f278197b0051..d2eb3228cbbf36 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx @@ -19,7 +19,7 @@ import { import { WrapperPage } from '../../../../../common/components/wrapper_page'; import { displaySuccessToast, useStateToaster } from '../../../../../common/components/toasters'; import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; -import { useUserInfo } from '../../../../components/user_info'; +import { useUserData } from '../../../../components/user_info'; import { AccordionTitle } from '../../../../components/rules/accordion_title'; import { FormData, FormHook } from '../../../../../shared_imports'; import { StepAboutRule } from '../../../../components/rules/step_about_rule'; @@ -84,13 +84,15 @@ const StepDefineRuleAccordion: StyledComponent< StepDefineRuleAccordion.displayName = 'StepDefineRuleAccordion'; const CreateRulePageComponent: React.FC = () => { - const { - loading: userInfoLoading, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - canUserCRUD, - } = useUserInfo(); + const [ + { + loading: userInfoLoading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + }, + ] = useUserData(); const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx index 5e6587dab1736a..f8f9da78b2a062 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx @@ -19,7 +19,7 @@ import { import { RuleDetailsPageComponent } from './index'; import { createStore, State } from '../../../../../common/store'; import { setAbsoluteRangeDatePicker } from '../../../../../common/store/inputs/actions'; -import { useUserInfo } from '../../../../components/user_info'; +import { useUserData } from '../../../../components/user_info'; import { useWithSource } from '../../../../../common/containers/source'; import { useParams } from 'react-router-dom'; import { mockHistory, Router } from '../../../../../cases/components/__mock__/router'; @@ -69,7 +69,7 @@ const store = createStore( describe('RuleDetailsPageComponent', () => { beforeAll(() => { - (useUserInfo as jest.Mock).mockReturnValue({}); + (useUserData as jest.Mock).mockReturnValue([{}]); (useParams as jest.Mock).mockReturnValue({}); (useWithSource as jest.Mock).mockReturnValue({ indicesExist: true, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index f48dc64966bfc6..2988e031c4dd6d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -44,7 +44,7 @@ import { StepAboutRuleToggleDetails } from '../../../../components/rules/step_ab import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page'; import { AlertsHistogramPanel } from '../../../../components/alerts_histogram_panel'; import { AlertsTable } from '../../../../components/alerts_table'; -import { useUserInfo } from '../../../../components/user_info'; +import { useUserData } from '../../../../components/user_info'; import { OverviewEmpty } from '../../../../../overview/components/overview_empty'; import { useAlertInfo } from '../../../../components/alerts_info'; import { StepDefineRule } from '../../../../components/rules/step_define_rule'; @@ -124,15 +124,17 @@ export const RuleDetailsPageComponent: FC = ({ setAbsoluteRangeDatePicker, }) => { const { to, from, deleteQuery, setQuery } = useGlobalTime(); - const { - loading: userInfoLoading, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - canUserCRUD, - hasIndexWrite, - signalIndexName, - } = useUserInfo(); + const [ + { + loading: userInfoLoading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + hasIndexWrite, + signalIndexName, + }, + ] = useUserData(); const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx index 2e45dbc6521b70..e89c899b12c392 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx @@ -10,7 +10,7 @@ import { shallow } from 'enzyme'; import '../../../../../common/mock/match_media'; import { TestProviders } from '../../../../../common/mock'; import { EditRulePage } from './index'; -import { useUserInfo } from '../../../../components/user_info'; +import { useUserData } from '../../../../components/user_info'; import { useParams } from 'react-router-dom'; jest.mock('../../../../containers/detection_engine/lists/use_lists_config'); @@ -28,7 +28,7 @@ jest.mock('react-router-dom', () => { describe('EditRulePage', () => { it('renders correctly', () => { - (useUserInfo as jest.Mock).mockReturnValue({}); + (useUserData as jest.Mock).mockReturnValue([{}]); (useParams as jest.Mock).mockReturnValue({}); const wrapper = shallow(, { wrappingComponent: TestProviders }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx index 4033d247c4ecba..530222ee19624e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx @@ -26,7 +26,7 @@ import { } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { displaySuccessToast, useStateToaster } from '../../../../../common/components/toasters'; import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; -import { useUserInfo } from '../../../../components/user_info'; +import { useUserData } from '../../../../components/user_info'; import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page'; import { FormHook, FormData } from '../../../../../shared_imports'; import { StepPanel } from '../../../../components/rules/step_panel'; @@ -72,13 +72,15 @@ interface ActionsStepRuleForm extends StepRuleForm { const EditRulePageComponent: FC = () => { const history = useHistory(); const [, dispatchToaster] = useStateToaster(); - const { - loading: userInfoLoading, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - canUserCRUD, - } = useUserInfo(); + const [ + { + loading: userInfoLoading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + }, + ] = useUserData(); const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx index 95ef85ec1317a1..886a24dd7cbe82 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx @@ -9,7 +9,7 @@ import { shallow } from 'enzyme'; import '../../../../common/mock/match_media'; import { RulesPage } from './index'; -import { useUserInfo } from '../../../components/user_info'; +import { useUserData } from '../../../components/user_info'; import { usePrePackagedRules } from '../../../containers/detection_engine/rules'; jest.mock('react-router-dom', () => { @@ -30,7 +30,7 @@ jest.mock('../../../containers/detection_engine/rules'); describe('RulesPage', () => { beforeAll(() => { - (useUserInfo as jest.Mock).mockReturnValue({}); + (useUserData as jest.Mock).mockReturnValue([{}]); (usePrePackagedRules as jest.Mock).mockReturnValue({}); }); it('renders correctly', () => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx index 92ec0bb5a72cd6..53c82569f94aeb 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx @@ -18,7 +18,7 @@ import { DetectionEngineHeaderPage } from '../../../components/detection_engine_ import { WrapperPage } from '../../../../common/components/wrapper_page'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; -import { useUserInfo } from '../../../components/user_info'; +import { useUserData } from '../../../components/user_info'; import { AllRules } from './all'; import { ImportDataModal } from '../../../../common/components/import_data_modal'; import { ReadOnlyCallOut } from '../../../components/rules/read_only_callout'; @@ -42,14 +42,16 @@ const RulesPageComponent: React.FC = () => { const [showImportModal, setShowImportModal] = useState(false); const [showValueListsModal, setShowValueListsModal] = useState(false); const refreshRulesData = useRef(null); - const { - loading: userInfoLoading, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - canUserCRUD, - hasIndexWrite, - } = useUserInfo(); + const [ + { + loading: userInfoLoading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + hasIndexWrite, + }, + ] = useUserData(); const { loading: listsConfigLoading, canWriteIndex: canWriteListsIndex, diff --git a/x-pack/plugins/security_solution/public/detections/routes.tsx b/x-pack/plugins/security_solution/public/detections/routes.tsx index 8f542d1f886704..b5f7bc6983752e 100644 --- a/x-pack/plugins/security_solution/public/detections/routes.tsx +++ b/x-pack/plugins/security_solution/public/detections/routes.tsx @@ -12,12 +12,11 @@ import { NotFoundPage } from '../app/404'; export const AlertsRoutes: React.FC = () => ( - ( - - )} - /> - } /> + + + + + + ); diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 7b20873bf63ccb..b32083fec1b5e3 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -4719,6 +4719,14 @@ "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "status", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index f7d2c81f536be1..65d9212f77dcc3 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -1020,6 +1020,8 @@ export interface SignalField { rule?: Maybe; original_time?: Maybe; + + status?: Maybe; } export interface RuleField { @@ -5098,6 +5100,8 @@ export namespace GetTimelineQuery { export type Signal = { __typename?: 'SignalField'; + status: Maybe; + original_time: Maybe; rule: Maybe<_Rule>; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx index 88886a874a9494..6c8eb9eb04941c 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx @@ -14,7 +14,7 @@ import { hostsModel } from '../../store'; import { MatrixHistogramOption, MatrixHistogramMappingTypes, - MatrixHisrogramConfigs, + MatrixHistogramConfigs, } from '../../../common/components/matrix_histogram/types'; import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; import { KpiHostsChartColors } from '../../components/kpi_hosts/types'; @@ -49,7 +49,7 @@ export const authMatrixDataMappingFields: MatrixHistogramMappingTypes = { }, }; -const histogramConfigs: MatrixHisrogramConfigs = { +const histogramConfigs: MatrixHistogramConfigs = { defaultStackByOption: authStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? authStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_AUTHENTICATIONS_DATA, diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx index cea987db485f4b..f28c3dfa1ad779 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx @@ -14,14 +14,13 @@ import { hostsModel } from '../../store'; import { eventsDefaultModel } from '../../../common/components/events_viewer/default_model'; import { MatrixHistogramOption, - MatrixHisrogramConfigs, + MatrixHistogramConfigs, } from '../../../common/components/matrix_histogram/types'; import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; import { useFullScreen } from '../../../common/containers/use_full_screen'; import * as i18n from '../translations'; import { HistogramType } from '../../../graphql/types'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; -import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers'; const EVENTS_HISTOGRAM_ID = 'eventsOverTimeQuery'; @@ -42,7 +41,7 @@ export const eventsStackByOptions: MatrixHistogramOption[] = [ const DEFAULT_STACK_BY = 'event.action'; -export const histogramConfigs: MatrixHisrogramConfigs = { +export const histogramConfigs: MatrixHistogramConfigs = { defaultStackByOption: eventsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_EVENTS_DATA, @@ -52,14 +51,14 @@ export const histogramConfigs: MatrixHisrogramConfigs = { title: i18n.NAVIGATION_EVENTS_TITLE, }; -export const EventsQueryTabBody = ({ +const EventsQueryTabBodyComponent: React.FC = ({ deleteQuery, endDate, filterQuery, pageFilters, setQuery, startDate, -}: HostsComponentsQueryProps) => { +}) => { const { initializeTimeline } = useManageTimeline(); const dispatch = useDispatch(); const { globalFullScreen } = useFullScreen(); @@ -67,9 +66,6 @@ export const EventsQueryTabBody = ({ initializeTimeline({ id: TimelineId.hostsPageEvents, defaultModel: eventsDefaultModel, - timelineRowActions: () => [ - getInvestigateInResolverAction({ dispatch, timelineId: TimelineId.hostsPageEvents }), - ], }); }, [dispatch, initializeTimeline]); @@ -106,4 +102,8 @@ export const EventsQueryTabBody = ({ ); }; +EventsQueryTabBodyComponent.displayName = 'EventsQueryTabBodyComponent'; + +export const EventsQueryTabBody = React.memo(EventsQueryTabBodyComponent); + EventsQueryTabBody.displayName = 'EventsQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx index 77283dc330257b..2886089a1eb99e 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx @@ -16,7 +16,7 @@ import { networkModel } from '../../store'; import { MatrixHistogramOption, - MatrixHisrogramConfigs, + MatrixHistogramConfigs, } from '../../../common/components/matrix_histogram/types'; import * as i18n from '../translations'; import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; @@ -33,7 +33,7 @@ const dnsStackByOptions: MatrixHistogramOption[] = [ const DEFAULT_STACK_BY = 'dns.question.registered_domain'; -export const histogramConfigs: Omit = { +export const histogramConfigs: Omit = { defaultStackByOption: dnsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? dnsStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_DNS_DATA, @@ -64,7 +64,7 @@ export const DnsQueryTabBody = ({ [] ); - const dnsHistogramConfigs: MatrixHisrogramConfigs = useMemo( + const dnsHistogramConfigs: MatrixHistogramConfigs = useMemo( () => ({ ...histogramConfigs, title: getTitle, diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx index 6e59d81a1eae91..111935782949bf 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx @@ -26,7 +26,7 @@ import { alertsStackByOptions, histogramConfigs, } from '../../../common/components/alerts_viewer/histogram_configs'; -import { MatrixHisrogramConfigs } from '../../../common/components/matrix_histogram/types'; +import { MatrixHistogramConfigs } from '../../../common/components/matrix_histogram/types'; import { getTabsOnHostsUrl } from '../../../common/components/link_to/redirect_to_hosts'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { SecurityPageName } from '../../../app/types'; @@ -93,7 +93,7 @@ const AlertsByCategoryComponent: React.FC = ({ [goToHostAlerts, formatUrl] ); - const alertsByCategoryHistogramConfigs: MatrixHisrogramConfigs = useMemo( + const alertsByCategoryHistogramConfigs: MatrixHistogramConfigs = useMemo( () => ({ ...histogramConfigs, defaultStackByOption: diff --git a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx index f18fccee50e22b..2e9c25f01b3c1c 100644 --- a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx @@ -14,7 +14,7 @@ import { SHOWING, UNIT } from '../../../common/components/events_viewer/translat import { getTabsOnHostsUrl } from '../../../common/components/link_to/redirect_to_hosts'; import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; import { - MatrixHisrogramConfigs, + MatrixHistogramConfigs, MatrixHistogramOption, } from '../../../common/components/matrix_histogram/types'; import { eventsStackByOptions } from '../../../hosts/pages/navigation'; @@ -127,7 +127,7 @@ const EventsByDatasetComponent: React.FC = ({ [combinedQueries, kibana, indexPattern, query, filters] ); - const eventsByDatasetHistogramConfigs: MatrixHisrogramConfigs = useMemo( + const eventsByDatasetHistogramConfigs: MatrixHistogramConfigs = useMemo( () => ({ ...histogramConfigs, stackByOptions: diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx index 07c4893e4550b4..3c9101878be8d2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx @@ -46,13 +46,7 @@ PanesFlexGroup.displayName = 'PanesFlexGroup'; type Props = Pick< FieldBrowserProps, - | 'browserFields' - | 'isEventViewer' - | 'height' - | 'onFieldSelected' - | 'onUpdateColumns' - | 'timelineId' - | 'width' + 'browserFields' | 'height' | 'onFieldSelected' | 'onUpdateColumns' | 'timelineId' | 'width' > & { /** * The current timeline column headers @@ -106,7 +100,6 @@ const FieldsBrowserComponent: React.FC = ({ browserFields, columnHeaders, filteredBrowserFields, - isEventViewer, isSearching, onCategorySelected, onFieldSelected, @@ -193,7 +186,6 @@ const FieldsBrowserComponent: React.FC = ({
void; onSearchInputChange: (event: React.ChangeEvent) => void; @@ -93,7 +92,6 @@ CountRow.displayName = 'CountRow'; const TitleRow = React.memo<{ id: string; - isEventViewer?: boolean; onOutsideClick: () => void; onUpdateColumns: OnUpdateColumns; }>(({ id, onOutsideClick, onUpdateColumns }) => { @@ -130,7 +128,6 @@ TitleRow.displayName = 'TitleRow'; export const Header = React.memo( ({ - isEventViewer, isSearching, filteredBrowserFields, onOutsideClick, @@ -140,12 +137,7 @@ export const Header = React.memo( timelineId, }) => ( - + = ({ columnHeaders, browserFields, height, - isEventViewer = false, onFieldSelected, onUpdateColumns, timelineId, @@ -164,7 +163,6 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ filteredBrowserFields != null ? filteredBrowserFields : browserFieldsWithDefaultCategory } height={height} - isEventViewer={isEventViewer} isSearching={isSearching} onCategorySelected={updateSelectedCategoryId} onFieldSelected={onFieldSelected} diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx index b918e5abc652b3..fe0f0c8f8b91f5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx @@ -8,7 +8,6 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { getTimelineDefaults, useTimelineManager, UseTimelineManager } from './'; import { FilterManager } from '../../../../../../../src/plugins/data/public/query/filter_manager'; import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { TimelineRowAction } from '../timeline/body/actions'; const isStringifiedComparisonEqual = (a: {}, b: {}): boolean => JSON.stringify(a) === JSON.stringify(b); @@ -17,13 +16,14 @@ describe('useTimelineManager', () => { const setupMock = coreMock.createSetup(); const testId = 'coolness'; const timelineDefaults = getTimelineDefaults(testId); - const timelineRowActions = () => []; const mockFilterManager = new FilterManager(setupMock.uiSettings); + beforeEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); }); - it('initilizes an undefined timeline', async () => { + + it('initializes an undefined timeline', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useTimelineManager() @@ -33,6 +33,7 @@ describe('useTimelineManager', () => { expect(isStringifiedComparisonEqual(uninitializedTimeline, timelineDefaults)).toBeTruthy(); }); }); + it('getIndexToAddById', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => @@ -43,6 +44,7 @@ describe('useTimelineManager', () => { expect(data).toEqual(timelineDefaults.indexToAdd); }); }); + it('setIndexToAdd', async () => { await act(async () => { const indexToAddArgs = { id: testId, indexToAdd: ['example'] }; @@ -52,13 +54,13 @@ describe('useTimelineManager', () => { await waitForNextUpdate(); result.current.initializeTimeline({ id: testId, - timelineRowActions, }); result.current.setIndexToAdd(indexToAddArgs); const data = result.current.getIndexToAddById(testId); expect(data).toEqual(indexToAddArgs.indexToAdd); }); }); + it('setIsTimelineLoading', async () => { await act(async () => { const isLoadingArgs = { id: testId, isLoading: true }; @@ -68,7 +70,6 @@ describe('useTimelineManager', () => { await waitForNextUpdate(); result.current.initializeTimeline({ id: testId, - timelineRowActions, }); let timeline = result.current.getManageTimelineById(testId); expect(timeline.isLoading).toBeFalsy(); @@ -77,29 +78,7 @@ describe('useTimelineManager', () => { expect(timeline.isLoading).toBeTruthy(); }); }); - it('setTimelineRowActions', async () => { - await act(async () => { - const timelineRowActionsEx = () => [ - { id: 'wow', content: 'hey', displayType: 'icon', onClick: () => {} } as TimelineRowAction, - ]; - const { result, waitForNextUpdate } = renderHook(() => - useTimelineManager() - ); - await waitForNextUpdate(); - result.current.initializeTimeline({ - id: testId, - timelineRowActions, - }); - let timeline = result.current.getManageTimelineById(testId); - expect(timeline.timelineRowActions).toEqual(timelineRowActions); - result.current.setTimelineRowActions({ - id: testId, - timelineRowActions: timelineRowActionsEx, - }); - timeline = result.current.getManageTimelineById(testId); - expect(timeline.timelineRowActions).toEqual(timelineRowActionsEx); - }); - }); + it('getTimelineFilterManager undefined on uninitialized', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => @@ -110,6 +89,7 @@ describe('useTimelineManager', () => { expect(data).toEqual(undefined); }); }); + it('getTimelineFilterManager defined at initialize', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => @@ -118,13 +98,13 @@ describe('useTimelineManager', () => { await waitForNextUpdate(); result.current.initializeTimeline({ id: testId, - timelineRowActions, filterManager: mockFilterManager, }); const data = result.current.getTimelineFilterManager(testId); expect(data).toEqual(mockFilterManager); }); }); + it('isManagedTimeline returns false when unset and then true when set', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => @@ -135,7 +115,6 @@ describe('useTimelineManager', () => { expect(data).toBeFalsy(); result.current.initializeTimeline({ id: testId, - timelineRowActions, filterManager: mockFilterManager, }); data = result.current.isManagedTimeline(testId); diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx index 560d4c6928e4e4..f82158fe65c11b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx @@ -9,12 +9,10 @@ import { noop } from 'lodash/fp'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { FilterManager } from '../../../../../../../src/plugins/data/public/query/filter_manager'; -import { TimelineRowAction } from '../timeline/body/actions'; import { SubsetTimelineModel } from '../../store/timeline/model'; import * as i18n from '../../../common/components/events_viewer/translations'; import * as i18nF from '../timeline/footer/translations'; import { timelineDefaults as timelineDefaultModel } from '../../store/timeline/defaults'; -import { Ecs, TimelineNonEcsData } from '../../../graphql/types'; interface ManageTimelineInit { documentType?: string; @@ -25,16 +23,11 @@ interface ManageTimelineInit { indexToAdd?: string[] | null; loadingText?: string; selectAll?: boolean; - timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[]; + queryFields?: string[]; title?: string; unit?: (totalCount: number) => string; } -export interface TimelineRowActionArgs { - ecsData: Ecs; - nonEcsData: TimelineNonEcsData[]; -} - interface ManageTimeline { documentType: string; defaultModel: SubsetTimelineModel; @@ -46,7 +39,6 @@ interface ManageTimeline { loadingText: string; queryFields: string[]; selectAll: boolean; - timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[]; title: string; unit: (totalCount: number) => string; } @@ -75,14 +67,6 @@ type ActionManageTimeline = type: 'SET_SELECT_ALL'; id: string; payload: boolean; - } - | { - type: 'SET_TIMELINE_ACTIONS'; - id: string; - payload: { - queryFields?: string[]; - timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[]; - }; }; export const getTimelineDefaults = (id: string) => ({ @@ -95,7 +79,6 @@ export const getTimelineDefaults = (id: string) => ({ id, isLoading: false, queryFields: [], - timelineRowActions: () => [], title: i18n.EVENTS, unit: (n: number) => i18n.UNIT(n), }); @@ -129,14 +112,7 @@ const reducerManageTimeline = ( selectAll: action.payload, }, } as ManageTimelineById; - case 'SET_TIMELINE_ACTIONS': - return { - ...state, - [action.id]: { - ...state[action.id], - ...action.payload, - }, - } as ManageTimelineById; + case 'SET_IS_LOADING': return { ...state, @@ -159,11 +135,6 @@ export interface UseTimelineManager { setIndexToAdd: (indexToAddArgs: { id: string; indexToAdd: string[] }) => void; setIsTimelineLoading: (isLoadingArgs: { id: string; isLoading: boolean }) => void; setSelectAll: (selectAllArgs: { id: string; selectAll: boolean }) => void; - setTimelineRowActions: (actionsArgs: { - id: string; - queryFields?: string[]; - timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[]; - }) => void; } export const useTimelineManager = ( @@ -181,25 +152,6 @@ export const useTimelineManager = ( }); }, []); - const setTimelineRowActions = useCallback( - ({ - id, - queryFields, - timelineRowActions, - }: { - id: string; - queryFields?: string[]; - timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[]; - }) => { - dispatch({ - type: 'SET_TIMELINE_ACTIONS', - id, - payload: { queryFields, timelineRowActions }, - }); - }, - [] - ); - const setIsTimelineLoading = useCallback( ({ id, isLoading }: { id: string; isLoading: boolean }) => { dispatch({ @@ -236,7 +188,7 @@ export const useTimelineManager = ( if (state[id] != null) { return state[id]; } - initializeTimeline({ id, timelineRowActions: () => [] }); + initializeTimeline({ id }); return getTimelineDefaults(id); }, [initializeTimeline, state] @@ -261,7 +213,6 @@ export const useTimelineManager = ( setIndexToAdd, setIsTimelineLoading, setSelectAll, - setTimelineRowActions, }; }; @@ -274,7 +225,6 @@ const init = { setIndexToAdd: () => undefined, setIsTimelineLoading: () => noop, setSelectAll: () => noop, - setTimelineRowActions: () => noop, }; const ManageTimelineContext = createContext(init); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index ac6c61b33b35ec..ed44fc14e3efae 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -36,7 +36,7 @@ import { KueryFilterQueryKind } from '../../../common/store/model'; import { Note } from '../../../common/lib/note'; import moment from 'moment'; import sinon from 'sinon'; -import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; +import { TimelineId, TimelineType, TimelineStatus } from '../../../../common/types/timeline'; jest.mock('../../../common/store/inputs/actions'); jest.mock('../../../common/components/url_state/normalize_time_range.ts'); @@ -942,7 +942,7 @@ describe('helpers', () => { test('it invokes date range picker dispatch', () => { timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -958,7 +958,7 @@ describe('helpers', () => { test('it invokes add timeline dispatch', () => { timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -966,7 +966,7 @@ describe('helpers', () => { })(); expect(dispatchAddTimeline).toHaveBeenCalledWith({ - id: 'timeline-1', + id: TimelineId.active, savedTimeline: true, timeline: mockTimelineModel, }); @@ -975,7 +975,7 @@ describe('helpers', () => { test('it does not invoke kql filter query dispatches if timeline.kqlQuery.filterQuery is null', () => { timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -989,7 +989,7 @@ describe('helpers', () => { test('it does not invoke notes dispatch if duplicate is true', () => { timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -1012,7 +1012,7 @@ describe('helpers', () => { }; timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -1036,7 +1036,7 @@ describe('helpers', () => { }; timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -1044,14 +1044,14 @@ describe('helpers', () => { })(); expect(dispatchSetKqlFilterQueryDraft).toHaveBeenCalledWith({ - id: 'timeline-1', + id: TimelineId.active, filterQueryDraft: { kind: 'kuery', expression: 'expression', }, }); expect(dispatchApplyKqlFilterQuery).toHaveBeenCalledWith({ - id: 'timeline-1', + id: TimelineId.active, filterQuery: { kuery: { kind: 'kuery', @@ -1065,7 +1065,7 @@ describe('helpers', () => { test('it invokes dispatchAddNotes if duplicate is false', () => { timelineDispatch({ duplicate: false, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [ @@ -1099,7 +1099,7 @@ describe('helpers', () => { test('it invokes dispatch to create a timeline note if duplicate is true and ruleNote exists', () => { timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -1119,7 +1119,7 @@ describe('helpers', () => { expect(dispatchAddNotes).not.toHaveBeenCalled(); expect(dispatchUpdateNote).toHaveBeenCalledWith({ note: expectedNote }); expect(dispatchAddGlobalTimelineNote).toHaveBeenLastCalledWith({ - id: 'timeline-1', + id: TimelineId.active, noteId: 'uuid.v4()', }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index c2e23cc19d89e2..b6b6148340a4a9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -22,7 +22,12 @@ import { DataProviderResult, } from '../../../graphql/types'; -import { DataProviderType, TimelineStatus, TimelineType } from '../../../../common/types/timeline'; +import { + DataProviderType, + TimelineId, + TimelineStatus, + TimelineType, +} from '../../../../common/types/timeline'; import { addNotes as dispatchAddNotes, @@ -315,7 +320,7 @@ export const queryTimelineById = ({ updateIsLoading, updateTimeline, }: QueryTimelineById) => { - updateIsLoading({ id: 'timeline-1', isLoading: true }); + updateIsLoading({ id: TimelineId.active, isLoading: true }); if (apolloClient) { apolloClient .query({ @@ -343,7 +348,7 @@ export const queryTimelineById = ({ updateTimeline({ duplicate, from, - id: 'timeline-1', + id: TimelineId.active, notes, timeline: { ...timeline, @@ -355,7 +360,7 @@ export const queryTimelineById = ({ } }) .finally(() => { - updateIsLoading({ id: 'timeline-1', isLoading: false }); + updateIsLoading({ id: TimelineId.active, isLoading: false }); }); } }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index 4c5db80a6c916a..f681043a9047d9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -11,6 +11,7 @@ import { Dispatch } from 'redux'; import { DeleteTimelineMutation, SortFieldTimeline, Direction } from '../../../graphql/types'; import { State } from '../../../common/store'; +import { TimelineId } from '../../../../common/types/timeline'; import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model'; import { timelineSelectors } from '../../../timelines/store/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; @@ -192,7 +193,7 @@ export const StatefulOpenTimelineComponent = React.memo( const deleteTimelines: DeleteTimelines = useCallback( async (timelineIds: string[]) => { if (timelineIds.includes(timeline.savedObjectId || '')) { - createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false }); + createNewTimeline({ id: TimelineId.active, columns: defaultHeaders, show: false }); } await apolloClient.mutate< @@ -369,7 +370,7 @@ export const StatefulOpenTimelineComponent = React.memo( const makeMapStateToProps = () => { const getTimeline = timelineSelectors.getTimelineByIdSelector(); const mapStateToProps = (state: State) => { - const timeline = getTimeline(state, 'timeline-1') ?? timelineDefaults; + const timeline = getTimeline(state, TimelineId.active) ?? timelineDefaults; return { timeline, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx new file mode 100644 index 00000000000000..64f8ce3727f39a --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx @@ -0,0 +1,55 @@ +/* + * 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, { MouseEvent } from 'react'; +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; + +import { EventsTd, EventsTdContent } from '../../styles'; +import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; + +interface ActionIconItemProps { + ariaLabel?: string; + id: string; + width?: number; + dataTestSubj?: string; + content?: string; + iconType?: string; + isDisabled?: boolean; + onClick?: (event: MouseEvent) => void; + children?: React.ReactNode; +} + +const ActionIconItemComponent: React.FC = ({ + id, + width = DEFAULT_ICON_BUTTON_WIDTH, + dataTestSubj, + content, + ariaLabel, + iconType, + isDisabled = false, + onClick, + children, +}) => ( + + + {children ?? ( + + + + )} + + +); + +ActionIconItemComponent.displayName = 'ActionIconItemComponent'; + +export const ActionIconItem = React.memo(ActionIconItemComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx new file mode 100644 index 00000000000000..a82821675d9565 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx @@ -0,0 +1,59 @@ +/* + * 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 { TimelineType, TimelineStatus } from '../../../../../../common/types/timeline'; +import { AssociateNote, UpdateNote } from '../../../notes/helpers'; +import * as i18n from '../translations'; +import { NotesButton } from '../../properties/helpers'; +import { Note } from '../../../../../common/lib/note'; +import { ActionIconItem } from './action_icon_item'; + +interface AddEventNoteActionProps { + associateNote: AssociateNote; + getNotesByIds: (noteIds: string[]) => Note[]; + noteIds: string[]; + showNotes: boolean; + status: TimelineStatus; + timelineType: TimelineType; + toggleShowNotes: () => void; + updateNote: UpdateNote; +} + +const AddEventNoteActionComponent: React.FC = ({ + associateNote, + getNotesByIds, + noteIds, + showNotes, + status, + timelineType, + toggleShowNotes, + updateNote, +}) => ( + + + +); + +AddEventNoteActionComponent.displayName = 'AddEventNoteActionComponent'; + +export const AddEventNoteAction = React.memo(AddEventNoteActionComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx index 78ee9bdd053b20..fb1709df013205 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx @@ -9,10 +9,7 @@ import { useSelector } from 'react-redux'; import { TestProviders, mockTimelineModel } from '../../../../../common/mock'; import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; -import * as i18n from '../translations'; - import { Actions } from '.'; -import { TimelineType } from '../../../../../../common/types/timeline'; jest.mock('react-redux', () => { const origin = jest.requireActual('react-redux'); @@ -30,22 +27,14 @@ describe('Actions', () => { ); @@ -58,22 +47,14 @@ describe('Actions', () => { ); @@ -86,22 +67,14 @@ describe('Actions', () => { ); @@ -116,22 +89,14 @@ describe('Actions', () => { ); @@ -140,197 +105,4 @@ describe('Actions', () => { expect(onEventToggled).toBeCalled(); }); - - test('it does NOT render a notes button when isEventsViewer is true', () => { - const toggleShowNotes = jest.fn(); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="timeline-notes-button-small"]').exists()).toBe(false); - }); - - test('it invokes toggleShowNotes when the button for adding notes is clicked', () => { - const toggleShowNotes = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="timeline-notes-button-small"]').first().simulate('click'); - - expect(toggleShowNotes).toBeCalled(); - }); - - test('it renders correct tooltip for NotesButton - timeline', () => { - const toggleShowNotes = jest.fn(); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="add-note"]').prop('toolTip')).toEqual(i18n.NOTES_TOOLTIP); - }); - - test('it renders correct tooltip for NotesButton - timeline template', () => { - (useSelector as jest.Mock).mockReturnValue({ - ...mockTimelineModel, - timelineType: TimelineType.template, - }); - const toggleShowNotes = jest.fn(); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="add-note"]').prop('toolTip')).toEqual( - i18n.NOTES_DISABLE_TOOLTIP - ); - (useSelector as jest.Mock).mockReturnValue(mockTimelineModel); - }); - - test('it does NOT render a pin button when isEventViewer is true', () => { - const onPinClicked = jest.fn(); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="pin"]').exists()).toBe(false); - }); - - test('it invokes onPinClicked when the button for pinning events is clicked', () => { - const onPinClicked = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="pin"]').first().simulate('click'); - - expect(onPinClicked).toHaveBeenCalled(); - }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index c9c8250922161c..3d08d56d6fb195 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -3,203 +3,90 @@ * 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 { useSelector } from 'react-redux'; -import { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { EuiButtonIcon, EuiLoadingSpinner, EuiCheckbox } from '@elastic/eui'; -import { Note } from '../../../../../common/lib/note'; -import { StoreState } from '../../../../../common/store/types'; -import { TimelineType } from '../../../../../../common/types/timeline'; - -import { TimelineModel } from '../../../../store/timeline/model'; - -import { AssociateNote, UpdateNote } from '../../../notes/helpers'; -import { Pin } from '../../pin'; -import { NotesButton } from '../../properties/helpers'; import { EventsLoading, EventsTd, EventsTdContent, EventsTdGroupActions } from '../../styles'; -import { eventHasNotes, getPinTooltip } from '../helpers'; import * as i18n from '../translations'; import { OnRowSelected } from '../../events'; -import { Ecs, TimelineNonEcsData } from '../../../../../graphql/types'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; -export interface TimelineRowActionOnClick { - eventId: string; - ecsData: Ecs; - data: TimelineNonEcsData[]; -} - -export interface TimelineRowAction { - ariaLabel?: string; - dataTestSubj?: string; - displayType: 'icon' | 'contextMenu'; - iconType?: string; - id: string; - isActionDisabled?: (ecsData?: Ecs) => boolean; - onClick: ({ eventId, ecsData }: TimelineRowActionOnClick) => void; - content: string | JSX.Element; - width?: number; -} - interface Props { actionsColumnWidth: number; additionalActions?: JSX.Element[]; - associateNote: AssociateNote; checked: boolean; onRowSelected: OnRowSelected; expanded: boolean; eventId: string; - eventIsPinned: boolean; - getNotesByIds: (noteIds: string[]) => Note[]; - isEventViewer?: boolean; loading: boolean; loadingEventIds: Readonly; - noteIds: string[]; onEventToggled: () => void; - onPinClicked: () => void; - showNotes: boolean; showCheckboxes: boolean; - toggleShowNotes: () => void; - updateNote: UpdateNote; } -const emptyNotes: string[] = []; - -export const Actions = React.memo( - ({ - actionsColumnWidth, - additionalActions, - associateNote, - checked, - expanded, - eventId, - eventIsPinned, - getNotesByIds, - isEventViewer = false, - loading = false, - loadingEventIds, - noteIds, - onEventToggled, - onPinClicked, - onRowSelected, - showCheckboxes, - showNotes, - toggleShowNotes, - updateNote, - }) => { - const timeline = useSelector((state) => { - return state.timeline.timelineById['timeline-1']; - }); - return ( - - {showCheckboxes && ( - - - {loadingEventIds.includes(eventId) ? ( - - ) : ( - ) => { - onRowSelected({ - eventIds: [eventId], - isSelected: event.currentTarget.checked, - }); - }} - /> - )} - - - )} +const ActionsComponent: React.FC = ({ + actionsColumnWidth, + additionalActions, + checked, + expanded, + eventId, + loading = false, + loadingEventIds, + onEventToggled, + onRowSelected, + showCheckboxes, +}) => { + const handleSelectEvent = useCallback( + (event: React.ChangeEvent) => + onRowSelected({ + eventIds: [eventId], + isSelected: event.currentTarget.checked, + }), + [eventId, onRowSelected] + ); - + return ( + + {showCheckboxes && ( + - {loading ? ( - + {loadingEventIds.includes(eventId) ? ( + ) : ( - )} + )} + + + {loading ? ( + + ) : ( + + )} + + - <>{additionalActions} + <>{additionalActions} + + ); +}; - {!isEventViewer && ( - <> - - - - - - - +ActionsComponent.displayName = 'ActionsComponent'; - - - - - - - )} - - ); - }, - (nextProps, prevProps) => { - return ( - prevProps.actionsColumnWidth === nextProps.actionsColumnWidth && - prevProps.additionalActions === nextProps.additionalActions && - prevProps.checked === nextProps.checked && - prevProps.expanded === nextProps.expanded && - prevProps.eventId === nextProps.eventId && - prevProps.eventIsPinned === nextProps.eventIsPinned && - prevProps.loading === nextProps.loading && - prevProps.loadingEventIds === nextProps.loadingEventIds && - prevProps.noteIds === nextProps.noteIds && - prevProps.onRowSelected === nextProps.onRowSelected && - prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.showNotes === nextProps.showNotes - ); - } -); -Actions.displayName = 'Actions'; +export const Actions = React.memo(ActionsComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx new file mode 100644 index 00000000000000..2f9f15938cad66 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx @@ -0,0 +1,58 @@ +/* + * 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, { useMemo } from 'react'; +import { EuiToolTip } from '@elastic/eui'; + +import { EventsTd, EventsTdContent } from '../../styles'; +import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; +import { eventHasNotes, getPinTooltip } from '../helpers'; +import { Pin } from '../../pin'; +import { TimelineType } from '../../../../../../common/types/timeline'; + +interface PinEventActionProps { + noteIds: string[]; + onPinClicked: () => void; + eventIsPinned: boolean; + timelineType: TimelineType; +} + +const PinEventActionComponent: React.FC = ({ + noteIds, + onPinClicked, + eventIsPinned, + timelineType, +}) => { + const tooltipContent = useMemo( + () => + getPinTooltip({ + isPinned: eventIsPinned, + eventHasNotes: eventHasNotes(noteIds), + timelineType, + }), + [eventIsPinned, noteIds, timelineType] + ); + + return ( + + + + + + + + ); +}; + +PinEventActionComponent.displayName = 'PinEventActionComponent'; + +export const PinEventAction = React.memo(PinEventActionComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index a3e177604fbd47..120fc12b425f4a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -235,7 +235,6 @@ export const ColumnHeadersComponent = ({ columnHeaders={columnHeaders} data-test-subj="field-browser" height={FIELD_BROWSER_HEIGHT} - isEventViewer={isEventViewer} onUpdateColumns={onUpdateColumns} timelineId={timelineId} toggleColumn={toggleColumn} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx new file mode 100644 index 00000000000000..ae552ade665cbc --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -0,0 +1,117 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { TestProviders, mockTimelineModel } from '../../../../../common/mock'; +import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; +import * as i18n from '../translations'; + +import { EventColumnView } from './event_column_view'; +import { TimelineType } from '../../../../../../common/types/timeline'; + +jest.mock('react-redux', () => { + const origin = jest.requireActual('react-redux'); + return { + ...origin, + useSelector: jest.fn(), + }; +}); + +describe('EventColumnView', () => { + (useSelector as jest.Mock).mockReturnValue(mockTimelineModel); + + const props = { + id: 'event-id', + actionsColumnWidth: DEFAULT_ACTIONS_COLUMN_WIDTH, + associateNote: jest.fn(), + columnHeaders: [], + columnRenderers: [], + data: [ + { + field: 'host.name', + }, + ], + ecsData: { + _id: 'id', + }, + eventIdToNoteIds: {}, + expanded: false, + getNotesByIds: jest.fn(), + loading: false, + loadingEventIds: [], + onColumnResized: jest.fn(), + onEventToggled: jest.fn(), + onPinEvent: jest.fn(), + onRowSelected: jest.fn(), + onUnPinEvent: jest.fn(), + refetch: jest.fn(), + selectedEventIds: {}, + showCheckboxes: false, + showNotes: false, + timelineId: 'timeline-1', + toggleShowNotes: jest.fn(), + updateNote: jest.fn(), + isEventPinned: false, + }; + + test('it does NOT render a notes button when isEventsViewer is true', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(wrapper.find('[data-test-subj="timeline-notes-button-small"]').exists()).toBe(false); + }); + + test('it invokes toggleShowNotes when the button for adding notes is clicked', () => { + const wrapper = mount(, { wrappingComponent: TestProviders }); + + expect(props.toggleShowNotes).not.toHaveBeenCalled(); + + wrapper.find('[data-test-subj="timeline-notes-button-small"]').first().simulate('click'); + + expect(props.toggleShowNotes).toHaveBeenCalled(); + }); + + test('it renders correct tooltip for NotesButton - timeline', () => { + const wrapper = mount(, { wrappingComponent: TestProviders }); + + expect(wrapper.find('[data-test-subj="add-note"]').prop('toolTip')).toEqual(i18n.NOTES_TOOLTIP); + }); + + test('it renders correct tooltip for NotesButton - timeline template', () => { + (useSelector as jest.Mock).mockReturnValue({ + ...mockTimelineModel, + timelineType: TimelineType.template, + }); + + const wrapper = mount(, { wrappingComponent: TestProviders }); + + expect(wrapper.find('[data-test-subj="add-note"]').prop('toolTip')).toEqual( + i18n.NOTES_DISABLE_TOOLTIP + ); + (useSelector as jest.Mock).mockReturnValue(mockTimelineModel); + }); + + test('it does NOT render a pin button when isEventViewer is true', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(wrapper.find('[data-test-subj="pin"]').exists()).toBe(false); + }); + + test('it invokes onPinClicked when the button for pinning events is clicked', () => { + const wrapper = mount(, { wrappingComponent: TestProviders }); + + expect(props.onPinEvent).not.toHaveBeenCalled(); + + wrapper.find('[data-test-subj="pin"]').first().simulate('click'); + + expect(props.onPinEvent).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index e7462188001e99..f1d45d5458554e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -4,29 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import uuid from 'uuid'; +import { useSelector, shallowEqual } from 'react-redux'; -import { - EuiButtonIcon, - EuiToolTip, - EuiContextMenuPanel, - EuiPopover, - EuiContextMenuItem, -} from '@elastic/eui'; -import styled from 'styled-components'; import { TimelineNonEcsData, Ecs } from '../../../../../graphql/types'; -import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; import { Note } from '../../../../../common/lib/note'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { AssociateNote, UpdateNote } from '../../../notes/helpers'; import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; -import { EventsTd, EventsTdContent, EventsTrData } from '../../styles'; +import { EventsTrData } from '../../styles'; import { Actions } from '../actions'; import { DataDrivenColumns } from '../data_driven_columns'; -import { eventHasNotes, getPinOnClick } from '../helpers'; +import { + eventHasNotes, + getEventType, + getPinOnClick, + InvestigateInResolverAction, +} from '../helpers'; import { ColumnRenderer } from '../renderers/column_renderer'; -import { useManageTimeline } from '../../../manage_timeline'; +import { AlertContextMenu } from '../../../../../detections/components/alerts_table/timeline_actions/alert_context_menu'; +import { InvestigateInTimelineAction } from '../../../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action'; +import { AddEventNoteAction } from '../actions/add_note_icon_item'; +import { PinEventAction } from '../actions/pin_event_action'; +import { StoreState } from '../../../../../common/store/types'; +import { inputsModel } from '../../../../../common/store'; +import { TimelineId } from '../../../../../../common/types/timeline'; + +import { TimelineModel } from '../../../../store/timeline/model'; interface Props { id: string; @@ -48,6 +53,7 @@ interface Props { onPinEvent: OnPinEvent; onRowSelected: OnRowSelected; onUnPinEvent: OnUnPinEvent; + refetch: inputsModel.Refetch; selectedEventIds: Readonly>; showCheckboxes: boolean; showNotes: boolean; @@ -81,6 +87,7 @@ export const EventColumnView = React.memo( onPinEvent, onRowSelected, onUnPinEvent, + refetch, selectedEventIds, showCheckboxes, showNotes, @@ -88,114 +95,10 @@ export const EventColumnView = React.memo( toggleShowNotes, updateNote, }) => { - const { getManageTimelineById } = useManageTimeline(); - const timelineActions = useMemo( - () => getManageTimelineById(timelineId).timelineRowActions({ nonEcsData: data, ecsData }), - [data, ecsData, getManageTimelineById, timelineId] + const { timelineType, status } = useSelector( + (state) => state.timeline.timelineById[timelineId], + shallowEqual ); - const [isPopoverOpen, setPopover] = useState(false); - - const onButtonClick = useCallback(() => { - setPopover(!isPopoverOpen); - }, [isPopoverOpen]); - - const closePopover = useCallback(() => { - setPopover(false); - }, []); - - const button = ( - - ); - - const onClickCb = useCallback((cb: () => void) => { - cb(); - closePopover(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const additionalActions = useMemo(() => { - const grouped = timelineActions.reduce( - ( - acc: { - contextMenu: JSX.Element[]; - icon: JSX.Element[]; - }, - action - ) => { - if (action.displayType === 'icon') { - return { - ...acc, - icon: [ - ...acc.icon, - - - - action.onClick({ eventId: id, ecsData, data })} - /> - - - , - ], - }; - } - return { - ...acc, - contextMenu: [ - ...acc.contextMenu, - onClickCb(() => action.onClick({ eventId: id, ecsData, data }))} - > - {action.content} - , - ], - }; - }, - { icon: [], contextMenu: [] } - ); - return grouped.contextMenu.length > 0 - ? [ - ...grouped.icon, - - - - - - - , - ] - : grouped.icon; - }, [button, closePopover, id, onClickCb, data, ecsData, timelineActions, isPopoverOpen]); const handlePinClicked = useCallback( () => @@ -209,29 +112,90 @@ export const EventColumnView = React.memo( [eventIdToNoteIds, id, isEventPinned, onPinEvent, onUnPinEvent] ); + const eventType = getEventType(ecsData); + + const additionalActions = useMemo( + () => [ + , + ...(timelineId !== TimelineId.active && eventType === 'signal' + ? [ + , + ] + : []), + ...(!isEventViewer + ? [ + , + , + ] + : []), + , + ], + [ + associateNote, + data, + ecsData, + eventIdToNoteIds, + eventType, + getNotesByIds, + handlePinClicked, + id, + isEventPinned, + isEventViewer, + refetch, + showNotes, + status, + timelineId, + timelineType, + toggleShowNotes, + updateNote, + ] + ); + return ( ( /> ); - }, - (prevProps, nextProps) => { - return ( - prevProps.id === nextProps.id && - prevProps.actionsColumnWidth === nextProps.actionsColumnWidth && - prevProps.columnHeaders === nextProps.columnHeaders && - prevProps.columnRenderers === nextProps.columnRenderers && - prevProps.data === nextProps.data && - prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && - prevProps.expanded === nextProps.expanded && - prevProps.loading === nextProps.loading && - prevProps.loadingEventIds === nextProps.loadingEventIds && - prevProps.isEventPinned === nextProps.isEventPinned && - prevProps.onRowSelected === nextProps.onRowSelected && - prevProps.selectedEventIds === nextProps.selectedEventIds && - prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.showNotes === nextProps.showNotes && - prevProps.timelineId === nextProps.timelineId - ); } ); -const ContextMenuPanel = styled(EuiContextMenuPanel)` - font-size: ${({ theme }) => theme.eui.euiFontSizeS}; -`; -ContextMenuPanel.displayName = 'ContextMenuPanel'; +EventColumnView.displayName = 'EventColumnView'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx index ca7a64db58c957..64d55f8cf6c6a2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx @@ -6,6 +6,7 @@ import React from 'react'; +import { inputsModel } from '../../../../../common/store'; import { BrowserFields, DocValueFields } from '../../../../../common/containers/source'; import { TimelineItem, TimelineNonEcsData } from '../../../../../graphql/types'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; @@ -44,6 +45,7 @@ interface Props { onUpdateColumns: OnUpdateColumns; onUnPinEvent: OnUnPinEvent; pinnedEventIds: Readonly>; + refetch: inputsModel.Refetch; rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; showCheckboxes: boolean; @@ -71,6 +73,7 @@ const EventsComponent: React.FC = ({ onUpdateColumns, onUnPinEvent, pinnedEventIds, + refetch, rowRenderers, selectedEventIds, showCheckboxes, @@ -78,7 +81,7 @@ const EventsComponent: React.FC = ({ updateNote, }) => ( - {data.map((event, i) => ( + {data.map((event) => ( = ({ onRowSelected={onRowSelected} onUnPinEvent={onUnPinEvent} onUpdateColumns={onUpdateColumns} + refetch={refetch} rowRenderers={rowRenderers} selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 3236482e6bc273..c91fc473708e2f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -9,6 +9,7 @@ import { useSelector } from 'react-redux'; import uuid from 'uuid'; import VisibilitySensor from 'react-visibility-sensor'; +import { TimelineId } from '../../../../../../common/types/timeline'; import { BrowserFields, DocValueFields } from '../../../../../common/containers/source'; import { TimelineDetailsQuery } from '../../../../containers/details'; import { TimelineItem, DetailItem, TimelineNonEcsData } from '../../../../../graphql/types'; @@ -33,7 +34,7 @@ import { getEventType } from '../helpers'; import { NoteCards } from '../../../notes/note_cards'; import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context'; import { EventColumnView } from './event_column_view'; -import { StoreState } from '../../../../../common/store'; +import { inputsModel, StoreState } from '../../../../../common/store'; interface Props { actionsColumnWidth: number; @@ -55,6 +56,7 @@ interface Props { onUnPinEvent: OnUnPinEvent; onUpdateColumns: OnUpdateColumns; isEventPinned: boolean; + refetch: inputsModel.Refetch; rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; showCheckboxes: boolean; @@ -121,6 +123,7 @@ const StatefulEventComponent: React.FC = ({ onRowSelected, onUnPinEvent, onUpdateColumns, + refetch, rowRenderers, selectedEventIds, showCheckboxes, @@ -130,9 +133,9 @@ const StatefulEventComponent: React.FC = ({ }) => { const [expanded, setExpanded] = useState<{ [eventId: string]: boolean }>({}); const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({}); - const timeline = useSelector((state) => { - return state.timeline.timelineById['timeline-1']; - }); + const { status: timelineStatus } = useSelector( + (state) => state.timeline.timelineById[TimelineId.active] + ); const divElement = useRef(null); const onToggleShowNotes = useCallback(() => { @@ -206,6 +209,7 @@ const StatefulEventComponent: React.FC = ({ onPinEvent={onPinEvent} onRowSelected={onRowSelected} onUnPinEvent={onUnPinEvent} + refetch={refetch} selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} showNotes={!!showNotes[event._id]} @@ -226,7 +230,7 @@ const StatefulEventComponent: React.FC = ({ getNotesByIds={getNotesByIds} noteIds={eventIdToNoteIds[event._id] || emptyNotes} showAddNote={!!showNotes[event._id]} - status={timeline.status} + status={timelineStatus} toggleShowAddNote={onToggleShowNotes} updateNote={updateNote} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx similarity index 67% rename from x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts rename to x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx index b62888fbf84272..5753efa2bf1bb4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx @@ -4,16 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import React, { useCallback, useMemo } from 'react'; import { get, isEmpty } from 'lodash/fp'; -import { Dispatch } from 'redux'; +import { useDispatch } from 'react-redux'; import { Ecs, TimelineItem, TimelineNonEcsData } from '../../../../graphql/types'; -import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers'; import { updateTimelineGraphEventId } from '../../../store/timeline/actions'; -import { EventType } from '../../../../timelines/store/timeline/model'; +import { EventType } from '../../../store/timeline/model'; import { OnPinEvent, OnUnPinEvent } from '../events'; - -import { TimelineRowAction, TimelineRowActionOnClick } from './actions'; +import { ActionIconItem } from './actions/action_icon_item'; import * as i18n from './translations'; import { TimelineTypeLiteral, TimelineType } from '../../../../../common/types/timeline'; @@ -89,8 +88,8 @@ export const getEventIdToDataMapping = ( timelineData: TimelineItem[], eventIds: string[], fieldsToKeep: string[] -): Record => { - return timelineData.reduce((acc, v) => { +): Record => + timelineData.reduce((acc, v) => { const fvm = eventIds.includes(v._id) ? { [v._id]: v.data.filter((ti) => fieldsToKeep.includes(ti.field)) } : {}; @@ -99,7 +98,6 @@ export const getEventIdToDataMapping = ( ...fvm, }; }, {}); -}; /** Return eventType raw or signal */ export const getEventType = (event: Ecs): Omit => { @@ -109,29 +107,40 @@ export const getEventType = (event: Ecs): Omit => { return 'raw'; }; -export const isInvestigateInResolverActionEnabled = (ecsData?: Ecs) => { +export const isInvestigateInResolverActionEnabled = (ecsData?: Ecs) => + get(['agent', 'type', 0], ecsData) === 'endpoint' && + get(['process', 'entity_id'], ecsData)?.length === 1 && + get(['process', 'entity_id', 0], ecsData) !== ''; + +interface InvestigateInResolverActionProps { + timelineId: string; + ecsData: Ecs; +} + +const InvestigateInResolverActionComponent: React.FC = ({ + timelineId, + ecsData, +}) => { + const dispatch = useDispatch(); + const isDisabled = useMemo(() => !isInvestigateInResolverActionEnabled(ecsData), [ecsData]); + const handleClick = useCallback( + () => dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: ecsData._id })), + [dispatch, ecsData._id, timelineId] + ); + return ( - get(['agent', 'type', 0], ecsData) === 'endpoint' && - get(['process', 'entity_id'], ecsData)?.length === 1 && - get(['process', 'entity_id', 0], ecsData) !== '' + ); }; -export const getInvestigateInResolverAction = ({ - dispatch, - timelineId, -}: { - dispatch: Dispatch; - timelineId: string; -}): TimelineRowAction => ({ - ariaLabel: i18n.ACTION_INVESTIGATE_IN_RESOLVER, - content: i18n.ACTION_INVESTIGATE_IN_RESOLVER, - dataTestSubj: 'investigate-in-resolver', - displayType: 'icon', - iconType: 'node', - id: 'investigateInResolver', - isActionDisabled: (ecsData?: Ecs) => !isInvestigateInResolverActionEnabled(ecsData), - onClick: ({ eventId }: TimelineRowActionOnClick) => - dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: eventId })), - width: DEFAULT_ICON_BUTTON_WIDTH, -}); +InvestigateInResolverActionComponent.displayName = 'InvestigateInResolverActionComponent'; + +export const InvestigateInResolverAction = React.memo(InvestigateInResolverActionComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 4eac5360321c14..657e1617e8d24e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -64,7 +64,6 @@ describe('Body', () => { data: mockTimelineData, docValueFields: [], eventIdToNoteIds: {}, - id: 'timeline-test', isSelectAllChecked: false, getNotesByIds: mockGetNotesByIds, loadingEventIds: [], @@ -78,11 +77,13 @@ describe('Body', () => { onUnPinEvent: jest.fn(), onUpdateColumns: jest.fn(), pinnedEventIds: {}, + refetch: jest.fn(), rowRenderers, selectedEventIds: {}, show: true, sort: mockSort, showCheckboxes: false, + timelineId: 'timeline-test', timelineType: TimelineType.default, toggleColumn: jest.fn(), updateNote: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 6f578ffe3e9568..40cc12afde51dc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -6,10 +6,11 @@ import React, { useMemo, useRef } from 'react'; +import { inputsModel } from '../../../../common/store'; import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; import { TimelineItem, TimelineNonEcsData } from '../../../../graphql/types'; import { Note } from '../../../../common/lib/note'; -import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model'; +import { ColumnHeaderOptions, EventType } from '../../../../timelines/store/timeline/model'; import { AddNoteToEvent, UpdateNote } from '../../notes/helpers'; import { OnColumnRemoved, @@ -42,10 +43,10 @@ export interface BodyProps { docValueFields: DocValueFields[]; getNotesByIds: (noteIds: string[]) => Note[]; graphEventId?: string; - id: string; isEventViewer?: boolean; isSelectAllChecked: boolean; eventIdToNoteIds: Readonly>; + eventType?: EventType; loadingEventIds: Readonly; onColumnRemoved: OnColumnRemoved; onColumnResized: OnColumnResized; @@ -57,18 +58,23 @@ export interface BodyProps { onUpdateColumns: OnUpdateColumns; onUnPinEvent: OnUnPinEvent; pinnedEventIds: Readonly>; + refetch: inputsModel.Refetch; rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; show: boolean; showCheckboxes: boolean; sort: Sort; + timelineId: string; timelineType: TimelineType; toggleColumn: (column: ColumnHeaderOptions) => void; updateNote: UpdateNote; } -export const hasAdditonalActions = (id: string): boolean => - id === TimelineId.detectionsPage || id === TimelineId.detectionsRulesDetailsPage; +export const hasAdditionalActions = (id: string, eventType?: EventType): boolean => + id === TimelineId.detectionsPage || + id === TimelineId.detectionsRulesDetailsPage || + ((id === TimelineId.active && eventType && ['all', 'signal', 'alert'].includes(eventType)) ?? + false); const EXTRA_WIDTH = 4; // px @@ -82,9 +88,9 @@ export const Body = React.memo( data, docValueFields, eventIdToNoteIds, + eventType, getNotesByIds, graphEventId, - id, isEventViewer = false, isSelectAllChecked, loadingEventIds, @@ -99,11 +105,13 @@ export const Body = React.memo( onUnPinEvent, pinnedEventIds, rowRenderers, + refetch, selectedEventIds, show, showCheckboxes, sort, toggleColumn, + timelineId, timelineType, updateNote, }) => { @@ -113,9 +121,9 @@ export const Body = React.memo( getActionsColumnWidth( isEventViewer, showCheckboxes, - hasAdditonalActions(id) ? DEFAULT_ICON_BUTTON_WIDTH + EXTRA_WIDTH : 0 + hasAdditionalActions(timelineId, eventType) ? DEFAULT_ICON_BUTTON_WIDTH + EXTRA_WIDTH : 0 ), - [isEventViewer, showCheckboxes, id] + [isEventViewer, showCheckboxes, timelineId, eventType] ); const columnWidths = useMemo( @@ -127,11 +135,15 @@ export const Body = React.memo( return ( <> {graphEventId && ( - + )} @@ -151,7 +163,7 @@ export const Body = React.memo( showEventsSelect={false} showSelectAllCheckbox={showCheckboxes} sort={sort} - timelineId={id} + timelineId={timelineId} toggleColumn={toggleColumn} /> @@ -166,7 +178,7 @@ export const Body = React.memo( docValueFields={docValueFields} eventIdToNoteIds={eventIdToNoteIds} getNotesByIds={getNotesByIds} - id={id} + id={timelineId} isEventViewer={isEventViewer} loadingEventIds={loadingEventIds} onColumnResized={onColumnResized} @@ -175,6 +187,7 @@ export const Body = React.memo( onUpdateColumns={onUpdateColumns} onUnPinEvent={onUnPinEvent} pinnedEventIds={pinnedEventIds} + refetch={refetch} rowRenderers={rowRenderers} selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx index 8deda03ece70eb..9b7b896a2ec691 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx @@ -14,7 +14,7 @@ import { RowRendererId, TimelineId } from '../../../../../common/types/timeline' import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; import { TimelineItem } from '../../../../graphql/types'; import { Note } from '../../../../common/lib/note'; -import { appSelectors, State } from '../../../../common/store'; +import { appSelectors, inputsModel, State } from '../../../../common/store'; import { appActions } from '../../../../common/store/actions'; import { useManageTimeline } from '../../manage_timeline'; import { ColumnHeaderOptions, TimelineModel } from '../../../store/timeline/model'; @@ -46,6 +46,7 @@ interface OwnProps { isEventViewer?: boolean; sort: Sort; toggleColumn: (column: ColumnHeaderOptions) => void; + refetch: inputsModel.Refetch; } type StatefulBodyComponentProps = OwnProps & PropsFromRedux; @@ -61,6 +62,7 @@ const StatefulBodyComponent = React.memo( data, docValueFields, eventIdToNoteIds, + eventType, excludedRowRendererIds, id, isEventViewer = false, @@ -76,6 +78,7 @@ const StatefulBodyComponent = React.memo( show, showCheckboxes, graphEventId, + refetch, sort, timelineType, toggleColumn, @@ -195,9 +198,9 @@ const StatefulBodyComponent = React.memo( data={data} docValueFields={docValueFields} eventIdToNoteIds={eventIdToNoteIds} + eventType={eventType} getNotesByIds={getNotesByIds} graphEventId={graphEventId} - id={id} isEventViewer={isEventViewer} isSelectAllChecked={isSelectAllChecked} loadingEventIds={loadingEventIds} @@ -211,11 +214,13 @@ const StatefulBodyComponent = React.memo( onUnPinEvent={onUnPinEvent} onUpdateColumns={onUpdateColumns} pinnedEventIds={pinnedEventIds} + refetch={refetch} rowRenderers={enabledRowRenderers} selectedEventIds={selectedEventIds} show={id === TimelineId.active ? show : true} showCheckboxes={showCheckboxes} sort={sort} + timelineId={id} timelineType={timelineType} toggleColumn={toggleColumn} updateNote={onUpdateNote} @@ -229,6 +234,7 @@ const StatefulBodyComponent = React.memo( deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) && deepEqual(prevProps.docValueFields, nextProps.docValueFields) && prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && + prevProps.eventType === nextProps.eventType && prevProps.graphEventId === nextProps.graphEventId && deepEqual(prevProps.notesById, nextProps.notesById) && prevProps.id === nextProps.id && diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 8f18a173f3beda..4ab05af5dd6d46 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -331,26 +331,23 @@ const LargeNotesButton = React.memo(({ noteIds, text, tog LargeNotesButton.displayName = 'LargeNotesButton'; interface SmallNotesButtonProps { - noteIds: string[]; toggleShowNotes: () => void; timelineType: TimelineTypeLiteral; } -const SmallNotesButton = React.memo( - ({ noteIds, toggleShowNotes, timelineType }) => { - const isTemplate = timelineType === TimelineType.template; - - return ( - toggleShowNotes()} - isDisabled={isTemplate} - /> - ); - } -); +const SmallNotesButton = React.memo(({ toggleShowNotes, timelineType }) => { + const isTemplate = timelineType === TimelineType.template; + + return ( + toggleShowNotes()} + isDisabled={isTemplate} + /> + ); +}); SmallNotesButton.displayName = 'SmallNotesButton'; /** @@ -375,11 +372,7 @@ const NotesButtonComponent = React.memo( {size === 'l' ? ( ) : ( - + )} {size === 'l' && showNotes ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx index e88ecee81d364f..b5aadaa6f1ef8e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { TimelineType } from '../../../../../common/types/timeline'; +import { TimelineId, TimelineType } from '../../../../../common/types/timeline'; import { useKibana } from '../../../../common/lib/kibana'; import { useCreateTimelineButton } from './use_create_timeline'; @@ -22,7 +22,7 @@ export const NewTemplateTimelineComponent: React.FC = ({ closeGearMenu, outline, title, - timelineId = 'timeline-1', + timelineId = TimelineId.active, }) => { const uiCapabilities = useKibana().services.application.capabilities; const capabilitiesCanUserCRUD: boolean = !!uiCapabilities.siem.crud; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx index a2ee1e56306b53..7b1c1bd2119cd3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx @@ -7,7 +7,6 @@ import { EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter, EuiProgress } from '@elastic/eui'; import { getOr, isEmpty } from 'lodash/fp'; import React, { useState, useMemo, useEffect } from 'react'; -import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { FlyoutHeaderWithCloseButton } from '../flyout/header_with_close_button'; @@ -17,7 +16,6 @@ import { Direction } from '../../../graphql/types'; import { useKibana } from '../../../common/lib/kibana'; import { ColumnHeaderOptions, KqlMode, EventType } from '../../../timelines/store/timeline/model'; import { defaultHeaders } from './body/column_headers/default_headers'; -import { getInvestigateInResolverAction } from './body/helpers'; import { Sort } from './body/sort'; import { StatefulBody } from './body/stateful_body'; import { DataProvider } from './data_providers/data_provider'; @@ -43,6 +41,7 @@ import { } from '../../../../../../../src/plugins/data/public'; import { useManageTimeline } from '../manage_timeline'; import { TimelineType, TimelineStatusLiteral } from '../../../../common/types/timeline'; +import { requiredFieldsForActions } from '../../../detections/components/alerts_table/default_config'; const TimelineContainer = styled.div` height: 100%; @@ -168,7 +167,6 @@ export const TimelineComponent: React.FC = ({ toggleColumn, usersViewing, }) => { - const dispatch = useDispatch(); const kibana = useKibana(); const [filterManager] = useState(new FilterManager(kibana.services.uiSettings)); const esQueryConfig = useMemo(() => esQuery.getEsQueryConfig(kibana.services.uiSettings), [ @@ -213,7 +211,10 @@ export const TimelineComponent: React.FC = ({ [isLoadingSource, combinedQueries, start, end] ); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; - const timelineQueryFields = useMemo(() => columnsHeader.map((c) => c.id), [columnsHeader]); + const timelineQueryFields = useMemo(() => { + const columnFields = columnsHeader.map((c) => c.id); + return [...columnFields, ...requiredFieldsForActions]; + }, [columnsHeader]); const timelineQuerySortField = useMemo( () => ({ sortFieldId: sort.columnId, @@ -228,7 +229,6 @@ export const TimelineComponent: React.FC = ({ filterManager, id, indexToAdd, - timelineRowActions: () => [getInvestigateInResolverAction({ dispatch, timelineId: id })], }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -317,6 +317,7 @@ export const TimelineComponent: React.FC = ({ data={events} docValueFields={docValueFields} id={id} + refetch={refetch} sort={sort} toggleColumn={toggleColumn} /> diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts index 5a162fd2206a1b..c67ad45bede94a 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts @@ -200,6 +200,7 @@ export const timelineQuery = gql` country_iso_code } signal { + status original_time rule { id diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx index 1d2e16b3fe5b8b..79d0f909c7d592 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useState } from 'react'; import styled from 'styled-components'; import { useParams } from 'react-router-dom'; -import { TimelineType } from '../../../common/types/timeline'; +import { TimelineId, TimelineType } from '../../../common/types/timeline'; import { HeaderPage } from '../../common/components/header_page'; import { WrapperPage } from '../../common/components/wrapper_page'; import { useKibana } from '../../common/lib/kibana'; @@ -65,7 +65,7 @@ export const TimelinesPageComponent: React.FC = () => { {tabName === TimelineType.default ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index 06dd6f44bea94a..8c3f30c75c35b7 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -42,7 +42,7 @@ import { Direction } from '../../../graphql/types'; import { addTimelineInStorage } from '../../containers/local_storage'; import { isPageTimeline } from './epic_local_storage'; -import { TimelineStatus, TimelineType } from '../../../../common/types/timeline'; +import { TimelineId, TimelineStatus, TimelineType } from '../../../../common/types/timeline'; jest.mock('../../containers/local_storage'); @@ -115,7 +115,7 @@ describe('epicLocalStorage', () => { }); it('filters correctly page timelines', () => { - expect(isPageTimeline('timeline-1')).toBe(false); + expect(isPageTimeline(TimelineId.active)).toBe(false); expect(isPageTimeline('hosts-page-alerts')).toBe(true); }); diff --git a/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts index bdc69f85d35429..60c2ce8ceca64f 100644 --- a/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts @@ -424,6 +424,7 @@ export const ecsSchema = gql` type SignalField { rule: RuleField original_time: ToStringArray + status: ToStringArray } type RuleEcsField { diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index fa55af351651e7..7638ebd03f6b16 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -1022,6 +1022,8 @@ export interface SignalField { rule?: Maybe; original_time?: Maybe; + + status?: Maybe; } export interface RuleField { @@ -4930,6 +4932,8 @@ export namespace SignalFieldResolvers { rule?: RuleResolver, TypeParent, TContext>; original_time?: OriginalTimeResolver, TypeParent, TContext>; + + status?: StatusResolver, TypeParent, TContext>; } export type RuleResolver< @@ -4942,6 +4946,11 @@ export namespace SignalFieldResolvers { Parent = SignalField, TContext = SiemContext > = Resolver; + export type StatusResolver< + R = Maybe, + Parent = SignalField, + TContext = SiemContext + > = Resolver; } export namespace RuleFieldResolvers { diff --git a/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts b/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts index 19b16bd4bc6d24..d1c8290b3462d7 100644 --- a/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts +++ b/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts @@ -324,6 +324,7 @@ export const signalFieldsMap: Readonly> = { 'signal.rule.note': 'signal.rule.note', 'signal.rule.threshold': 'signal.rule.threshold', 'signal.rule.exceptions_list': 'signal.rule.exceptions_list', + 'signal.status': 'signal.status', }; export const ruleFieldsMap: Readonly> = { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts index 245146dda183fe..90d5b538a52000 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts @@ -5,7 +5,7 @@ */ import { omit } from 'lodash/fp'; -import { TimelineType, TimelineStatus } from '../../../../../common/types/timeline'; +import { TimelineId, TimelineType, TimelineStatus } from '../../../../../common/types/timeline'; export const mockDuplicateIdErrors = []; @@ -332,8 +332,7 @@ export const mockCheckTimelinesStatusBeforeInstallResult = { value: '3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', operator: ':', }, - id: - 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', + id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-signal-id-3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509`, enabled: true, }, ], @@ -496,8 +495,7 @@ export const mockCheckTimelinesStatusBeforeInstallResult = { value: '30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d', operator: ':', }, - id: - 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d', + id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-signal-id-30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d`, enabled: true, }, ], @@ -675,8 +673,7 @@ export const mockCheckTimelinesStatusBeforeInstallResult = { value: '590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde', operator: ':', }, - id: - 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde', + id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-signal-id-590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde`, enabled: true, }, ], @@ -848,8 +845,7 @@ export const mockCheckTimelinesStatusAfterInstallResult = { value: '30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d', operator: ':', }, - id: - 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d', + id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-signal-id-30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d`, enabled: true, }, ], @@ -1031,8 +1027,7 @@ export const mockCheckTimelinesStatusAfterInstallResult = { value: '590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde', operator: ':', }, - id: - 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde', + id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-signal-id-590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde`, enabled: true, }, ], @@ -1152,8 +1147,7 @@ export const mockCheckTimelinesStatusAfterInstallResult = { value: '3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', operator: ':', }, - id: - 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', + id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-signal-id-3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509`, enabled: true, }, ], From 1c234bfd25124cfa2b7af5a47f7c595623f5dac9 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Tue, 1 Sep 2020 15:33:23 -0400 Subject: [PATCH 156/216] [SECURITY_SOLUTION][ENDPOINT] Trusted Apps Create API (#76178) * Create Trusted App API --- .../common/endpoint/constants.ts | 1 + .../endpoint/schema/trusted_apps.test.ts | 178 +++++++++++++++++- .../common/endpoint/schema/trusted_apps.ts | 17 ++ .../common/endpoint/types/trusted_apps.ts | 11 +- .../endpoint/routes/trusted_apps/handlers.ts | 34 +++- .../endpoint/routes/trusted_apps/index.ts | 22 ++- .../routes/trusted_apps/trusted_apps.test.ts | 119 +++++++++++- .../endpoint/routes/trusted_apps/utils.ts | 32 +++- 8 files changed, 404 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 507ce63c7b815f..b72a52f0a0eb72 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -13,3 +13,4 @@ export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurre export const LIMITED_CONCURRENCY_ENDPOINT_COUNT = 100; export const TRUSTED_APPS_LIST_API = '/api/endpoint/trusted_apps'; +export const TRUSTED_APPS_CREATE_API = '/api/endpoint/trusted_apps'; diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts index 7aec8e15c317ca..b0c769216732d2 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { GetTrustedAppsRequestSchema } from './trusted_apps'; +import { GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema } from './trusted_apps'; describe('When invoking Trusted Apps Schema', () => { describe('for GET List', () => { @@ -68,4 +68,180 @@ describe('When invoking Trusted Apps Schema', () => { }); }); }); + + describe('for POST Create', () => { + const getCreateTrustedAppItem = () => ({ + name: 'Some Anti-Virus App', + description: 'this one is ok', + os: 'windows', + entries: [ + { + field: 'path', + type: 'match', + operator: 'included', + value: 'c:/programs files/Anti-Virus', + }, + ], + }); + const body = PostTrustedAppCreateRequestSchema.body; + + it('should not error on a valid message', () => { + const bodyMsg = getCreateTrustedAppItem(); + expect(body.validate(bodyMsg)).toStrictEqual(bodyMsg); + }); + + it('should validate `name` is required', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + name: undefined, + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + + it('should validate `name` value to be non-empty', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + name: '', + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + + it('should validate `description` as optional', () => { + const { description, ...bodyMsg } = getCreateTrustedAppItem(); + expect(body.validate(bodyMsg)).toStrictEqual(bodyMsg); + }); + + it('should validate `description` to be non-empty if defined', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + description: '', + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + + it('should validate `os` to to only accept known values', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + os: undefined, + }; + expect(() => body.validate(bodyMsg)).toThrow(); + + const bodyMsg2 = { + ...bodyMsg, + os: '', + }; + expect(() => body.validate(bodyMsg2)).toThrow(); + + const bodyMsg3 = { + ...bodyMsg, + os: 'winz', + }; + expect(() => body.validate(bodyMsg3)).toThrow(); + + ['linux', 'macos', 'windows'].forEach((os) => { + expect(() => { + body.validate({ + ...bodyMsg, + os, + }); + }).not.toThrow(); + }); + }); + + it('should validate `entries` as required', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + entries: undefined, + }; + expect(() => body.validate(bodyMsg)).toThrow(); + + const { entries, ...bodyMsg2 } = getCreateTrustedAppItem(); + expect(() => body.validate(bodyMsg2)).toThrow(); + }); + + it('should validate `entries` to have at least 1 item', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + entries: [], + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + + describe('when `entries` are defined', () => { + const getTrustedAppItemEntryItem = () => getCreateTrustedAppItem().entries[0]; + + it('should validate `entry.field` is required', () => { + const { field, ...entry } = getTrustedAppItemEntryItem(); + const bodyMsg = { + ...getCreateTrustedAppItem(), + entries: [entry], + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + + it('should validate `entry.field` is limited to known values', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + entries: [ + { + ...getTrustedAppItemEntryItem(), + field: '', + }, + ], + }; + expect(() => body.validate(bodyMsg)).toThrow(); + + const bodyMsg2 = { + ...getCreateTrustedAppItem(), + entries: [ + { + ...getTrustedAppItemEntryItem(), + field: 'invalid value', + }, + ], + }; + expect(() => body.validate(bodyMsg2)).toThrow(); + + ['hash', 'path'].forEach((field) => { + const bodyMsg3 = { + ...getCreateTrustedAppItem(), + entries: [ + { + ...getTrustedAppItemEntryItem(), + field, + }, + ], + }; + + expect(() => body.validate(bodyMsg3)).not.toThrow(); + }); + }); + + it.todo('should validate `entry.type` is limited to known values'); + + it.todo('should validate `entry.operator` is limited to known values'); + + it('should validate `entry.value` required', () => { + const { value, ...entry } = getTrustedAppItemEntryItem(); + const bodyMsg = { + ...getCreateTrustedAppItem(), + entries: [entry], + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + + it('should validate `entry.value` is non-empty', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + entries: [ + { + ...getTrustedAppItemEntryItem(), + value: '', + }, + ], + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts index 20fab93aaf304a..7535b23a10e8af 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts @@ -12,3 +12,20 @@ export const GetTrustedAppsRequestSchema = { per_page: schema.maybe(schema.number({ defaultValue: 20, min: 1 })), }), }; + +export const PostTrustedAppCreateRequestSchema = { + body: schema.object({ + name: schema.string({ minLength: 1 }), + description: schema.maybe(schema.string({ minLength: 1 })), + os: schema.oneOf([schema.literal('linux'), schema.literal('macos'), schema.literal('windows')]), + entries: schema.arrayOf( + schema.object({ + field: schema.oneOf([schema.literal('hash'), schema.literal('path')]), + type: schema.literal('match'), + operator: schema.literal('included'), + value: schema.string({ minLength: 1 }), + }), + { minSize: 1 } + ), + }), +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index 2905274bef1cb0..7aeb6c6024b993 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -5,7 +5,10 @@ */ import { TypeOf } from '@kbn/config-schema'; -import { GetTrustedAppsRequestSchema } from '../schema/trusted_apps'; +import { + GetTrustedAppsRequestSchema, + PostTrustedAppCreateRequestSchema, +} from '../schema/trusted_apps'; /** API request params for retrieving a list of Trusted Apps */ export type GetTrustedAppsListRequest = TypeOf; @@ -16,6 +19,12 @@ export interface GetTrustedListAppsResponse { data: TrustedApp[]; } +/** API Request body for creating a new Trusted App entry */ +export type PostTrustedAppCreateRequest = TypeOf; +export interface PostTrustedAppCreateResponse { + data: TrustedApp; +} + interface MacosLinuxConditionEntry { field: 'hash' | 'path'; type: 'match'; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts index 6c29a2244c203d..977683ab55495a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts @@ -8,9 +8,10 @@ import { RequestHandler } from 'kibana/server'; import { GetTrustedAppsListRequest, GetTrustedListAppsResponse, + PostTrustedAppCreateRequest, } from '../../../../common/endpoint/types'; import { EndpointAppContext } from '../../types'; -import { exceptionItemToTrustedAppItem } from './utils'; +import { exceptionItemToTrustedAppItem, newTrustedAppItemToExceptionItem } from './utils'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; export const getTrustedAppsListRouteHandler = ( @@ -24,7 +25,7 @@ export const getTrustedAppsListRouteHandler = ( try { // Ensure list is created if it does not exist - await exceptionsListService?.createTrustedAppsList(); + await exceptionsListService.createTrustedAppsList(); const results = await exceptionsListService.findExceptionListItem({ listId: ENDPOINT_TRUSTED_APPS_LIST_ID, page, @@ -47,3 +48,32 @@ export const getTrustedAppsListRouteHandler = ( } }; }; + +export const getTrustedAppsCreateRouteHandler = ( + endpointAppContext: EndpointAppContext +): RequestHandler => { + const logger = endpointAppContext.logFactory.get('trusted_apps'); + + return async (constext, req, res) => { + const exceptionsListService = endpointAppContext.service.getExceptionsList(); + const newTrustedApp = req.body; + + try { + // Ensure list is created if it does not exist + await exceptionsListService.createTrustedAppsList(); + + const createdTrustedAppExceptionItem = await exceptionsListService.createExceptionListItem( + newTrustedAppItemToExceptionItem(newTrustedApp) + ); + + return res.ok({ + body: { + data: exceptionItemToTrustedAppItem(createdTrustedAppExceptionItem), + }, + }); + } catch (error) { + logger.error(error); + return res.internalError({ body: error }); + } + }; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts index 178aa06eee8775..1302b10533ccf6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts @@ -5,9 +5,15 @@ */ import { IRouter } from 'kibana/server'; -import { GetTrustedAppsRequestSchema } from '../../../../common/endpoint/schema/trusted_apps'; -import { TRUSTED_APPS_LIST_API } from '../../../../common/endpoint/constants'; -import { getTrustedAppsListRouteHandler } from './handlers'; +import { + GetTrustedAppsRequestSchema, + PostTrustedAppCreateRequestSchema, +} from '../../../../common/endpoint/schema/trusted_apps'; +import { + TRUSTED_APPS_CREATE_API, + TRUSTED_APPS_LIST_API, +} from '../../../../common/endpoint/constants'; +import { getTrustedAppsCreateRouteHandler, getTrustedAppsListRouteHandler } from './handlers'; import { EndpointAppContext } from '../../types'; export const registerTrustedAppsRoutes = ( @@ -23,4 +29,14 @@ export const registerTrustedAppsRoutes = ( }, getTrustedAppsListRouteHandler(endpointAppContext) ); + + // CREATE + router.post( + { + path: TRUSTED_APPS_CREATE_API, + validate: PostTrustedAppCreateRequestSchema, + options: { authRequired: true }, + }, + getTrustedAppsCreateRouteHandler(endpointAppContext) + ); }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts index 1d4a7919b89f53..488c8390411b04 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts @@ -12,12 +12,20 @@ import { import { IRouter, RequestHandler } from 'kibana/server'; import { httpServerMock, httpServiceMock } from '../../../../../../../src/core/server/mocks'; import { registerTrustedAppsRoutes } from './index'; -import { TRUSTED_APPS_LIST_API } from '../../../../common/endpoint/constants'; -import { GetTrustedAppsListRequest } from '../../../../common/endpoint/types'; +import { + TRUSTED_APPS_CREATE_API, + TRUSTED_APPS_LIST_API, +} from '../../../../common/endpoint/constants'; +import { + GetTrustedAppsListRequest, + PostTrustedAppCreateRequest, +} from '../../../../common/endpoint/types'; import { xpackMocks } from '../../../../../../mocks'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; import { EndpointAppContext } from '../../types'; import { ExceptionListClient } from '../../../../../lists/server'; +import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { ExceptionListItemSchema } from '../../../../../lists/common/schemas/response'; describe('when invoking endpoint trusted apps route handlers', () => { let routerMock: jest.Mocked; @@ -105,4 +113,111 @@ describe('when invoking endpoint trusted apps route handlers', () => { expect(endpointAppContext.logFactory.get('trusted_apps').error).toHaveBeenCalled(); }); }); + + describe('when creating a trusted app', () => { + let routeHandler: RequestHandler; + const createNewTrustedAppBody = (): PostTrustedAppCreateRequest => ({ + name: 'Some Anti-Virus App', + description: 'this one is ok', + os: 'windows', + entries: [ + { + field: 'path', + type: 'match', + operator: 'included', + value: 'c:/programs files/Anti-Virus', + }, + ], + }); + const createPostRequest = () => { + return httpServerMock.createKibanaRequest({ + path: TRUSTED_APPS_LIST_API, + method: 'post', + body: createNewTrustedAppBody(), + }); + }; + + beforeEach(() => { + // Get the registered POST handler from the IRouter instance + [, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => + path.startsWith(TRUSTED_APPS_CREATE_API) + )!; + + // Mock the impelementation of `createExceptionListItem()` so that the return value + // merges in the provided input + exceptionsListClient.createExceptionListItem.mockImplementation(async (newExceptionItem) => { + return ({ + ...getExceptionListItemSchemaMock(), + ...newExceptionItem, + } as unknown) as ExceptionListItemSchema; + }); + }); + + it('should create trusted app list first', async () => { + const request = createPostRequest(); + await routeHandler(context, request, response); + expect(exceptionsListClient.createTrustedAppsList).toHaveBeenCalled(); + expect(response.ok).toHaveBeenCalled(); + }); + + it('should map new trusted app item to an exception list item', async () => { + const request = createPostRequest(); + await routeHandler(context, request, response); + expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0]).toEqual({ + _tags: ['os:windows'], + comments: [], + description: 'this one is ok', + entries: [ + { + field: 'path', + operator: 'included', + type: 'match', + value: 'c:/programs files/Anti-Virus', + }, + ], + itemId: expect.stringMatching(/.*/), + listId: 'endpoint_trusted_apps', + meta: undefined, + name: 'Some Anti-Virus App', + namespaceType: 'agnostic', + tags: [], + type: 'simple', + }); + }); + + it('should return new trusted app item', async () => { + const request = createPostRequest(); + await routeHandler(context, request, response); + expect(response.ok.mock.calls[0][0]).toEqual({ + body: { + data: { + created_at: '2020-04-20T15:25:31.830Z', + created_by: 'some user', + description: 'this one is ok', + entries: [ + { + field: 'path', + operator: 'included', + type: 'match', + value: 'c:/programs files/Anti-Virus', + }, + ], + id: '1', + name: 'Some Anti-Virus App', + os: 'windows', + }, + }, + }); + }); + + it('should log unexpected error if one occurs', async () => { + exceptionsListClient.createExceptionListItem.mockImplementation(() => { + throw new Error('expected error for create'); + }); + const request = createPostRequest(); + await routeHandler(context, request, response); + expect(response.internalError).toHaveBeenCalled(); + expect(endpointAppContext.logFactory.get('trusted_apps').error).toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts index 2b417a4c6a8e16..794c1db4b49aa8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts @@ -4,8 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import uuid from 'uuid'; import { ExceptionListItemSchema } from '../../../../../lists/common/shared_exports'; -import { TrustedApp } from '../../../../common/endpoint/types'; +import { NewTrustedApp, TrustedApp } from '../../../../common/endpoint/types'; +import { ExceptionListClient } from '../../../../../lists/server'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; + +type NewExecptionItem = Parameters[0]; /** * Map an ExcptionListItem to a TrustedApp item @@ -40,3 +45,28 @@ const osFromTagsList = (tags: string[]): TrustedApp['os'] | 'unknown' => { } return 'unknown'; }; + +export const newTrustedAppItemToExceptionItem = ({ + os, + entries, + name, + description = '', +}: NewTrustedApp): NewExecptionItem => { + return { + _tags: tagsListFromOs(os), + comments: [], + description, + entries, + itemId: uuid.v4(), + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + meta: undefined, + name, + namespaceType: 'agnostic', + tags: [], + type: 'simple', + }; +}; + +const tagsListFromOs = (os: NewTrustedApp['os']): NewExecptionItem['_tags'] => { + return [`os:${os}`]; +}; From 4705755b3b5bf2a1d303406091be1e572d6c751b Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 1 Sep 2020 15:51:55 -0400 Subject: [PATCH 157/216] Delete unused file. (#76386) --- x-pack/plugins/uptime/public/breadcrumbs.ts | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 x-pack/plugins/uptime/public/breadcrumbs.ts diff --git a/x-pack/plugins/uptime/public/breadcrumbs.ts b/x-pack/plugins/uptime/public/breadcrumbs.ts deleted file mode 100644 index 41bc2aa2588073..00000000000000 --- a/x-pack/plugins/uptime/public/breadcrumbs.ts +++ /dev/null @@ -1,5 +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. - */ From 2ed920021e83e9809b3697960a7be94d7c9918a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Tue, 1 Sep 2020 21:55:14 +0200 Subject: [PATCH 158/216] Create APM issue template (#76362) --- .github/ISSUE_TEMPLATE/APM.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/APM.md diff --git a/.github/ISSUE_TEMPLATE/APM.md b/.github/ISSUE_TEMPLATE/APM.md new file mode 100644 index 00000000000000..983806f70bc3fa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/APM.md @@ -0,0 +1,11 @@ +--- +name: APM Issue +about: Issues related to the APM solution in Kibana +labels: Team:apm +title: [APM] +--- + +**Versions** +Kibana: (if relevant) +APM Server: (if relevant) +Elasticsearch: (if relevant) From f6aa79853556e863e9323c26fc57fa2ef06da380 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Tue, 1 Sep 2020 15:50:29 -0500 Subject: [PATCH 159/216] remove dupe tinymath section (#76093) --- .../canvas/canvas-tinymath-functions.asciidoc | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/docs/canvas/canvas-tinymath-functions.asciidoc b/docs/canvas/canvas-tinymath-functions.asciidoc index 73808fc6625d12..f92f7c642a2ee3 100644 --- a/docs/canvas/canvas-tinymath-functions.asciidoc +++ b/docs/canvas/canvas-tinymath-functions.asciidoc @@ -492,37 +492,6 @@ find the mean by index. |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.`. The maximum value of all numbers if -`args` contains only numbers. Returns an array with the the maximum values at each -index, including all scalar numbers in `args` in the calculation at each index if -`args` contains at least one array. - -*Throws*: `'Array length mismatch'` if `args` contains arrays of different lengths - -*Example* -[source, js] ------------- -max(1, 2, 3) // returns 3 -max([10, 20, 30, 40], 15) // returns [15, 20, 30, 40] -max([1, 9], 4, [3, 5]) // returns [max([1, 4, 3]), max([9, 4, 5])] = [4, 9] ------------- - -[float] -=== mean( ...args ) - -Finds the mean value of one of more numbers/arrays of numbers passed into the function. -If at least one array of numbers is passed into the function, the function will -find the mean by index. - -[cols="3*^<"] -|=== -|Param |Type |Description - -|...args -|number \| Array. -|one or more numbers or arrays of numbers -|=== - *Returns*: `number` | `Array.`. The mean value of all numbers if `args` contains only numbers. Returns an array with the the mean values of each index, including all scalar numbers in `args` in the calculation at each index if `args` From b5faf41b04201fc3081efea15480df7bbc1a1789 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Tue, 1 Sep 2020 13:59:11 -0700 Subject: [PATCH 160/216] Manually building `KueryNode` for Fleet's routes (#75693) * Simple benchmark tests for kuery * Building manually is "better" still not free * Building the KueryNode manually * Removing benchmark tests * Another query is building the KueryNode manually * Empty strings are inherently falsy * No longer reaching into the data plugin, import from the "root" indexes * Using AGENT_ACTION_SAVED_OBJECT_TYPE everywhere * Adding SavedObjectsRepository#find unit test for KueryNode * Adding KQL KueryNode test for validateConvertFilterToKueryNode * /s/KQL string/KQL expression * Updating API docs * Adding micro benchmark * Revert "Adding micro benchmark" This reverts commit 97e19c0bf37b03f3740fe7c00e0613fbbfe4600a. * Adding an empty string filters test Co-authored-by: Elastic Machine --- ...e-public.savedobjectsfindoptions.filter.md | 2 +- ...gin-core-public.savedobjectsfindoptions.md | 2 +- ...e-server.savedobjectsfindoptions.filter.md | 2 +- ...gin-core-server.savedobjectsfindoptions.md | 2 +- src/core/public/public.api.md | 4 +- .../service/lib/filter_utils.test.ts | 14 +++++- .../saved_objects/service/lib/filter_utils.ts | 7 +-- .../service/lib/repository.test.js | 45 ++++++++++++++++++- src/core/server/saved_objects/types.ts | 5 ++- src/core/server/server.api.md | 4 +- .../server/services/agents/actions.ts | 37 ++++++++++++++- 11 files changed, 110 insertions(+), 14 deletions(-) diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.filter.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.filter.md index 900f8e333f3376..2c20fe2dab00f8 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.filter.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.filter.md @@ -7,5 +7,5 @@ Signature: ```typescript -filter?: string; +filter?: string | KueryNode; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md index ebd0a99531755b..903462ac3039d5 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md @@ -17,7 +17,7 @@ export interface SavedObjectsFindOptions | --- | --- | --- | | [defaultSearchOperator](./kibana-plugin-core-public.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | | | [fields](./kibana-plugin-core-public.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | -| [filter](./kibana-plugin-core-public.savedobjectsfindoptions.filter.md) | string | | +| [filter](./kibana-plugin-core-public.savedobjectsfindoptions.filter.md) | string | KueryNode | | | [hasReference](./kibana-plugin-core-public.savedobjectsfindoptions.hasreference.md) | {
type: string;
id: string;
} | | | [namespaces](./kibana-plugin-core-public.savedobjectsfindoptions.namespaces.md) | string[] | | | [page](./kibana-plugin-core-public.savedobjectsfindoptions.page.md) | number | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.filter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.filter.md index ae7b7a28bcd09a..c98a4fe5e8796f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.filter.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.filter.md @@ -7,5 +7,5 @@ Signature: ```typescript -filter?: string; +filter?: string | KueryNode; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md index 15a9d99b3d062e..804c83f7c1b48f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md @@ -17,7 +17,7 @@ export interface SavedObjectsFindOptions | --- | --- | --- | | [defaultSearchOperator](./kibana-plugin-core-server.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | | | [fields](./kibana-plugin-core-server.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | -| [filter](./kibana-plugin-core-server.savedobjectsfindoptions.filter.md) | string | | +| [filter](./kibana-plugin-core-server.savedobjectsfindoptions.filter.md) | string | KueryNode | | | [hasReference](./kibana-plugin-core-server.savedobjectsfindoptions.hasreference.md) | {
type: string;
id: string;
} | | | [namespaces](./kibana-plugin-core-server.savedobjectsfindoptions.namespaces.md) | string[] | | | [page](./kibana-plugin-core-server.savedobjectsfindoptions.page.md) | number | | diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 570732fa6e5d66..bacbd6e757114a 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1189,8 +1189,10 @@ export interface SavedObjectsFindOptions { // (undocumented) defaultSearchOperator?: 'AND' | 'OR'; fields?: string[]; + // Warning: (ae-forgotten-export) The symbol "KueryNode" needs to be exported by the entry point index.d.ts + // // (undocumented) - filter?: string; + filter?: string | KueryNode; // (undocumented) hasReference?: { type: string; diff --git a/src/core/server/saved_objects/service/lib/filter_utils.test.ts b/src/core/server/saved_objects/service/lib/filter_utils.test.ts index 4d9bcdda3c8ae9..60e8aa0afdda4c 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.test.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.test.ts @@ -83,7 +83,19 @@ const mockMappings = { describe('Filter Utils', () => { describe('#validateConvertFilterToKueryNode', () => { - test('Validate a simple filter', () => { + test('Empty string filters are ignored', () => { + expect(validateConvertFilterToKueryNode(['foo'], '', mockMappings)).toBeUndefined(); + }); + test('Validate a simple KQL KueryNode filter', () => { + expect( + validateConvertFilterToKueryNode( + ['foo'], + esKuery.nodeTypes.function.buildNode('is', `foo.attributes.title`, 'best', true), + mockMappings + ) + ).toEqual(esKuery.fromKueryExpression('foo.title: "best"')); + }); + test('Validate a simple KQL expression filter', () => { expect( validateConvertFilterToKueryNode(['foo'], 'foo.attributes.title: "best"', mockMappings) ).toEqual(esKuery.fromKueryExpression('foo.title: "best"')); diff --git a/src/core/server/saved_objects/service/lib/filter_utils.ts b/src/core/server/saved_objects/service/lib/filter_utils.ts index 5fbe62a074b29b..d19f06d74e4193 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.ts @@ -28,11 +28,12 @@ const astFunctionType = ['is', 'range', 'nested']; export const validateConvertFilterToKueryNode = ( allowedTypes: string[], - filter: string, + filter: string | KueryNode, indexMapping: IndexMapping ): KueryNode | undefined => { - if (filter && filter.length > 0 && indexMapping) { - const filterKueryNode = esKuery.fromKueryExpression(filter); + if (filter && indexMapping) { + const filterKueryNode = + typeof filter === 'string' ? esKuery.fromKueryExpression(filter) : filter; const validationFilterKuery = validateFilterKueryNode({ astFilter: filterKueryNode, diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 39433981dfd596..b1d6028465713f 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -25,6 +25,8 @@ import { encodeHitVersion } from '../../version'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { DocumentMigrator } from '../../migrations/core/document_migrator'; import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { nodeTypes } from '../../../../../plugins/data/common/es_query'; jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() })); @@ -2529,7 +2531,7 @@ describe('SavedObjectsRepository', () => { expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, relevantOpts); }); - it(`accepts KQL filter and passes kueryNode to getSearchDsl`, async () => { + it(`accepts KQL expression filter and passes KueryNode to getSearchDsl`, async () => { const findOpts = { namespace, search: 'foo*', @@ -2570,6 +2572,47 @@ describe('SavedObjectsRepository', () => { `); }); + it(`accepts KQL KueryNode filter and passes KueryNode to getSearchDsl`, async () => { + const findOpts = { + namespace, + search: 'foo*', + searchFields: ['foo'], + type: ['dashboard'], + sortField: 'name', + sortOrder: 'desc', + defaultSearchOperator: 'AND', + hasReference: { + type: 'foo', + id: '1', + }, + indexPattern: undefined, + filter: nodeTypes.function.buildNode('is', `dashboard.attributes.otherField`, '*'), + }; + + await findSuccess(findOpts, namespace); + const { kueryNode } = getSearchDslNS.getSearchDsl.mock.calls[0][2]; + expect(kueryNode).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "dashboard.otherField", + }, + Object { + "type": "wildcard", + "value": "@kuery-wildcard@", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + } + `); + }); + it(`supports multiple types`, async () => { const types = ['config', 'index-pattern']; await findSuccess({ type: types }); diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index edbdbe4d167848..000153cd542fa9 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -39,6 +39,9 @@ import { SavedObjectUnsanitizedDoc } from './serialization'; import { SavedObjectsMigrationLogger } from './migrations/core/migration_logger'; import { SavedObject } from '../../types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { KueryNode } from '../../../plugins/data/common'; + export { SavedObjectAttributes, SavedObjectAttribute, @@ -89,7 +92,7 @@ export interface SavedObjectsFindOptions { rootSearchFields?: string[]; hasReference?: { type: string; id: string }; defaultSearchOperator?: 'AND' | 'OR'; - filter?: string; + filter?: string | KueryNode; namespaces?: string[]; /** An optional ES preference value to be used for the query **/ preference?: string; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index fb4e4494801ed6..05afad5a4f7a41 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2320,8 +2320,10 @@ export interface SavedObjectsFindOptions { // (undocumented) defaultSearchOperator?: 'AND' | 'OR'; fields?: string[]; + // Warning: (ae-forgotten-export) The symbol "KueryNode" needs to be exported by the entry point index.d.ts + // // (undocumented) - filter?: string; + filter?: string | KueryNode; // (undocumented) hasReference?: { type: string; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts index 8d1b320c89ae62..cd0dd921312306 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts @@ -9,6 +9,7 @@ import { Agent, AgentAction, AgentActionSOAttributes } from '../../../common/typ import { AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { savedObjectToAgentAction } from './saved_objects'; import { appContextService } from '../app_context'; +import { nodeTypes } from '../../../../../../src/plugins/data/common'; export async function createAgentAction( soClient: SavedObjectsClientContract, @@ -29,9 +30,24 @@ export async function getAgentActionsForCheckin( soClient: SavedObjectsClientContract, agentId: string ): Promise { + const filter = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode( + 'not', + nodeTypes.function.buildNode( + 'is', + `${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.sent_at`, + '*' + ) + ), + nodeTypes.function.buildNode( + 'is', + `${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.agent_id`, + agentId + ), + ]); const res = await soClient.find({ type: AGENT_ACTION_SAVED_OBJECT_TYPE, - filter: `not ${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.sent_at: * and ${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.agent_id:${agentId}`, + filter, }); return Promise.all( @@ -78,9 +94,26 @@ export async function getAgentActionByIds( } export async function getNewActionsSince(soClient: SavedObjectsClientContract, timestamp: string) { + const filter = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode( + 'not', + nodeTypes.function.buildNode( + 'is', + `${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.sent_at`, + '*' + ) + ), + nodeTypes.function.buildNode( + 'range', + `${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.created_at`, + { + gte: timestamp, + } + ), + ]); const res = await soClient.find({ type: AGENT_ACTION_SAVED_OBJECT_TYPE, - filter: `not ${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.sent_at: * AND ${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.created_at >= "${timestamp}"`, + filter, }); return res.saved_objects.map(savedObjectToAgentAction); From 030d5e1390c87f3e35e47ae6a597e5d68cedc26e Mon Sep 17 00:00:00 2001 From: John Schulz Date: Tue, 1 Sep 2020 17:28:23 -0400 Subject: [PATCH 161/216] [Ingest Manager] Improve agent vs kibana version checks (#76238) * Add logic spec'd in issue comments. Tests pass. * Change fn to accept 1 (opt 2) string vs object * Add tests based on issue comments * Change expected error message in test * Capitalize Kibana in error message Co-authored-by: Elastic Machine --- .../server/services/agents/enroll.test.ts | 70 ++++++++----------- .../server/services/agents/enroll.ts | 61 +++++++++++----- .../apis/fleet/agents/enroll.ts | 2 +- 3 files changed, 75 insertions(+), 58 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/agents/enroll.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/enroll.test.ts index 764564cfa49f5b..44473bf2d8d793 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/enroll.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/enroll.test.ts @@ -5,62 +5,52 @@ */ import { validateAgentVersion } from './enroll'; -import { appContextService } from '../app_context'; -import { IngestManagerAppContext } from '../../plugin'; describe('validateAgentVersion', () => { it('should throw with agent > kibana version', () => { - appContextService.start(({ - kibanaVersion: '8.0.0', - } as unknown) as IngestManagerAppContext); - expect(() => - validateAgentVersion({ - local: { elastic: { agent: { version: '8.8.0' } } }, - userProvided: {}, - }) - ).toThrowError(/Agent version is not compatible with kibana version/); + expect(() => validateAgentVersion('8.8.0', '8.0.0')).toThrowError('not compatible'); }); it('should work with agent < kibana version', () => { - appContextService.start(({ - kibanaVersion: '8.0.0', - } as unknown) as IngestManagerAppContext); - validateAgentVersion({ local: { elastic: { agent: { version: '7.8.0' } } }, userProvided: {} }); + validateAgentVersion('7.8.0', '8.0.0'); }); it('should work with agent = kibana version', () => { - appContextService.start(({ - kibanaVersion: '8.0.0', - } as unknown) as IngestManagerAppContext); - validateAgentVersion({ local: { elastic: { agent: { version: '8.0.0' } } }, userProvided: {} }); + validateAgentVersion('8.0.0', '8.0.0'); }); it('should work with SNAPSHOT version', () => { - appContextService.start(({ - kibanaVersion: '8.0.0-SNAPSHOT', - } as unknown) as IngestManagerAppContext); - validateAgentVersion({ - local: { elastic: { agent: { version: '8.0.0-SNAPSHOT' } } }, - userProvided: {}, - }); + validateAgentVersion('8.0.0-SNAPSHOT', '8.0.0-SNAPSHOT'); }); it('should work with a agent using SNAPSHOT version', () => { - appContextService.start(({ - kibanaVersion: '7.8.0', - } as unknown) as IngestManagerAppContext); - validateAgentVersion({ - local: { elastic: { agent: { version: '7.8.0-SNAPSHOT' } } }, - userProvided: {}, - }); + validateAgentVersion('7.8.0-SNAPSHOT', '7.8.0'); }); it('should work with a kibana using SNAPSHOT version', () => { - appContextService.start(({ - kibanaVersion: '7.8.0-SNAPSHOT', - } as unknown) as IngestManagerAppContext); - validateAgentVersion({ - local: { elastic: { agent: { version: '7.8.0' } } }, - userProvided: {}, - }); + validateAgentVersion('7.8.0', '7.8.0-SNAPSHOT'); + }); + + it('very close versions, e.g. patch/prerelease - all combos should work', () => { + validateAgentVersion('7.9.1', '7.9.2'); + validateAgentVersion('7.8.1', '7.8.2'); + validateAgentVersion('7.6.99', '7.6.2'); + validateAgentVersion('7.6.2', '7.6.99'); + validateAgentVersion('5.94.3', '5.94.1234-SNAPSHOT'); + validateAgentVersion('5.94.3-SNAPSHOT', '5.94.1'); + }); + + it('somewhat close versions, minor release is 1 or 2 versions back and is older than the stack', () => { + validateAgentVersion('7.9.1', '7.10.2'); + validateAgentVersion('7.9.9', '7.11.1'); + validateAgentVersion('7.6.99', '7.6.2'); + validateAgentVersion('7.6.2', '7.6.99'); + expect(() => validateAgentVersion('5.94.3-SNAPSHOT', '5.93.1')).toThrowError('not compatible'); + expect(() => validateAgentVersion('5.94.3', '5.92.99-SNAPSHOT')).toThrowError('not compatible'); + }); + + it('versions where Agent is a minor version or major version greater (newer) than the stack should not work', () => { + expect(() => validateAgentVersion('7.10.1', '7.9.99')).toThrowError('not compatible'); + expect(() => validateAgentVersion('7.9.9', '6.11.1')).toThrowError('not compatible'); + expect(() => validateAgentVersion('5.94.3', '5.92.99-SNAPSHOT')).toThrowError('not compatible'); }); }); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts b/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts index 606d5c4dcbb90a..3c5850c722e971 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts @@ -20,7 +20,8 @@ export async function enroll( metadata?: { local: any; userProvided: any }, sharedId?: string ): Promise { - validateAgentVersion(metadata); + const agentVersion = metadata?.local?.elastic?.agent?.version; + validateAgentVersion(agentVersion); const existingAgent = sharedId ? await getAgentBySharedId(soClient, sharedId) : null; @@ -89,24 +90,50 @@ async function getAgentBySharedId(soClient: SavedObjectsClientContract, sharedId return null; } -export function validateAgentVersion(metadata?: { local: any; userProvided: any }) { - const kibanaVersion = semver.parse(appContextService.getKibanaVersion()); - if (!kibanaVersion) { - throw Boom.badRequest('Kibana version is not set'); - } - const version = semver.parse(metadata?.local?.elastic?.agent?.version); - if (!version) { - throw Boom.badRequest('Agent version not provided in metadata.'); +export function validateAgentVersion( + agentVersion: string, + kibanaVersion = appContextService.getKibanaVersion() +) { + const agentVersionParsed = semver.parse(agentVersion); + if (!agentVersionParsed) { + throw Boom.badRequest('Agent version not provided'); } - if (!version || !semver.lte(formatVersion(version), formatVersion(kibanaVersion))) { - throw Boom.badRequest('Agent version is not compatible with kibana version'); + const kibanaVersionParsed = semver.parse(kibanaVersion); + if (!kibanaVersionParsed) { + throw Boom.badRequest('Kibana version is not set or provided'); } -} -/** - * used to remove prelease from version as includePrerelease in not working as expected - */ -function formatVersion(version: semver.SemVer) { - return `${version.major}.${version.minor}.${version.patch}`; + const diff = semver.diff(agentVersion, kibanaVersion); + switch (diff) { + // section 1) very close versions, only patch release differences - all combos should work + // Agent a.b.1 < Kibana a.b.2 + // Agent a.b.2 > Kibana a.b.1 + case null: + case 'prerelease': + case 'prepatch': + case 'patch': + return; // OK + + // section 2) somewhat close versions, Agent minor release is 1 or 2 versions back and is older than the stack: + // Agent a.9.x < Kibana a.10.x + // Agent a.9.x < Kibana a.11.x + case 'preminor': + case 'minor': + if ( + agentVersionParsed.minor < kibanaVersionParsed.minor && + kibanaVersionParsed.minor - agentVersionParsed.minor <= 2 + ) + return; + + // section 3) versions where Agent is a minor version or major version greater (newer) than the stack should not work: + // Agent 7.10.x > Kibana 7.9.x + // Agent 8.0.x > Kibana 7.9.x + default: + if (semver.lte(agentVersionParsed, kibanaVersionParsed)) return; + else + throw Boom.badRequest( + `Agent version ${agentVersion} is not compatible with Kibana version ${kibanaVersion}` + ); + } } diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/enroll.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/enroll.ts index ce356dbd081c88..93f656ea952d22 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/enroll.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/enroll.ts @@ -115,7 +115,7 @@ export default function (providerContext: FtrProviderContext) { }, }) .expect(400); - expect(apiResponse.message).to.match(/Agent version is not compatible with kibana/); + expect(apiResponse.message).to.match(/is not compatible/); }); it('should allow to enroll an agent with a valid enrollment token', async () => { From 9a7c418327f32f69b9dc708d0ae48861a2fb4d64 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Tue, 1 Sep 2020 17:29:19 -0400 Subject: [PATCH 162/216] [Ingest Manager] Support both zip & tar archives from Registry (#76197) * Quick pass at restoring support for both zip & tar Restored unzip functions from https://github.com/elastic/kibana/pull/43764 Persist the `download` value returned by EPR (e.g. `/epr/system/system-0.5.3.zip` or `/epr/system/system-0.5.3.tar.gz`) as "archive key" for a package name/version combo. The same name&version should return the same archive. The value initially given by the registry. Based on that value, we decide which decompression to use. * Use template literal vs JSON.stringify for keygen * Factor unzip/untar logic out to getBufferExtractor * Add tests for getBufferExtractor * Replace `[aA]rchiveKey*` with `[aA]rchiveLocation*` * Include given name & version in error message Co-authored-by: Elastic Machine --- package.json | 1 + .../server/services/epm/registry/cache.ts | 9 +++- .../server/services/epm/registry/extract.ts | 38 ++++++++++++++++ .../services/epm/registry/index.test.ts | 30 ++++++++++++- .../server/services/epm/registry/index.ts | 45 ++++++++++--------- 5 files changed, 99 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 3485ce5d7a7fc2..28f2025300f395 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,7 @@ "@kbn/test-subj-selector": "0.2.1", "@kbn/ui-framework": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", + "@types/yauzl": "^2.9.1", "JSONStream": "1.3.5", "abortcontroller-polyfill": "^1.4.0", "accept": "3.0.2", diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts index af11bc7f6c8311..e9c8317a6251d8 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { pkgToPkgKey } from './index'; const cache: Map = new Map(); export const cacheGet = (key: string) => cache.get(key); @@ -10,4 +11,10 @@ export const cacheSet = (key: string, value: Buffer) => cache.set(key, value); export const cacheHas = (key: string) => cache.has(key); export const cacheClear = () => cache.clear(); export const cacheDelete = (key: string) => cache.delete(key); -export const getCacheKey = (key: string) => key + '.tar.gz'; + +const archiveLocationCache: Map = new Map(); +export const getArchiveLocation = (name: string, version: string) => + archiveLocationCache.get(pkgToPkgKey({ name, version })); + +export const setArchiveLocation = (name: string, version: string, location: string) => + archiveLocationCache.set(pkgToPkgKey({ name, version }), location); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/extract.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/extract.ts index 1f708c5edbcc7f..6d029b54a63171 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/extract.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/extract.ts @@ -5,6 +5,7 @@ */ import tar from 'tar'; +import yauzl from 'yauzl'; import { bufferToStream, streamToBuffer } from './streams'; export interface ArchiveEntry { @@ -30,3 +31,40 @@ export async function untarBuffer( deflatedStream.pipe(inflateStream); }); } + +export async function unzipBuffer( + buffer: Buffer, + filter = (entry: ArchiveEntry): boolean => true, + onEntry = (entry: ArchiveEntry): void => {} +): Promise { + const zipfile = await yauzlFromBuffer(buffer, { lazyEntries: true }); + zipfile.readEntry(); + zipfile.on('entry', async (entry: yauzl.Entry) => { + const path = entry.fileName; + if (!filter({ path })) return zipfile.readEntry(); + + const entryBuffer = await getZipReadStream(zipfile, entry).then(streamToBuffer); + onEntry({ buffer: entryBuffer, path }); + zipfile.readEntry(); + }); + return new Promise((resolve, reject) => zipfile.on('end', resolve).on('error', reject)); +} + +function yauzlFromBuffer(buffer: Buffer, opts: yauzl.Options): Promise { + return new Promise((resolve, reject) => + yauzl.fromBuffer(buffer, opts, (err?: Error, handle?: yauzl.ZipFile) => + err ? reject(err) : resolve(handle) + ) + ); +} + +function getZipReadStream( + zipfile: yauzl.ZipFile, + entry: yauzl.Entry +): Promise { + return new Promise((resolve, reject) => + zipfile.openReadStream(entry, (err?: Error, readStream?: NodeJS.ReadableStream) => + err ? reject(err) : resolve(readStream) + ) + ); +} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.test.ts index 085dc990fa376e..b40638eefbae2f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.test.ts @@ -5,7 +5,17 @@ */ import { AssetParts } from '../../../types'; -import { pathParts, splitPkgKey } from './index'; +import { getBufferExtractor, pathParts, splitPkgKey } from './index'; +import { getArchiveLocation } from './cache'; +import { untarBuffer, unzipBuffer } from './extract'; + +jest.mock('./cache', () => { + return { + getArchiveLocation: jest.fn(), + }; +}); + +const mockedGetArchiveLocation = getArchiveLocation as jest.Mock; const testPaths = [ { @@ -80,3 +90,21 @@ describe('splitPkgKey tests', () => { expect(pkgVersion).toBe('0.13.0-alpha.1+abcd'); }); }); + +describe('getBufferExtractor', () => { + it('throws if the archive has not been downloaded/cached yet', () => { + expect(() => getBufferExtractor('missing', '1.2.3')).toThrow('no archive location'); + }); + + it('returns unzipBuffer if the archive key ends in .zip', () => { + mockedGetArchiveLocation.mockImplementation(() => '.zip'); + const extractor = getBufferExtractor('will-use-mocked-key', 'a.b.c'); + expect(extractor).toBe(unzipBuffer); + }); + + it('returns untarBuffer if the key ends in anything else', () => { + mockedGetArchiveLocation.mockImplementation(() => 'xyz'); + const extractor = getBufferExtractor('will-use-mocked-key', 'a.b.c'); + expect(extractor).toBe(untarBuffer); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index b6353789604689..61c8cd4aabb7b4 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -17,8 +17,8 @@ import { RegistrySearchResults, RegistrySearchResult, } from '../../../types'; -import { cacheGet, cacheSet, getCacheKey, cacheHas } from './cache'; -import { ArchiveEntry, untarBuffer } from './extract'; +import { cacheGet, cacheSet, cacheHas, getArchiveLocation, setArchiveLocation } from './cache'; +import { ArchiveEntry, untarBuffer, unzipBuffer } from './extract'; import { fetchUrl, getResponse, getResponseStream } from './requests'; import { streamToBuffer } from './streams'; import { getRegistryUrl } from './registry_url'; @@ -130,7 +130,9 @@ export async function getArchiveInfo( filter = (entry: ArchiveEntry): boolean => true ): Promise { const paths: string[] = []; - const onEntry = (entry: ArchiveEntry) => { + const archiveBuffer = await getOrFetchArchiveBuffer(pkgName, pkgVersion); + const bufferExtractor = getBufferExtractor(pkgName, pkgVersion); + await bufferExtractor(archiveBuffer, filter, (entry: ArchiveEntry) => { const { path, buffer } = entry; const { file } = pathParts(path); if (!file) return; @@ -138,9 +140,7 @@ export async function getArchiveInfo( cacheSet(path, buffer); paths.push(path); } - }; - - await extract(pkgName, pkgVersion, filter, onEntry); + }); return paths; } @@ -175,24 +175,20 @@ export function pathParts(path: string): AssetParts { } as AssetParts; } -async function extract( - pkgName: string, - pkgVersion: string, - filter = (entry: ArchiveEntry): boolean => true, - onEntry: (entry: ArchiveEntry) => void -) { - const archiveBuffer = await getOrFetchArchiveBuffer(pkgName, pkgVersion); +export function getBufferExtractor(pkgName: string, pkgVersion: string) { + const archiveLocation = getArchiveLocation(pkgName, pkgVersion); + if (!archiveLocation) throw new Error(`no archive location for ${pkgName} ${pkgVersion}`); + const isZip = archiveLocation.endsWith('.zip'); + const bufferExtractor = isZip ? unzipBuffer : untarBuffer; - return untarBuffer(archiveBuffer, filter, onEntry); + return bufferExtractor; } async function getOrFetchArchiveBuffer(pkgName: string, pkgVersion: string): Promise { - // assume .tar.gz for now. add support for .zip if/when we need it - const key = getCacheKey(`${pkgName}-${pkgVersion}`); - let buffer = cacheGet(key); + const key = getArchiveLocation(pkgName, pkgVersion); + let buffer = key && cacheGet(key); if (!buffer) { buffer = await fetchArchiveBuffer(pkgName, pkgVersion); - cacheSet(key, buffer); } if (buffer) { @@ -203,16 +199,21 @@ async function getOrFetchArchiveBuffer(pkgName: string, pkgVersion: string): Pro } export async function ensureCachedArchiveInfo(name: string, version: string) { - const pkgkey = getCacheKey(`${name}-${version}`); - if (!cacheHas(pkgkey)) { + const pkgkey = getArchiveLocation(name, version); + if (!pkgkey || !cacheHas(pkgkey)) { await getArchiveInfo(name, version); } } async function fetchArchiveBuffer(pkgName: string, pkgVersion: string): Promise { const { download: archivePath } = await fetchInfo(pkgName, pkgVersion); - const registryUrl = getRegistryUrl(); - return getResponseStream(`${registryUrl}${archivePath}`).then(streamToBuffer); + const archiveUrl = `${getRegistryUrl()}${archivePath}`; + const buffer = await getResponseStream(archiveUrl).then(streamToBuffer); + + setArchiveLocation(pkgName, pkgVersion, archivePath); + cacheSet(archivePath, buffer); + + return buffer; } export function getAsset(key: string) { From ac8e9367f7e354dcce1ad7e72d7877e30c01bf85 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Tue, 1 Sep 2020 17:39:04 -0400 Subject: [PATCH 163/216] [Resolver] generator uses setup_node_env (#76422) --- x-pack/plugins/security_solution/package.json | 2 +- .../plugins/security_solution/scripts/endpoint/README.md | 4 ---- .../security_solution/scripts/endpoint/cli_tsconfig.json | 7 ------- .../scripts/endpoint/resolver_generator.js | 8 ++++++++ ...resolver_generator.ts => resolver_generator_script.ts} | 0 5 files changed, 9 insertions(+), 12 deletions(-) delete mode 100644 x-pack/plugins/security_solution/scripts/endpoint/cli_tsconfig.json create mode 100644 x-pack/plugins/security_solution/scripts/endpoint/resolver_generator.js rename x-pack/plugins/security_solution/scripts/endpoint/{resolver_generator.ts => resolver_generator_script.ts} (100%) diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 687099541b3d28..4d2602d1498eef 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -10,7 +10,7 @@ "cypress:open": "cypress open --config-file ./cypress/cypress.json", "cypress:run": "cypress run --browser chrome --headless --spec ./cypress/integration/**/*.spec.ts --config-file ./cypress/cypress.json --reporter ../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; ../../node_modules/.bin/mochawesome-merge --reportDir ../../../target/kibana-security-solution/cypress/results > ../../../target/kibana-security-solution/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../target/kibana-security-solution/cypress/results/output.json --reportDir ../../../target/kibana-security-solution/cypress/results; mkdir -p ../../../target/junit && cp ../../../target/kibana-security-solution/cypress/results/*.xml ../../../target/junit/ && exit $status;", "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/config.ts", - "test:generate": "ts-node --project scripts/endpoint/cli_tsconfig.json scripts/endpoint/resolver_generator.ts" + "test:generate": "node scripts/endpoint/resolver_generator" }, "devDependencies": { "@types/md5": "^2.2.0", diff --git a/x-pack/plugins/security_solution/scripts/endpoint/README.md b/x-pack/plugins/security_solution/scripts/endpoint/README.md index bd9502f2f59e03..2827ab065504b5 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/README.md +++ b/x-pack/plugins/security_solution/scripts/endpoint/README.md @@ -5,10 +5,6 @@ The default behavior is to create 1 endpoint with 1 alert and a moderate number A seed value can be provided as a string for the random number generator for repeatable behavior, useful for demos etc. Use the `-d` option if you want to delete and remake the indices, otherwise it will add documents to existing indices. -The sample data generator script depends on ts-node, install with npm: - -`npm install -g ts-node` - Example command sequence to get ES and kibana running with sample data after installing ts-node: `yarn es snapshot` -> starts ES diff --git a/x-pack/plugins/security_solution/scripts/endpoint/cli_tsconfig.json b/x-pack/plugins/security_solution/scripts/endpoint/cli_tsconfig.json deleted file mode 100644 index 5c68f8ee0abf26..00000000000000 --- a/x-pack/plugins/security_solution/scripts/endpoint/cli_tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../../../tsconfig.json", - "compilerOptions": { - "target": "es2019", - "resolveJsonModule": true - } - } diff --git a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator.js b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator.js new file mode 100644 index 00000000000000..fb5a865439bc2d --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator.js @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +require('../../../../../src/setup_node_env'); +require('./resolver_generator_script'); diff --git a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator.ts b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts similarity index 100% rename from x-pack/plugins/security_solution/scripts/endpoint/resolver_generator.ts rename to x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts From 7d82e273a681c1ff685bea913dbcb96950a00aa0 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 2 Sep 2020 01:12:17 +0300 Subject: [PATCH 164/216] Add `auto` interval to histogram AggConfig (#76001) * Add `auto` interval to histogram AggConfig Closes: #75438 * fix JEST * update UI * add tests * some changes * small changes * cleanup code * fix PR comment * fix PR comments * fix PR comment * Update src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts Co-authored-by: Wylie Conlon * Change algorithm for auto interval * Update src/plugins/data/common/search/aggs/buckets/histogram.ts Co-authored-by: Luke Elmers * added some comments * Update src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts Co-authored-by: Wylie Conlon Co-authored-by: Elastic Machine Co-authored-by: Wylie Conlon Co-authored-by: Luke Elmers --- .../search/aggs/buckets/_interval_options.ts | 5 +- .../create_filter/date_histogram.test.ts | 7 +- .../search/aggs/buckets/date_histogram.ts | 8 +- .../search/aggs/buckets/histogram.test.ts | 21 +++ .../common/search/aggs/buckets/histogram.ts | 65 +++----- .../search/aggs/buckets/histogram_fn.test.ts | 7 +- .../search/aggs/buckets/histogram_fn.ts | 6 + .../lib/histogram_calculate_interval.test.ts | 144 ++++++++++++++++++ .../lib/histogram_calculate_interval.ts | 143 +++++++++++++++++ .../lib/time_buckets/time_buckets.test.ts | 3 +- .../buckets/lib/time_buckets/time_buckets.ts | 5 +- .../utils/calculate_auto_time_expression.ts | 3 +- .../public/components/agg_params_map.ts | 1 + .../public/components/controls/index.ts | 1 + .../public/components/controls/max_bars.tsx | 108 +++++++++++++ .../components/controls/number_interval.tsx | 93 ++++++++--- .../page_objects/visualize_editor_page.ts | 9 ++ 17 files changed, 553 insertions(+), 76 deletions(-) create mode 100644 src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts create mode 100644 src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts create mode 100644 src/plugins/vis_default_editor/public/components/controls/max_bars.tsx diff --git a/src/plugins/data/common/search/aggs/buckets/_interval_options.ts b/src/plugins/data/common/search/aggs/buckets/_interval_options.ts index 00cf50c272fa04..f94484a6edc2e4 100644 --- a/src/plugins/data/common/search/aggs/buckets/_interval_options.ts +++ b/src/plugins/data/common/search/aggs/buckets/_interval_options.ts @@ -20,12 +20,15 @@ import { i18n } from '@kbn/i18n'; import { IBucketAggConfig } from './bucket_agg_type'; +export const autoInterval = 'auto'; +export const isAutoInterval = (value: unknown) => value === autoInterval; + export const intervalOptions = [ { display: i18n.translate('data.search.aggs.buckets.intervalOptions.autoDisplayName', { defaultMessage: 'Auto', }), - val: 'auto', + val: autoInterval, enabled(agg: IBucketAggConfig) { // not only do we need a time field, but the selected field needs // to be the time field. (see #3028) diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.test.ts index 143d5498369003..3d0224b213e8d8 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.test.ts @@ -19,7 +19,7 @@ import moment from 'moment'; import { createFilterDateHistogram } from './date_histogram'; -import { intervalOptions } from '../_interval_options'; +import { intervalOptions, autoInterval } from '../_interval_options'; import { AggConfigs } from '../../agg_configs'; import { mockAggTypesRegistry } from '../../test_helpers'; import { IBucketDateHistogramAggConfig } from '../date_histogram'; @@ -33,7 +33,10 @@ describe('AggConfig Filters', () => { let bucketStart: any; let field: any; - const init = (interval: string = 'auto', duration: any = moment.duration(15, 'minutes')) => { + const init = ( + interval: string = autoInterval, + duration: any = moment.duration(15, 'minutes') + ) => { field = { name: 'date', }; diff --git a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts index fdf9c456b38760..c273ca53a5fed4 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { KBN_FIELD_TYPES, TimeRange, TimeRangeBounds, UI_SETTINGS } from '../../../../common'; -import { intervalOptions } from './_interval_options'; +import { intervalOptions, autoInterval, isAutoInterval } from './_interval_options'; import { createFilterDateHistogram } from './create_filter/date_histogram'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; @@ -44,7 +44,7 @@ const updateTimeBuckets = ( customBuckets?: IBucketDateHistogramAggConfig['buckets'] ) => { const bounds = - agg.params.timeRange && (agg.fieldIsTimeField() || agg.params.interval === 'auto') + agg.params.timeRange && (agg.fieldIsTimeField() || isAutoInterval(agg.params.interval)) ? calculateBounds(agg.params.timeRange) : undefined; const buckets = customBuckets || agg.buckets; @@ -149,7 +149,7 @@ export const getDateHistogramBucketAgg = ({ return agg.getIndexPattern().timeFieldName; }, onChange(agg: IBucketDateHistogramAggConfig) { - if (get(agg, 'params.interval') === 'auto' && !agg.fieldIsTimeField()) { + if (isAutoInterval(get(agg, 'params.interval')) && !agg.fieldIsTimeField()) { delete agg.params.interval; } }, @@ -187,7 +187,7 @@ export const getDateHistogramBucketAgg = ({ } return state; }, - default: 'auto', + default: autoInterval, options: intervalOptions, write(agg, output, aggs) { updateTimeBuckets(agg, calculateBounds); diff --git a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts index 3727747984d3e1..a8ac72c174c726 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts @@ -103,7 +103,28 @@ describe('Histogram Agg', () => { }); }); + describe('maxBars', () => { + test('should not be written to the DSL', () => { + const aggConfigs = getAggConfigs({ + maxBars: 50, + field: { + name: 'field', + }, + }); + const { [BUCKET_TYPES.HISTOGRAM]: params } = aggConfigs.aggs[0].toDsl(); + + expect(params).not.toHaveProperty('maxBars'); + }); + }); + describe('interval', () => { + test('accepts "auto" value', () => { + const params = getParams({ + interval: 'auto', + }); + + expect(params).toHaveProperty('interval', 1); + }); test('accepts a whole number', () => { const params = getParams({ interval: 100, diff --git a/src/plugins/data/common/search/aggs/buckets/histogram.ts b/src/plugins/data/common/search/aggs/buckets/histogram.ts index 2b263013e55a2a..4b631e1fd7cd7e 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.ts @@ -28,6 +28,8 @@ import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { createFilterHistogram } from './create_filter/histogram'; import { BUCKET_TYPES } from './bucket_agg_types'; import { ExtendedBounds } from './lib/extended_bounds'; +import { isAutoInterval, autoInterval } from './_interval_options'; +import { calculateHistogramInterval } from './lib/histogram_calculate_interval'; export interface AutoBounds { min: number; @@ -47,6 +49,7 @@ export interface IBucketHistogramAggConfig extends IBucketAggConfig { export interface AggParamsHistogram extends BaseAggParams { field: string; interval: string; + maxBars?: number; intervalBase?: number; min_doc_count?: boolean; has_extended_bounds?: boolean; @@ -102,6 +105,7 @@ export const getHistogramBucketAgg = ({ }, { name: 'interval', + default: autoInterval, modifyAggConfigOnSearchRequestStart( aggConfig: IBucketHistogramAggConfig, searchSource: any, @@ -127,9 +131,12 @@ export const getHistogramBucketAgg = ({ return childSearchSource .fetch(options) .then((resp: any) => { + const min = resp.aggregations?.minAgg?.value ?? 0; + const max = resp.aggregations?.maxAgg?.value ?? 0; + aggConfig.setAutoBounds({ - min: get(resp, 'aggregations.minAgg.value'), - max: get(resp, 'aggregations.maxAgg.value'), + min, + max, }); }) .catch((e: Error) => { @@ -143,46 +150,24 @@ export const getHistogramBucketAgg = ({ }); }, write(aggConfig, output) { - let interval = parseFloat(aggConfig.params.interval); - if (interval <= 0) { - interval = 1; - } - const autoBounds = aggConfig.getAutoBounds(); - - // ensure interval does not create too many buckets and crash browser - if (autoBounds) { - const range = autoBounds.max - autoBounds.min; - const bars = range / interval; - - if (bars > getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS)) { - const minInterval = range / getConfig(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 - const orderOfMagnitude = Math.pow(10, Math.floor(Math.log10(minInterval))); - let roundInterval = orderOfMagnitude; - - while (roundInterval < minInterval) { - roundInterval += orderOfMagnitude; - } - interval = roundInterval; - } - } - const base = aggConfig.params.intervalBase; - - if (base) { - if (interval < base) { - // In case the specified interval is below the base, just increase it to it's base - interval = base; - } else if (interval % base !== 0) { - // In case the interval is not a multiple of the base round it to the next base - interval = Math.round(interval / base) * base; - } - } - - output.params.interval = interval; + const values = aggConfig.getAutoBounds(); + + output.params.interval = calculateHistogramInterval({ + values, + interval: aggConfig.params.interval, + maxBucketsUiSettings: getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS), + maxBucketsUserInput: aggConfig.params.maxBars, + intervalBase: aggConfig.params.intervalBase, + }); }, }, + { + name: 'maxBars', + shouldShow(agg) { + return isAutoInterval(get(agg, 'params.interval')); + }, + write: () => {}, + }, { name: 'min_doc_count', default: false, diff --git a/src/plugins/data/common/search/aggs/buckets/histogram_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/histogram_fn.test.ts index 34b6fa1a6dcd6d..354946f99a2f58 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram_fn.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram_fn.test.ts @@ -43,6 +43,7 @@ describe('agg_expression_functions', () => { "interval": "10", "intervalBase": undefined, "json": undefined, + "maxBars": undefined, "min_doc_count": undefined, }, "schema": undefined, @@ -55,8 +56,9 @@ describe('agg_expression_functions', () => { test('includes optional params when they are provided', () => { const actual = fn({ field: 'field', - interval: '10', + interval: 'auto', intervalBase: 1, + maxBars: 25, min_doc_count: false, has_extended_bounds: false, extended_bounds: JSON.stringify({ @@ -77,9 +79,10 @@ describe('agg_expression_functions', () => { }, "field": "field", "has_extended_bounds": false, - "interval": "10", + "interval": "auto", "intervalBase": 1, "json": undefined, + "maxBars": 25, "min_doc_count": false, }, "schema": undefined, diff --git a/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts b/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts index 877fd13e59f879..2e833bbe0a3ebb 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts @@ -85,6 +85,12 @@ export const aggHistogram = (): FunctionDefinition => ({ defaultMessage: 'Specifies whether to use min_doc_count for this aggregation', }), }, + maxBars: { + types: ['number'], + help: i18n.translate('data.search.aggs.buckets.histogram.maxBars.help', { + defaultMessage: 'Calculate interval to get approximately this many bars', + }), + }, has_extended_bounds: { types: ['boolean'], help: i18n.translate('data.search.aggs.buckets.histogram.hasExtendedBounds.help', { diff --git a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts new file mode 100644 index 00000000000000..fd788d33392951 --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts @@ -0,0 +1,144 @@ +/* + * 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 { + calculateHistogramInterval, + CalculateHistogramIntervalParams, +} from './histogram_calculate_interval'; + +describe('calculateHistogramInterval', () => { + describe('auto calculating mode', () => { + let params: CalculateHistogramIntervalParams; + + beforeEach(() => { + params = { + interval: 'auto', + intervalBase: undefined, + maxBucketsUiSettings: 100, + maxBucketsUserInput: undefined, + values: { + min: 0, + max: 1, + }, + }; + }); + + describe('maxBucketsUserInput is defined', () => { + test('should not set interval which more than largest possible', () => { + const p = { + ...params, + maxBucketsUserInput: 200, + values: { + min: 150, + max: 250, + }, + }; + expect(calculateHistogramInterval(p)).toEqual(1); + }); + + test('should correctly work for float numbers (small numbers)', () => { + expect( + calculateHistogramInterval({ + ...params, + maxBucketsUserInput: 50, + values: { + min: 0.1, + max: 0.9, + }, + }) + ).toBe(0.02); + }); + + test('should correctly work for float numbers (big numbers)', () => { + expect( + calculateHistogramInterval({ + ...params, + maxBucketsUserInput: 10, + values: { + min: 10.45, + max: 1000.05, + }, + }) + ).toBe(100); + }); + }); + + describe('maxBucketsUserInput is not defined', () => { + test('should not set interval which more than largest possible', () => { + expect( + calculateHistogramInterval({ + ...params, + values: { + min: 0, + max: 100, + }, + }) + ).toEqual(1); + }); + + test('should set intervals for integer numbers (diff less than maxBucketsUiSettings)', () => { + expect( + calculateHistogramInterval({ + ...params, + values: { + min: 1, + max: 10, + }, + }) + ).toEqual(0.1); + }); + + test('should set intervals for integer numbers (diff more than maxBucketsUiSettings)', () => { + // diff === 44445; interval === 500; buckets === 89 + expect( + calculateHistogramInterval({ + ...params, + values: { + min: 45678, + max: 90123, + }, + }) + ).toEqual(500); + }); + + test('should set intervals the same for the same interval', () => { + // both diffs are the same + // diff === 1.655; interval === 0.02; buckets === 82 + expect( + calculateHistogramInterval({ + ...params, + values: { + min: 1.245, + max: 2.9, + }, + }) + ).toEqual(0.02); + expect( + calculateHistogramInterval({ + ...params, + values: { + min: 0.5, + max: 2.3, + }, + }) + ).toEqual(0.02); + }); + }); + }); +}); diff --git a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts new file mode 100644 index 00000000000000..f4e42fa8881ef5 --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts @@ -0,0 +1,143 @@ +/* + * 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 { isAutoInterval } from '../_interval_options'; + +interface IntervalValuesRange { + min: number; + max: number; +} + +export interface CalculateHistogramIntervalParams { + interval: string; + maxBucketsUiSettings: number; + maxBucketsUserInput?: number; + intervalBase?: number; + values?: IntervalValuesRange; +} + +/** + * Round interval by order of magnitude to provide clean intervals + */ +const roundInterval = (minInterval: number) => { + const orderOfMagnitude = Math.pow(10, Math.floor(Math.log10(minInterval))); + let interval = orderOfMagnitude; + + while (interval < minInterval) { + interval += orderOfMagnitude; + } + + return interval; +}; + +const calculateForGivenInterval = ( + diff: number, + interval: number, + maxBucketsUiSettings: CalculateHistogramIntervalParams['maxBucketsUiSettings'] +) => { + const bars = diff / interval; + + if (bars > maxBucketsUiSettings) { + const minInterval = diff / maxBucketsUiSettings; + + return roundInterval(minInterval); + } + + return interval; +}; + +/** + * Algorithm for determining auto-interval + + 1. Define maxBars as Math.min(, ) + 2. Find the min and max values in the data + 3. Subtract the min from max to get diff + 4. Set exactInterval to diff / maxBars + 5. Based on exactInterval, find the power of 10 that's lower and higher + 6. Find the number of expected buckets that lowerPower would create: diff / lowerPower + 7. Find the number of expected buckets that higherPower would create: diff / higherPower + 8. There are three possible final intervals, pick the one that's closest to maxBars: + - The lower power of 10 + - The lower power of 10, times 2 + - The lower power of 10, times 5 + **/ +const calculateAutoInterval = ( + diff: number, + maxBucketsUiSettings: CalculateHistogramIntervalParams['maxBucketsUiSettings'], + maxBucketsUserInput: CalculateHistogramIntervalParams['maxBucketsUserInput'] +) => { + const maxBars = Math.min(maxBucketsUiSettings, maxBucketsUserInput ?? maxBucketsUiSettings); + const exactInterval = diff / maxBars; + + const lowerPower = Math.pow(10, Math.floor(Math.log10(exactInterval))); + + const autoBuckets = diff / lowerPower; + + if (autoBuckets > maxBars) { + if (autoBuckets / 2 <= maxBars) { + return lowerPower * 2; + } else if (autoBuckets / 5 <= maxBars) { + return lowerPower * 5; + } else { + return lowerPower * 10; + } + } + + return lowerPower; +}; + +export const calculateHistogramInterval = ({ + interval, + maxBucketsUiSettings, + maxBucketsUserInput, + intervalBase, + values, +}: CalculateHistogramIntervalParams) => { + const isAuto = isAutoInterval(interval); + let calculatedInterval = isAuto ? 0 : parseFloat(interval); + + // should return NaN on non-numeric or invalid values + if (Number.isNaN(calculatedInterval)) { + return calculatedInterval; + } + + if (values) { + const diff = values.max - values.min; + + if (diff) { + calculatedInterval = isAuto + ? calculateAutoInterval(diff, maxBucketsUiSettings, maxBucketsUserInput) + : calculateForGivenInterval(diff, calculatedInterval, maxBucketsUiSettings); + } + } + + if (intervalBase) { + if (calculatedInterval < intervalBase) { + // In case the specified interval is below the base, just increase it to it's base + calculatedInterval = intervalBase; + } else if (calculatedInterval % intervalBase !== 0) { + // In case the interval is not a multiple of the base round it to the next base + calculatedInterval = Math.round(calculatedInterval / intervalBase) * intervalBase; + } + } + + const defaultValueForUnspecifiedInterval = 1; + + return calculatedInterval || defaultValueForUnspecifiedInterval; +}; diff --git a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts index ae7630ecd3dac0..04e64233ce1965 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts @@ -20,6 +20,7 @@ import moment from 'moment'; import { TimeBuckets, TimeBucketsConfig } from './time_buckets'; +import { autoInterval } from '../../_interval_options'; describe('TimeBuckets', () => { const timeBucketConfig: TimeBucketsConfig = { @@ -103,7 +104,7 @@ describe('TimeBuckets', () => { test('setInterval/getInterval - intreval is a "auto"', () => { const timeBuckets = new TimeBuckets(timeBucketConfig); - timeBuckets.setInterval('auto'); + timeBuckets.setInterval(autoInterval); const interval = timeBuckets.getInterval(); expect(interval.description).toEqual('0 milliseconds'); diff --git a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts index 6402a6e83ead9a..d054df0c9274ed 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts @@ -28,6 +28,7 @@ import { convertIntervalToEsInterval, EsInterval, } from './calc_es_interval'; +import { autoInterval } from '../../_interval_options'; interface TimeBucketsInterval extends moment.Duration { // TODO double-check whether all of these are needed @@ -189,8 +190,8 @@ export class TimeBuckets { interval = input.val; } - if (!interval || interval === 'auto') { - this._i = 'auto'; + if (!interval || interval === autoInterval) { + this._i = autoInterval; return; } diff --git a/src/plugins/data/common/search/aggs/utils/calculate_auto_time_expression.ts b/src/plugins/data/common/search/aggs/utils/calculate_auto_time_expression.ts index 622e8101f34aba..3637ded44c50ab 100644 --- a/src/plugins/data/common/search/aggs/utils/calculate_auto_time_expression.ts +++ b/src/plugins/data/common/search/aggs/utils/calculate_auto_time_expression.ts @@ -21,6 +21,7 @@ import { UI_SETTINGS } from '../../../../common/constants'; import { TimeRange } from '../../../../common/query'; import { TimeBuckets } from '../buckets/lib/time_buckets'; import { toAbsoluteDates } from './date_interval_utils'; +import { autoInterval } from '../buckets/_interval_options'; export function getCalculateAutoTimeExpression(getConfig: (key: string) => any) { return function calculateAutoTimeExpression(range: TimeRange) { @@ -36,7 +37,7 @@ export function getCalculateAutoTimeExpression(getConfig: (key: string) => any) 'dateFormat:scaled': getConfig('dateFormat:scaled'), }); - buckets.setInterval('auto'); + buckets.setInterval(autoInterval); buckets.setBounds({ min: moment(dates.from), max: moment(dates.to), diff --git a/src/plugins/vis_default_editor/public/components/agg_params_map.ts b/src/plugins/vis_default_editor/public/components/agg_params_map.ts index 9bc3146b9903bb..e9019e479f92fe 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_map.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_map.ts @@ -43,6 +43,7 @@ const buckets = { }, [BUCKET_TYPES.HISTOGRAM]: { interval: controls.NumberIntervalParamEditor, + maxBars: controls.MaxBarsParamEditor, min_doc_count: controls.MinDocCountParamEditor, has_extended_bounds: controls.HasExtendedBoundsParamEditor, extended_bounds: controls.ExtendedBoundsParamEditor, diff --git a/src/plugins/vis_default_editor/public/components/controls/index.ts b/src/plugins/vis_default_editor/public/components/controls/index.ts index cfb236e5e22e37..26e6609c7711d2 100644 --- a/src/plugins/vis_default_editor/public/components/controls/index.ts +++ b/src/plugins/vis_default_editor/public/components/controls/index.ts @@ -52,3 +52,4 @@ export { TopSizeParamEditor } from './top_size'; export { TopSortFieldParamEditor } from './top_sort_field'; export { OrderParamEditor } from './order'; export { UseGeocentroidParamEditor } from './use_geocentroid'; +export { MaxBarsParamEditor } from './max_bars'; diff --git a/src/plugins/vis_default_editor/public/components/controls/max_bars.tsx b/src/plugins/vis_default_editor/public/components/controls/max_bars.tsx new file mode 100644 index 00000000000000..b0d517e0928dff --- /dev/null +++ b/src/plugins/vis_default_editor/public/components/controls/max_bars.tsx @@ -0,0 +1,108 @@ +/* + * 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 React, { useCallback, useEffect } from 'react'; +import { EuiFormRow, EuiFieldNumber, EuiFieldNumberProps, EuiIconTip } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../../kibana_react/public'; +import { AggParamEditorProps } from '../agg_param_props'; +import { UI_SETTINGS } from '../../../../data/public'; + +export interface SizeParamEditorProps extends AggParamEditorProps { + iconTip?: React.ReactNode; + disabled?: boolean; +} + +const autoPlaceholder = i18n.translate('visDefaultEditor.controls.maxBars.autoPlaceholder', { + defaultMessage: 'Auto', +}); + +const label = ( + <> + {' '} + + } + type="questionInCircle" + /> + +); + +function MaxBarsParamEditor({ + disabled, + iconTip, + value, + setValue, + showValidation, + setValidity, + setTouched, +}: SizeParamEditorProps) { + const { services } = useKibana(); + const uiSettingMaxBars = services.uiSettings?.get(UI_SETTINGS.HISTOGRAM_MAX_BARS); + const isValid = + disabled || + value === undefined || + value === '' || + Number(value) > 0 || + value < uiSettingMaxBars; + + useEffect(() => { + setValidity(isValid); + }, [isValid, setValidity]); + + const onChange: EuiFieldNumberProps['onChange'] = useCallback( + (ev) => setValue(ev.target.value === '' ? '' : parseFloat(ev.target.value)), + [setValue] + ); + + return ( + + + + ); +} + +export { MaxBarsParamEditor }; diff --git a/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx b/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx index f6354027ab01b8..8cdc92581cefbf 100644 --- a/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx @@ -20,7 +20,16 @@ import { get } from 'lodash'; import React, { useEffect, useCallback } from 'react'; -import { EuiFieldNumber, EuiFormRow, EuiIconTip } from '@elastic/eui'; +import { + EuiFieldNumber, + EuiFormRow, + EuiIconTip, + EuiSwitch, + EuiSwitchProps, + EuiFieldNumberProps, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { UI_SETTINGS } from '../../../../data/public'; @@ -47,6 +56,25 @@ const label = ( ); +const autoInterval = 'auto'; +const isAutoInterval = (value: unknown) => value === autoInterval; + +const selectIntervalPlaceholder = i18n.translate( + 'visDefaultEditor.controls.numberInterval.selectIntervalPlaceholder', + { + defaultMessage: 'Enter an interval', + } +); +const autoIntervalIsUsedPlaceholder = i18n.translate( + 'visDefaultEditor.controls.numberInterval.autoInteralIsUsed', + { + defaultMessage: 'Auto interval is used', + } +); +const useAutoIntervalLabel = i18n.translate('visDefaultEditor.controls.useAutoInterval', { + defaultMessage: 'Use auto interval', +}); + function NumberIntervalParamEditor({ agg, editorConfig, @@ -55,18 +83,28 @@ function NumberIntervalParamEditor({ setTouched, setValidity, setValue, -}: AggParamEditorProps) { +}: AggParamEditorProps) { + const isAutoChecked = isAutoInterval(value); const base: number = get(editorConfig, 'interval.base') as number; const min = base || 0; - const isValid = value !== undefined && value >= min; + const isValid = + value !== '' && value !== undefined && (isAutoInterval(value) || Number(value) >= min); useEffect(() => { setValidity(isValid); }, [isValid, setValidity]); - const onChange = useCallback( - ({ target }: React.ChangeEvent) => - setValue(isNaN(target.valueAsNumber) ? undefined : target.valueAsNumber), + const onChange: EuiFieldNumberProps['onChange'] = useCallback( + ({ target }) => setValue(isNaN(target.valueAsNumber) ? '' : target.valueAsNumber), + [setValue] + ); + + const onAutoSwitchChange: EuiSwitchProps['onChange'] = useCallback( + (e) => { + const isAutoSwitchChecked = e.target.checked; + + setValue(isAutoSwitchChecked ? autoInterval : ''); + }, [setValue] ); @@ -78,23 +116,32 @@ function NumberIntervalParamEditor({ isInvalid={showValidation && !isValid} helpText={get(editorConfig, 'interval.help')} > - + + + + + + + + ); } diff --git a/test/functional/page_objects/visualize_editor_page.ts b/test/functional/page_objects/visualize_editor_page.ts index 9fcb38efce0db9..8cac43d97317b3 100644 --- a/test/functional/page_objects/visualize_editor_page.ts +++ b/test/functional/page_objects/visualize_editor_page.ts @@ -445,6 +445,15 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP } else if (type === 'custom') { await comboBox.setCustom('visEditorInterval', newValue); } else { + if (type === 'numeric') { + const autoMode = await testSubjects.getAttribute( + `visEditorIntervalSwitch${aggNth}`, + 'aria-checked' + ); + if (autoMode === 'true') { + await testSubjects.click(`visEditorIntervalSwitch${aggNth}`); + } + } if (append) { await testSubjects.append(`visEditorInterval${aggNth}`, String(newValue)); } else { From 298a7899bcb2c2bae87be2fcf380e9784348e953 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 1 Sep 2020 15:40:51 -0700 Subject: [PATCH 165/216] skip flaky suite (#76245) --- test/functional/apps/dashboard/embeddable_rendering.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/dashboard/embeddable_rendering.js b/test/functional/apps/dashboard/embeddable_rendering.js index 73c36c7562e8be..b7b795ae11c967 100644 --- a/test/functional/apps/dashboard/embeddable_rendering.js +++ b/test/functional/apps/dashboard/embeddable_rendering.js @@ -99,7 +99,8 @@ export default function ({ getService, getPageObjects }) { await dashboardExpect.vegaTextsDoNotExist(['5,000']); }; - describe('dashboard embeddable rendering', function describeIndexTests() { + // Failing: See https://github.com/elastic/kibana/issues/76245 + describe.skip('dashboard embeddable rendering', function describeIndexTests() { before(async () => { await security.testUser.setRoles(['kibana_admin', 'animals', 'test_logstash_reader']); await esArchiver.load('dashboard/current/kibana'); From d4a9ea2fff07d4b9a2e66880f4c0005d080c6d4b Mon Sep 17 00:00:00 2001 From: Constance Date: Tue, 1 Sep 2020 16:21:49 -0700 Subject: [PATCH 166/216] [Enterprise Search] Fix various icons in dark mode (#76430) * Update Home catalogue icons to use new 28.0 EUI app icons * Fix App Search engine icons in dark mode - Convert to React SVG element in order to be able to inherit correct fill in dark mode - Fix vertical centering (taking advantage of SVG centering for us) - Move icons directly to the engines overview component instead of having to reach upwards to a shared assets folder for them * [Cleanup] Move Setup Guide images - to be nested within their own component views that use them, instead of having to grab them upwards from a shared assets folder --- .../applications/app_search/assets/engine.svg | 3 -- .../applications/app_search/assets/logo.svg | 4 --- .../app_search/assets/meta_engine.svg | 4 --- .../engine_overview/assets/engine_icon.tsx | 24 +++++++++++++++ .../assets/meta_engine_icon.tsx | 29 ++++++++++++++++++ .../engine_overview/engine_overview.scss | 4 ++- .../engine_overview/engine_overview.tsx | 8 ++--- .../setup_guide}/assets/getting_started.png | Bin .../components/setup_guide/setup_guide.tsx | 2 +- .../workplace_search/assets/logo.svg | 5 --- .../setup_guide}/assets/getting_started.png | Bin .../views/setup_guide/setup_guide.tsx | 2 +- .../enterprise_search/public/plugin.ts | 6 ++-- 13 files changed, 64 insertions(+), 27 deletions(-) delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/assets/engine.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/assets/logo.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/assets/meta_engine.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/assets/engine_icon.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/assets/meta_engine_icon.tsx rename x-pack/plugins/enterprise_search/public/applications/app_search/{ => components/setup_guide}/assets/getting_started.png (100%) delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/logo.svg rename x-pack/plugins/enterprise_search/public/applications/workplace_search/{ => views/setup_guide}/assets/getting_started.png (100%) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/engine.svg b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/engine.svg deleted file mode 100644 index ceab918e92e702..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/engine.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/logo.svg b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/logo.svg deleted file mode 100644 index 2284a425b5add2..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/logo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/meta_engine.svg b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/meta_engine.svg deleted file mode 100644 index 4e01e9a0b34fb9..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/meta_engine.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/assets/engine_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/assets/engine_icon.tsx new file mode 100644 index 00000000000000..64494bfaa7949f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/assets/engine_icon.tsx @@ -0,0 +1,24 @@ +/* + * 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'; + +export const EngineIcon: React.FC = () => ( + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/assets/meta_engine_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/assets/meta_engine_icon.tsx new file mode 100644 index 00000000000000..e2939ddcde9d79 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/assets/meta_engine_icon.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +export const MetaEngineIcon: React.FC = () => ( + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss index e39bbbc95564b4..5e8a20ba425add 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss @@ -15,6 +15,8 @@ .engineIcon { display: inline-block; width: $euiSize; - height: $euiSize; + // Use line-height of EuiTitle - SVG will vertically center accordingly + height: map-get(map-get($euiTitles, 's'), 'line-height'); + vertical-align: top; margin-right: $euiSizeXS; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx index c3b47b2b585bd4..9703fde7e140af 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx @@ -20,8 +20,8 @@ import { FlashMessages } from '../../../shared/flash_messages'; import { LicenseContext, ILicenseContext, hasPlatinumLicense } from '../../../shared/licensing'; import { KibanaContext, IKibanaContext } from '../../../index'; -import EnginesIcon from '../../assets/engine.svg'; -import MetaEnginesIcon from '../../assets/meta_engine.svg'; +import { EngineIcon } from './assets/engine_icon'; +import { MetaEngineIcon } from './assets/meta_engine_icon'; import { EngineOverviewHeader, LoadingState, EmptyState } from './components'; import { EngineTable } from './engine_table'; @@ -93,7 +93,7 @@ export const EngineOverview: React.FC = () => {

- + {

- + ( - - - - diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/getting_started.png b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/assets/getting_started.png similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/getting_started.png rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/assets/getting_started.png diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx index f9b00bdf29642d..d632792f2a666f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx @@ -13,7 +13,7 @@ import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; import { SetupGuide as SetupGuideLayout } from '../../../shared/setup_guide'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; -import GettingStarted from '../../assets/getting_started.png'; +import GettingStarted from './assets/getting_started.png'; const GETTING_STARTED_LINK_URL = 'https://www.elastic.co/guide/en/workplace-search/current/workplace-search-getting-started.html'; diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 1ce6bae8ff6031..83598a0dc971dc 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -27,8 +27,6 @@ import { WORKPLACE_SEARCH_PLUGIN, } from '../common/constants'; import { ExternalUrl, IExternalUrl } from './applications/shared/enterprise_search_url'; -import AppSearchLogo from './applications/app_search/assets/logo.svg'; -import WorkplaceSearchLogo from './applications/workplace_search/assets/logo.svg'; export interface ClientConfigType { host?: string; @@ -117,7 +115,7 @@ export class EnterpriseSearchPlugin implements Plugin { plugins.home.featureCatalogue.register({ id: APP_SEARCH_PLUGIN.ID, title: APP_SEARCH_PLUGIN.NAME, - icon: AppSearchLogo, + icon: 'appSearchApp', description: APP_SEARCH_PLUGIN.DESCRIPTION, path: APP_SEARCH_PLUGIN.URL, category: FeatureCatalogueCategory.DATA, @@ -127,7 +125,7 @@ export class EnterpriseSearchPlugin implements Plugin { plugins.home.featureCatalogue.register({ id: WORKPLACE_SEARCH_PLUGIN.ID, title: WORKPLACE_SEARCH_PLUGIN.NAME, - icon: WorkplaceSearchLogo, + icon: 'workplaceSearchApp', description: WORKPLACE_SEARCH_PLUGIN.DESCRIPTION, path: WORKPLACE_SEARCH_PLUGIN.URL, category: FeatureCatalogueCategory.DATA, From a3af33ddaf32cfbfb53f66478bf20649a998a06b Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 1 Sep 2020 16:55:04 -0700 Subject: [PATCH 167/216] [Reporting] Add functional test for Reports in non-default spaces (#76053) * [Reporting] Add functional test for Download CSV in non-default space * skip tests * unskip * rm comment Co-authored-by: Elastic Machine --- .../ecommerce_kibana_spaces/data.json.gz | Bin 0 -> 1752 bytes .../ecommerce_kibana_spaces/mappings.json | 2635 +++++++++++++++++ .../reporting_and_security.config.ts | 1 - .../reporting_and_security/index.ts | 3 +- .../reporting_and_security/network_policy.ts | 2 +- .../reporting_and_security/spaces.ts | 76 + .../reporting_and_security/usage.ts | 2 +- 7 files changed, 2715 insertions(+), 4 deletions(-) create mode 100644 x-pack/test/functional/es_archives/reporting/ecommerce_kibana_spaces/data.json.gz create mode 100644 x-pack/test/functional/es_archives/reporting/ecommerce_kibana_spaces/mappings.json create mode 100644 x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana_spaces/data.json.gz b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana_spaces/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..06e83f8c267d6618669bd8641c79c3dda19c1679 GIT binary patch literal 1752 zcmV;}1}FI+iwFpY>qcJ!17u-zVJ>QOZ*Bn9TU&3VI23;GuZVbDH3$f~z}srNm1d>s ztftv{n6^R=IElA_Gsbk9t@7XZ*jxgHWXQDJ#8Rt}S72+JT*fmDu0Q-*7$7789rioD> zjR!_Q7*4Q*(Res9XL5QmqZlbB)gQ?W#i*bsG-dvF<)UY?Mx_Co<^+d9B4)XJxz~J} z#{6D$jmvv2MiGX`d(9)ky#@rqp(G@nk$53#IGF-90rFkm@d2ob&&Y~n_~y=0m|#J| zi1|x23&j+irGkbQ3x*en*QpGn;F{6siuzxL2$N!#&E8?7z!1pP~;+kLSRFrT+}KdEA9vt(RXr$)sn{e{7oxBYmd&o9~eI> z1=b^bHzYN!%Z@_bOO56MJ3Uhf+GA zg?xUFd)pS6DwMM;MuXJniQXCQ2`#+}R!vK!yN7F777%;QV(85BL5VjYiR|E^DC?(^Q<@} zy9ry`vvRm4-m^|HEqK8Nzo+7aCv(a3&x_qJ(Va3MrpXyyeR#-k*2)XBoJ9CqT$;TB ztm5A6_E5Xtjfb)s`!tHgj{7$)Z`~B#w1_O2{4MA!W(j=MujV#y?P_Cvq#L`l2%*Wl z#H|;MOHjby9E;mhgShBq8J5vECVgu(wJg_mT+7mp ziSaE%x497in~RH=*W8TuX*?$jccL4>_0e~pv(a^l&Qbz;OCz8^eQcVq&~~Qb zeYlTM^H2CIL{t$i)JwwegKLb$Nk(;CWn?bmcIobDqhPur{DV>)x&uoeSk|khm}5I$ zb}hp3Kp>bsVKKc7i&eAFqHAWW+H^^h9{lko?&S!2b@qQn)>rvVw{ItB+Z~wtWHNZu z*c6~-IirXL?!b329*o}jjg4~thV9mMP!;u(a3Nl!Ldq7oMWD{lvwyJ9KVgn)6NLH2+oPE(vn$bFP9)x3dCH=w1SK2YlKSv81T&!~>Dz2Zm+Zwu$;f8=HOGo{jqs0)q;iv4N0dIf3nGJWCuO zvlmJ_4-^#))P+GHT;qcbyaHc2-xoZ;DYtrp-~luBA_Sryc+>JD)R7DSQh zaK*ZfR{hA*1`43eiA<+&jQXZAH3qIZcCCp%F(AP=`hUR*r)+mI**K-!sm6WNo|;f7 z3|(WS4<^HJt>w}mP%ibZ3hVFP(70ER36()lW#%-GOs`e;Cf51LUxzsAqFpc6me}bIdYe#FxD&q_tKj@0WI$WU&n4pZ^|)+myIXZaLvq*;(W{VHwpt ubBwXUgzfn~p^WdX@ahr%HnX^n!MqXay6#DbmTp { + describe('Network Policy', () => { before(async () => { await esArchiver.load(archive); // includes a canvas worksheet with an offending image URL }); diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts b/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts new file mode 100644 index 00000000000000..0145ca2a180927 --- /dev/null +++ b/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import * as Rx from 'rxjs'; +import { filter, first, map, switchMap, tap, timeout } from 'rxjs/operators'; +import { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const reportingAPI = getService('reportingAPI'); + const supertest = getService('supertest'); + const log = getService('log'); + + const getCompleted$ = (downloadPath: string) => { + return Rx.interval(2000).pipe( + tap(() => log.debug(`checking report status at ${downloadPath}...`)), + switchMap(() => supertest.get(downloadPath)), + filter(({ status: statusCode }) => statusCode === 200), + map((response) => response.text), + first(), + timeout(15000) + ); + }; + + describe('Exports from Non-default Space', () => { + before(async () => { + await esArchiver.load('reporting/ecommerce'); + await esArchiver.load('reporting/ecommerce_kibana_spaces'); // dashboard in non default space + }); + + after(async () => { + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana_spaces'); + }); + + afterEach(async () => { + await reportingAPI.deleteAllReports(); + }); + + it('should complete a job of CSV saved search export in non-default space', async () => { + const downloadPath = await reportingAPI.postJob( + `/s/non_default_space/api/reporting/generate/csv?jobParams=%28browserTimezone%3AUTC%2CconflictedTypesFields%3A%21%28%29%2Cfields%3A%21%28order_date%2Ccategory%2Ccustomer_first_name%2Ccustomer_full_name%2Ctotal_quantity%2Ctotal_unique_products%2Ctaxless_total_price%2Ctaxful_total_price%2Ccurrency%29%2CindexPatternId%3A%27067dec90-e7ee-11ea-a730-d58e9ea7581b%27%2CmetaFields%3A%21%28_source%2C_id%2C_type%2C_index%2C_score%29%2CobjectType%3Asearch%2CsearchRequest%3A%28body%3A%28_source%3A%28includes%3A%21%28order_date%2Ccategory%2Ccustomer_first_name%2Ccustomer_full_name%2Ctotal_quantity%2Ctotal_unique_products%2Ctaxless_total_price%2Ctaxful_total_price%2Ccurrency%29%29%2Cdocvalue_fields%3A%21%28%28field%3Aorder_date%2Cformat%3Adate_time%29%29%2Cquery%3A%28bool%3A%28filter%3A%21%28%28match_all%3A%28%29%29%2C%28range%3A%28order_date%3A%28format%3Astrict_date_optional_time%2Cgte%3A%272019-06-11T08%3A24%3A16.425Z%27%2Clte%3A%272019-07-13T09%3A31%3A07.520Z%27%29%29%29%29%2Cmust%3A%21%28%29%2Cmust_not%3A%21%28%29%2Cshould%3A%21%28%29%29%29%2Cscript_fields%3A%28%29%2Csort%3A%21%28%28order_date%3A%28order%3Adesc%2Cunmapped_type%3Aboolean%29%29%29%2Cstored_fields%3A%21%28order_date%2Ccategory%2Ccustomer_first_name%2Ccustomer_full_name%2Ctotal_quantity%2Ctotal_unique_products%2Ctaxless_total_price%2Ctaxful_total_price%2Ccurrency%29%2Cversion%3A%21t%29%2Cindex%3A%27ecommerce%2A%27%29%2Ctitle%3A%27Ecom%20Search%27%29` + ); + + // Retry the download URL until a "completed" response status is returned + const completed$ = getCompleted$(downloadPath); + const reportCompleted = await completed$.toPromise(); + expect(reportCompleted).to.match(/^"order_date",/); + }); + + it('should complete a job of PNG export of a dashboard in non-default space', async () => { + const downloadPath = await reportingAPI.postJob( + `/s/non_default_space/api/reporting/generate/png?jobParams=%28browserTimezone%3AUTC%2Clayout%3A%28dimensions%3A%28height%3A512%2Cwidth%3A2402%29%2Cid%3Apng%29%2CobjectType%3Adashboard%2CrelativeUrl%3A%27%2Fs%2Fnon_default_space%2Fapp%2Fdashboards%23%2Fview%2F3c9ee360-e7ee-11ea-a730-d58e9ea7581b%3F_g%3D%28filters%3A%21%21%28%29%2CrefreshInterval%3A%28pause%3A%21%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3A%21%272019-06-10T03%3A17%3A28.800Z%21%27%2Cto%3A%21%272019-07-14T19%3A25%3A06.385Z%21%27%29%29%26_a%3D%28description%3A%21%27%21%27%2Cfilters%3A%21%21%28%29%2CfullScreenMode%3A%21%21f%2Coptions%3A%28hidePanelTitles%3A%21%21f%2CuseMargins%3A%21%21t%29%2Cquery%3A%28language%3Akuery%2Cquery%3A%21%27%21%27%29%2CtimeRestore%3A%21%21t%2Ctitle%3A%21%27Ecom%2520Dashboard%2520Non%2520Default%2520Space%21%27%2CviewMode%3Aview%29%27%2Ctitle%3A%27Ecom%20Dashboard%20Non%20Default%20Space%27%29` + ); + + const completed$: Rx.Observable = getCompleted$(downloadPath); + const reportCompleted = await completed$.toPromise(); + expect(reportCompleted).to.not.be(null); + }); + + it('should complete a job of PDF export of a dashboard in non-default space', async () => { + const downloadPath = await reportingAPI.postJob( + `/s/non_default_space/api/reporting/generate/printablePdf?jobParams=%28browserTimezone%3AUTC%2Clayout%3A%28dimensions%3A%28height%3A512%2Cwidth%3A2402%29%2Cid%3Apreserve_layout%29%2CobjectType%3Adashboard%2CrelativeUrls%3A%21%28%27%2Fs%2Fnon_default_space%2Fapp%2Fdashboards%23%2Fview%2F3c9ee360-e7ee-11ea-a730-d58e9ea7581b%3F_g%3D%28filters%3A%21%21%28%29%2CrefreshInterval%3A%28pause%3A%21%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3A%21%272019-06-10T03%3A17%3A28.800Z%21%27%2Cto%3A%21%272019-07-14T19%3A25%3A06.385Z%21%27%29%29%26_a%3D%28description%3A%21%27%21%27%2Cfilters%3A%21%21%28%29%2CfullScreenMode%3A%21%21f%2Coptions%3A%28hidePanelTitles%3A%21%21f%2CuseMargins%3A%21%21t%29%2Cquery%3A%28language%3Akuery%2Cquery%3A%21%27%21%27%29%2CtimeRestore%3A%21%21t%2Ctitle%3A%21%27Ecom%2520Dashboard%2520Non%2520Default%2520Space%21%27%2CviewMode%3Aview%29%27%29%2Ctitle%3A%27Ecom%20Dashboard%20Non%20Default%20Space%27%29` + ); + + const completed$ = getCompleted$(downloadPath); + const reportCompleted = await completed$.toPromise(); + expect(reportCompleted).to.not.be(null); + }); + }); +} diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts b/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts index 24e68b3917d6cd..feda5c1386e98d 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts @@ -21,7 +21,7 @@ export default function ({ getService }: FtrProviderContext) { const reportingAPI = getService('reportingAPI'); const usageAPI = getService('usageAPI'); - describe('reporting usage', () => { + describe('Usage', () => { before(async () => { await esArchiver.load(OSS_KIBANA_ARCHIVE_PATH); await esArchiver.load(OSS_DATA_ARCHIVE_PATH); From 03002453d3d90831670c5215a613a52bd0306f00 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 1 Sep 2020 21:03:43 -0700 Subject: [PATCH 168/216] skip flaky suite (#75724) --- x-pack/test/functional/apps/infra/home_page.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts index 04f289b69bb71d..82aba0503fb9d0 100644 --- a/x-pack/test/functional/apps/infra/home_page.ts +++ b/x-pack/test/functional/apps/infra/home_page.ts @@ -21,7 +21,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'infraHome']); const supertest = getService('supertest'); - describe('Home page', function () { + // FLAKY: https://github.com/elastic/kibana/issues/75724 + describe.skip('Home page', function () { this.tags('includeFirefox'); before(async () => { await esArchiver.load('empty_kibana'); From 6686cffe01ab5ec667bcb612b27fa9cd3443e19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Wed, 2 Sep 2020 11:17:30 +0200 Subject: [PATCH 169/216] [ILM] TS conversion of policies table (#76006) * [ILM] TS conversion of policies table * [ILM] Fix type check errors * [ILM] Fix i18n check errors * [ILM] Add types to linkedIndices * [ILM] Add PR review fixes * [ILM] Add PR review fixes (filter fn) * [ILM] Fix i18n errors Co-authored-by: Elastic Machine --- ...est.js.snap => policy_table.test.tsx.snap} | 88 ++- ...cy_table.test.js => policy_table.test.tsx} | 103 ++-- .../public/application/index.tsx | 6 +- .../edit_policy/edit_policy.container.tsx | 40 +- ... add_policy_to_template_confirm_modal.tsx} | 38 +- .../confirm_delete.js => confirm_delete.tsx} | 15 +- .../no_match/components/no_match/index.js | 7 - .../no_match/components/no_match/no_match.js | 17 - .../policy_table/components/no_match/index.js | 7 - .../policy_table/policy_table.container.js | 59 -- .../components/policy_table/policy_table.js | 530 ------------------ .../policy_table/components/table_content.tsx | 376 +++++++++++++ .../sections/policy_table/index.d.ts | 7 - .../sections/policy_table/index.js | 7 - .../policy_table/index.js => index.ts} | 0 .../policy_table/policy_table.container.tsx | 75 +++ .../sections/policy_table/policy_table.tsx | 183 ++++++ .../application/services/filter_items.js | 17 - .../application/services/filter_items.ts | 13 + .../services/flatten_panel_tree.js | 20 - .../services/{index.js => index.ts} | 0 .../application/services/policies/types.ts | 1 + .../public/application/services/sort_table.js | 21 - .../public/application/services/sort_table.ts | 23 + .../public/application/store/actions/index.js | 7 - .../application/store/actions/policies.js | 42 -- .../public/application/store/index.d.ts | 7 - .../public/application/store/index.js | 7 - .../application/store/reducers/index.js | 12 - .../application/store/reducers/policies.js | 76 --- .../application/store/selectors/index.js | 7 - .../application/store/selectors/policies.js | 42 -- .../public/application/store/store.js | 17 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 35 files changed, 820 insertions(+), 1054 deletions(-) rename x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/{policy_table.test.js.snap => policy_table.test.tsx.snap} (70%) rename x-pack/plugins/index_lifecycle_management/__jest__/components/{policy_table.test.js => policy_table.test.tsx} (68%) rename x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/{policy_table/add_policy_to_template_confirm_modal.js => add_policy_to_template_confirm_modal.tsx} (89%) rename x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/{policy_table/confirm_delete.js => confirm_delete.tsx} (85%) delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/components/no_match/index.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/components/no_match/no_match.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/index.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.container.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/index.d.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/index.js rename x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/{components/policy_table/index.js => index.ts} (100%) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.container.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/filter_items.js create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/filter_items.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/flatten_panel_tree.js rename x-pack/plugins/index_lifecycle_management/public/application/services/{index.js => index.ts} (100%) delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.js create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/index.d.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/index.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/store/store.js diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap b/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap similarity index 70% rename from x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap rename to x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap index ad3e0956fcf259..cbb9f828887018 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap @@ -52,68 +52,60 @@ exports[`policy table should show empty state when there are not any policies 1`
-
+
+ +

+ Create your first index lifecycle policy +

+ class="euiText euiText--medium" + > +

+ An index lifecycle policy helps you manage your indices as they age. +

+
+ +
+
+ -
-
+ +
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx similarity index 68% rename from x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js rename to x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx index 60e3e9443bec98..d95b4503c266b0 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx @@ -4,54 +4,62 @@ * you may not use this file except in compliance with the Elastic License. */ import moment from 'moment-timezone'; -import React from 'react'; -import { Provider } from 'react-redux'; -// axios has a $http like interface so using it to simulate $http -import axios from 'axios'; -import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import sinon from 'sinon'; +import React, { ReactElement } from 'react'; +import { ReactWrapper } from 'enzyme'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject, takeMountedSnapshot } from '@elastic/eui/lib/test'; -import { scopedHistoryMock } from '../../../../../src/core/public/mocks'; -import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; -import { fetchedPolicies } from '../../public/application/store/actions'; -import { indexLifecycleManagementStore } from '../../public/application/store'; -import { PolicyTable } from '../../public/application/sections/policy_table'; +import { + fatalErrorsServiceMock, + injectedMetadataServiceMock, + scopedHistoryMock, +} from '../../../../../src/core/public/mocks'; +import { HttpService } from '../../../../../src/core/public/http'; +import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks'; + +import { PolicyTable } from '../../public/application/sections/policy_table/policy_table'; import { init as initHttp } from '../../public/application/services/http'; import { init as initUiMetric } from '../../public/application/services/ui_metric'; +import { PolicyFromES } from '../../public/application/services/policies/types'; -initHttp(axios.create({ adapter: axiosXhrAdapter }), (path) => path); -initUiMetric({ reportUiStats: () => {} }); - -let server = null; +initHttp( + new HttpService().setup({ + injectedMetadata: injectedMetadataServiceMock.createSetupContract(), + fatalErrors: fatalErrorsServiceMock.createSetupContract(), + }) +); +initUiMetric(usageCollectionPluginMock.createSetupContract()); -let store = null; -const policies = []; +const policies: PolicyFromES[] = []; for (let i = 0; i < 105; i++) { policies.push({ version: i, - modified_date: moment().subtract(i, 'days').valueOf(), - linkedIndices: i % 2 === 0 ? [`index${i}`] : null, + modified_date: moment().subtract(i, 'days').toISOString(), + linkedIndices: i % 2 === 0 ? [`index${i}`] : undefined, name: `testy${i}`, + policy: { + name: `testy${i}`, + phases: {}, + }, }); } jest.mock(''); -let component = null; +let component: ReactElement; -const snapshot = (rendered) => { +const snapshot = (rendered: string[]) => { expect(rendered).toMatchSnapshot(); }; -const mountedSnapshot = (rendered) => { +const mountedSnapshot = (rendered: ReactWrapper) => { expect(takeMountedSnapshot(rendered)).toMatchSnapshot(); }; -const names = (rendered) => { +const names = (rendered: ReactWrapper) => { return findTestSubject(rendered, 'policyTablePolicyNameLink'); }; -const namesText = (rendered) => { - return names(rendered).map((button) => button.text()); +const namesText = (rendered: ReactWrapper): string[] => { + return (names(rendered) as ReactWrapper).map((button) => button.text()); }; -const testSort = (headerName) => { +const testSort = (headerName: string) => { const rendered = mountWithIntl(component); const nameHeader = findTestSubject(rendered, `policyTableHeaderCell-${headerName}`).find( 'button' @@ -63,7 +71,7 @@ const testSort = (headerName) => { rendered.update(); snapshot(namesText(rendered)); }; -const openContextMenu = (buttonIndex) => { +const openContextMenu = (buttonIndex: number) => { const rendered = mountWithIntl(component); const actionsButton = findTestSubject(rendered, 'policyActionsContextMenuButton'); actionsButton.at(buttonIndex).simulate('click'); @@ -73,33 +81,26 @@ const openContextMenu = (buttonIndex) => { describe('policy table', () => { beforeEach(() => { - store = indexLifecycleManagementStore(); component = ( - - {}} /> - + ); - store.dispatch(fetchedPolicies(policies)); - server = sinon.fakeServer.create(); - server.respondWith('/api/index_lifecycle_management/policies', [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(policies), - ]); }); - test('should show spinner when policies are loading', () => { - store = indexLifecycleManagementStore(); + + test('should show empty state when there are not any policies', () => { component = ( - - {}} /> - + ); const rendered = mountWithIntl(component); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy(); - }); - test('should show empty state when there are not any policies', () => { - store.dispatch(fetchedPolicies([])); - const rendered = mountWithIntl(component); mountedSnapshot(rendered); }); test('should change pages when a pagination link is clicked on', () => { @@ -123,7 +124,7 @@ describe('policy table', () => { test('should filter based on content of search input', () => { const rendered = mountWithIntl(component); const searchInput = rendered.find('.euiFieldSearch').first(); - searchInput.instance().value = 'testy0'; + ((searchInput.instance() as unknown) as HTMLInputElement).value = 'testy0'; searchInput.simulate('keyup', { key: 'Enter', keyCode: 13, which: 13 }); rendered.update(); snapshot(namesText(rendered)); @@ -147,7 +148,7 @@ describe('policy table', () => { expect(buttons.at(0).text()).toBe('View indices linked to policy'); expect(buttons.at(1).text()).toBe('Add policy to index template'); expect(buttons.at(2).text()).toBe('Delete policy'); - expect(buttons.at(2).getDOMNode().disabled).toBeTruthy(); + expect((buttons.at(2).getDOMNode() as HTMLButtonElement).disabled).toBeTruthy(); }); test('should have proper actions in context menu when there are not linked indices', () => { const rendered = openContextMenu(1); @@ -155,7 +156,7 @@ describe('policy table', () => { expect(buttons.length).toBe(2); expect(buttons.at(0).text()).toBe('Add policy to index template'); expect(buttons.at(1).text()).toBe('Delete policy'); - expect(buttons.at(1).getDOMNode().disabled).toBeFalsy(); + expect((buttons.at(1).getDOMNode() as HTMLButtonElement).disabled).toBeFalsy(); }); test('confirmation modal should show when delete button is pressed', () => { const rendered = openContextMenu(1); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx index 31a9abdc7145ee..d7812f186a03fc 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx @@ -6,12 +6,10 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { Provider } from 'react-redux'; import { I18nStart, ScopedHistory, ApplicationStart } from 'kibana/public'; import { UnmountCallback } from 'src/core/public'; import { App } from './app'; -import { indexLifecycleManagementStore } from './store'; export const renderApp = ( element: Element, @@ -22,9 +20,7 @@ export const renderApp = ( ): UnmountCallback => { render( - - - + , element ); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx index 359134e015f7f2..f4697693b86c6f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { EuiButton, EuiCallOut, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useLoadPoliciesList } from '../../services/api'; @@ -50,25 +50,29 @@ export const EditPolicy: React.FunctionComponent +

+ +

} - color="danger" - > -

- {message} ({statusCode}) -

- - - - + body={ +

+ {message} ({statusCode}) +

+ } + actions={ + + + + } + /> ); } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx similarity index 89% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx index 47134ad0977209..90ac3c03856dea 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx @@ -20,21 +20,35 @@ import { EuiText, } from '@elastic/eui'; -import { toasts } from '../../../../services/notification'; -import { addLifecyclePolicyToTemplate, loadIndexTemplates } from '../../../../services/api'; -import { showApiError } from '../../../../services/api_errors'; -import { LearnMoreLink } from '../../../edit_policy/components'; +import { LearnMoreLink } from '../../edit_policy/components'; +import { PolicyFromES } from '../../../services/policies/types'; +import { addLifecyclePolicyToTemplate, loadIndexTemplates } from '../../../services/api'; +import { toasts } from '../../../services/notification'; +import { showApiError } from '../../../services/api_errors'; -export class AddPolicyToTemplateConfirmModal extends Component { - state = { - templates: [], - }; +interface Props { + policy: PolicyFromES; + onCancel: () => void; +} +interface State { + templates: Array<{ name: string }>; + templateName?: string; + aliasName?: string; + templateError?: string; +} +export class AddPolicyToTemplateConfirmModal extends Component { + constructor(props: Props) { + super(props); + this.state = { + templates: [], + }; + } async componentDidMount() { const templates = await loadIndexTemplates(); this.setState({ templates }); } addPolicyToTemplate = async () => { - const { policy, callback, onCancel } = this.props; + const { policy, onCancel } = this.props; const { templateName, aliasName } = this.state; const policyName = policy.name; if (!templateName) { @@ -71,9 +85,6 @@ export class AddPolicyToTemplateConfirmModal extends Component { ); showApiError(e, title); } - if (callback) { - callback(); - } }; renderTemplateHasPolicyWarning() { const selectedTemplate = this.getSelectedTemplate(); @@ -144,7 +155,7 @@ export class AddPolicyToTemplateConfirmModal extends Component { options={options} value={templateName} onChange={(e) => { - this.setState({ templateError: null, templateName: e.target.value }); + this.setState({ templateError: undefined, templateName: e.target.value }); }} /> @@ -204,7 +215,6 @@ export class AddPolicyToTemplateConfirmModal extends Component { defaultMessage: 'Add policy', } )} - onClose={onCancel} >

diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/confirm_delete.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx similarity index 85% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/confirm_delete.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx index 0ecc9cc13ecd0a..8d8e5ac2a24728 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/confirm_delete.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx @@ -9,11 +9,17 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; -import { toasts } from '../../../../services/notification'; -import { deletePolicy } from '../../../../services/api'; -import { showApiError } from '../../../../services/api_errors'; +import { PolicyFromES } from '../../../services/policies/types'; +import { toasts } from '../../../services/notification'; +import { showApiError } from '../../../services/api_errors'; +import { deletePolicy } from '../../../services/api'; -export class ConfirmDelete extends Component { +interface Props { + policyToDelete: PolicyFromES; + callback: () => void; + onCancel: () => void; +} +export class ConfirmDelete extends Component { deletePolicy = async () => { const { policyToDelete, callback } = this.props; const policyName = policyToDelete.name; @@ -61,7 +67,6 @@ export class ConfirmDelete extends Component { /> } buttonColor="danger" - onClose={onCancel} >

( -
- -
-); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/index.js deleted file mode 100644 index 63e8cdebd97716..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { NoMatch } from './components/no_match'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.container.js deleted file mode 100644 index 8bd78774d2d55a..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.container.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; - -import { - fetchPolicies, - policyFilterChanged, - policyPageChanged, - policyPageSizeChanged, - policySortChanged, -} from '../../../../store/actions'; - -import { - getPolicies, - getPageOfPolicies, - getPolicyPager, - getPolicyFilter, - getPolicySort, - isPolicyListLoaded, -} from '../../../../store/selectors'; - -import { PolicyTable as PresentationComponent } from './policy_table'; - -const mapDispatchToProps = (dispatch) => { - return { - policyFilterChanged: (filter) => { - dispatch(policyFilterChanged({ filter })); - }, - policyPageChanged: (pageNumber) => { - dispatch(policyPageChanged({ pageNumber })); - }, - policyPageSizeChanged: (pageSize) => { - dispatch(policyPageSizeChanged({ pageSize })); - }, - policySortChanged: (sortField, isSortAscending) => { - dispatch(policySortChanged({ sortField, isSortAscending })); - }, - fetchPolicies: (withIndices) => { - dispatch(fetchPolicies(withIndices)); - }, - }; -}; - -export const PolicyTable = connect( - (state) => ({ - totalNumberOfPolicies: getPolicies(state).length, - policies: getPageOfPolicies(state), - pager: getPolicyPager(state), - filter: getPolicyFilter(state), - sortField: getPolicySort(state).sortField, - isSortAscending: getPolicySort(state).isSortAscending, - policyListLoaded: isPolicyListLoaded(state), - }), - mapDispatchToProps -)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js deleted file mode 100644 index ec1cdb987f4b32..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js +++ /dev/null @@ -1,530 +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 React, { Component, Fragment } from 'react'; -import moment from 'moment-timezone'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { - EuiButton, - EuiButtonEmpty, - EuiLink, - EuiEmptyPrompt, - EuiFieldSearch, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiPopover, - EuiContextMenu, - EuiSpacer, - EuiTable, - EuiTableBody, - EuiTableHeader, - EuiTableHeaderCell, - EuiTablePagination, - EuiTableRow, - EuiTableRowCell, - EuiTitle, - EuiText, - EuiPageBody, - EuiPageContent, - EuiScreenReaderOnly, -} from '@elastic/eui'; - -import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; -import { reactRouterNavigate } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { getIndexListUri } from '../../../../../../../index_management/public'; -import { UIM_EDIT_CLICK } from '../../../../constants/ui_metric'; -import { getPolicyPath } from '../../../../services/navigation'; -import { flattenPanelTree } from '../../../../services/flatten_panel_tree'; -import { trackUiMetric } from '../../../../services/ui_metric'; -import { NoMatch } from '../no_match'; -import { ConfirmDelete } from './confirm_delete'; -import { AddPolicyToTemplateConfirmModal } from './add_policy_to_template_confirm_modal'; - -const COLUMNS = { - name: { - label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.nameHeader', { - defaultMessage: 'Name', - }), - width: 200, - }, - linkedIndices: { - label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.linkedIndicesHeader', { - defaultMessage: 'Linked indices', - }), - width: 120, - }, - version: { - label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.versionHeader', { - defaultMessage: 'Version', - }), - width: 120, - }, - modified_date: { - label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.modifiedDateHeader', { - defaultMessage: 'Modified date', - }), - width: 200, - }, -}; - -export class PolicyTable extends Component { - constructor(props) { - super(props); - - this.state = { - selectedPoliciesMap: {}, - renderConfirmModal: null, - }; - } - componentDidMount() { - this.props.fetchPolicies(true); - } - renderEmpty() { - return ( - - -

- } - body={ - -

- -

-
- } - actions={this.renderCreatePolicyButton()} - /> - ); - } - renderDeleteConfirmModal = () => { - const { policyToDelete } = this.state; - if (!policyToDelete) { - return null; - } - return ( - this.setState({ renderConfirmModal: null, policyToDelete: null })} - /> - ); - }; - renderAddPolicyToTemplateConfirmModal = () => { - const { policyToAddToTemplate } = this.state; - if (!policyToAddToTemplate) { - return null; - } - return ( - this.setState({ renderConfirmModal: null, policyToAddToTemplate: null })} - /> - ); - }; - handleDelete = () => { - this.props.fetchPolicies(true); - this.setState({ renderDeleteConfirmModal: null, policyToDelete: null }); - }; - onSort = (column) => { - const { sortField, isSortAscending, policySortChanged } = this.props; - const newIsSortAscending = sortField === column ? !isSortAscending : true; - policySortChanged(column, newIsSortAscending); - }; - - buildHeader() { - const { sortField, isSortAscending } = this.props; - const headers = Object.entries(COLUMNS).map(([fieldName, { label, width }]) => { - const isSorted = sortField === fieldName; - return ( - this.onSort(fieldName)} - isSorted={isSorted} - isSortAscending={isSortAscending} - data-test-subj={`policyTableHeaderCell-${fieldName}`} - className={'policyTable__header--' + fieldName} - width={width} - > - {label} - - ); - }); - headers.push( - - ); - return headers; - } - - buildRowCell(fieldName, value) { - if (fieldName === 'name') { - return ( - /* eslint-disable-next-line @elastic/eui/href-or-on-click */ - - trackUiMetric('click', UIM_EDIT_CLICK) - )} - > - {value} - - ); - } else if (fieldName === 'linkedIndices') { - return ( - - {value ? value.length : '0'} - - ); - } else if (fieldName === 'modified_date' && value) { - return moment(value).format('YYYY-MM-DD HH:mm:ss'); - } - return value; - } - renderCreatePolicyButton() { - return ( - - - - ); - } - renderConfirmModal() { - const { renderConfirmModal } = this.state; - if (renderConfirmModal) { - return renderConfirmModal(); - } else { - return null; - } - } - buildActionPanelTree(policy) { - const hasLinkedIndices = Boolean(policy.linkedIndices && policy.linkedIndices.length); - - const viewIndicesLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.policyTable.viewIndicesButtonText', - { - defaultMessage: 'View indices linked to policy', - } - ); - const addPolicyToTemplateLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.policyTable.addPolicyToTemplateButtonText', - { - defaultMessage: 'Add policy to index template', - } - ); - const deletePolicyLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonText', - { - defaultMessage: 'Delete policy', - } - ); - const deletePolicyTooltip = hasLinkedIndices - ? i18n.translate('xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonDisabledTooltip', { - defaultMessage: 'You cannot delete a policy that is being used by an index', - }) - : null; - const items = []; - if (hasLinkedIndices) { - items.push({ - name: viewIndicesLabel, - icon: 'list', - onClick: () => { - this.props.navigateToApp('management', { - path: `/data/index_management${getIndexListUri(`ilm.policy:${policy.name}`, true)}`, - }); - }, - }); - } - items.push({ - name: addPolicyToTemplateLabel, - icon: 'plusInCircle', - onClick: () => - this.setState({ - renderConfirmModal: this.renderAddPolicyToTemplateConfirmModal, - policyToAddToTemplate: policy, - }), - }); - items.push({ - name: deletePolicyLabel, - disabled: hasLinkedIndices, - icon: 'trash', - toolTipContent: deletePolicyTooltip, - onClick: () => - this.setState({ - renderConfirmModal: this.renderDeleteConfirmModal, - policyToDelete: policy, - }), - }); - const panelTree = { - id: 0, - title: i18n.translate('xpack.indexLifecycleMgmt.policyTable.policyActionsMenu.panelTitle', { - defaultMessage: 'Policy options', - }), - items, - }; - return flattenPanelTree(panelTree); - } - togglePolicyPopover = (policy) => { - if (this.isPolicyPopoverOpen(policy)) { - this.closePolicyPopover(policy); - } else { - this.openPolicyPopover(policy); - } - }; - isPolicyPopoverOpen = (policy) => { - return this.state.policyPopover === policy.name; - }; - closePolicyPopover = (policy) => { - if (this.isPolicyPopoverOpen(policy)) { - this.setState({ policyPopover: null }); - } - }; - openPolicyPopover = (policy) => { - this.setState({ policyPopover: policy.name }); - }; - buildRowCells(policy) { - const { name } = policy; - const cells = Object.entries(COLUMNS).map(([fieldName, { width }]) => { - const value = policy[fieldName]; - - if (fieldName === 'name') { - return ( - -
- {this.buildRowCell(fieldName, value)} -
- - ); - } - - return ( - - {this.buildRowCell(fieldName, value)} - - ); - }); - const button = ( - this.togglePolicyPopover(policy)} - color="primary" - > - {i18n.translate('xpack.indexLifecycleMgmt.policyTable.actionsButtonText', { - defaultMessage: 'Actions', - })} - - ); - cells.push( - - this.closePolicyPopover(policy)} - panelPaddingSize="none" - withTitle - anchorPosition="rightUp" - repositionOnScroll - > - - - - ); - return cells; - } - - buildRows() { - const { policies = [] } = this.props; - return policies.map((policy) => { - const { name } = policy; - return {this.buildRowCells(policy)}; - }); - } - - renderPager() { - const { pager, policyPageChanged, policyPageSizeChanged } = this.props; - return ( - - ); - } - - onItemSelectionChanged = (selectedPolicies) => { - this.setState({ selectedPolicies }); - }; - - render() { - const { - totalNumberOfPolicies, - policyFilterChanged, - filter, - policyListLoaded, - policies, - } = this.props; - const { selectedPoliciesMap } = this.state; - const numSelected = Object.keys(selectedPoliciesMap).length; - let content; - let tableContent; - if (totalNumberOfPolicies || !policyListLoaded) { - if (!policyListLoaded) { - tableContent = ; - } else if (totalNumberOfPolicies > 0) { - tableContent = ( - - - - - - - {this.buildHeader()} - {this.buildRows()} - - ); - } else { - tableContent = ; - } - content = ( - - - {numSelected > 0 ? ( - - this.setState({ showDeleteConfirmation: true })} - > - - - - ) : null} - - { - policyFilterChanged(event.target.value); - }} - data-test-subj="policyTableFilterInput" - placeholder={i18n.translate( - 'xpack.indexLifecycleMgmt.policyTable.systempoliciesSearchInputPlaceholder', - { - defaultMessage: 'Search', - } - )} - aria-label={i18n.translate( - 'xpack.indexLifecycleMgmt.policyTable.systempoliciesSearchInputAriaLabel', - { - defaultMessage: 'Search policies', - } - )} - /> - - - - {tableContent} - - ); - } else { - content = this.renderEmpty(); - } - - return ( - - -
- {this.renderConfirmModal()} - {totalNumberOfPolicies || !policyListLoaded ? ( - - - - -

- -

-
-
- {totalNumberOfPolicies ? ( - {this.renderCreatePolicyButton()} - ) : null} -
- - -

- -

-
-
- ) : null} - - {content} - - {totalNumberOfPolicies && totalNumberOfPolicies > 10 ? this.renderPager() : null} -
-
-
- ); - } -} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx new file mode 100644 index 00000000000000..da36ff4df98f5c --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx @@ -0,0 +1,376 @@ +/* + * 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, { ReactElement, useState, Fragment, ReactNode } from 'react'; +import { + EuiButtonEmpty, + EuiContextMenu, + EuiLink, + EuiPopover, + EuiScreenReaderOnly, + EuiSpacer, + EuiTable, + EuiTableBody, + EuiTableHeader, + EuiTableHeaderCell, + EuiTablePagination, + EuiTableRow, + EuiTableRowCell, + EuiText, + Pager, + EuiContextMenuPanelDescriptor, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; + +import moment from 'moment'; +import { ApplicationStart } from 'kibana/public'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { RouteComponentProps } from 'react-router-dom'; +import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; +import { getIndexListUri } from '../../../../../../index_management/public'; +import { PolicyFromES } from '../../../services/policies/types'; +import { getPolicyPath } from '../../../services/navigation'; +import { sortTable } from '../../../services'; +import { trackUiMetric } from '../../../services/ui_metric'; + +import { UIM_EDIT_CLICK } from '../../../constants'; +import { AddPolicyToTemplateConfirmModal } from './add_policy_to_template_confirm_modal'; +import { ConfirmDelete } from './confirm_delete'; + +type PolicyProperty = Extract< + keyof PolicyFromES, + 'version' | 'name' | 'linkedIndices' | 'modified_date' +>; +const COLUMNS: Array<[PolicyProperty, { label: string; width: number }]> = [ + [ + 'name', + { + label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.nameHeader', { + defaultMessage: 'Name', + }), + width: 200, + }, + ], + [ + 'linkedIndices', + { + label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.linkedIndicesHeader', { + defaultMessage: 'Linked indices', + }), + width: 120, + }, + ], + [ + 'version', + { + label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.versionHeader', { + defaultMessage: 'Version', + }), + width: 120, + }, + ], + [ + 'modified_date', + { + label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.modifiedDateHeader', { + defaultMessage: 'Modified date', + }), + width: 200, + }, + ], +]; + +interface Props { + policies: PolicyFromES[]; + totalNumber: number; + navigateToApp: ApplicationStart['navigateToApp']; + setConfirmModal: (modal: ReactElement | null) => void; + handleDelete: () => void; + history: RouteComponentProps['history']; +} +export const TableContent: React.FunctionComponent = ({ + policies, + totalNumber, + navigateToApp, + setConfirmModal, + handleDelete, + history, +}) => { + const [popoverPolicy, setPopoverPolicy] = useState(); + const [sort, setSort] = useState<{ sortField: PolicyProperty; isSortAscending: boolean }>({ + sortField: 'name', + isSortAscending: true, + }); + const [pageSize, setPageSize] = useState(10); + const [currentPage, setCurrentPage] = useState(0); + + let sortedPolicies = sortTable(policies, sort.sortField, sort.isSortAscending); + const pager = new Pager(totalNumber, pageSize, currentPage); + const { firstItemIndex, lastItemIndex } = pager; + sortedPolicies = sortedPolicies.slice(firstItemIndex, lastItemIndex + 1); + + const isPolicyPopoverOpen = (policyName: string): boolean => popoverPolicy === policyName; + const closePolicyPopover = (): void => { + setPopoverPolicy(''); + }; + const openPolicyPopover = (policyName: string): void => { + setPopoverPolicy(policyName); + }; + const togglePolicyPopover = (policyName: string): void => { + if (isPolicyPopoverOpen(policyName)) { + closePolicyPopover(); + } else { + openPolicyPopover(policyName); + } + }; + + const onSort = (column: PolicyProperty) => { + const newIsSortAscending = sort.sortField === column ? !sort.isSortAscending : true; + setSort({ sortField: column, isSortAscending: newIsSortAscending }); + }; + + const headers = []; + COLUMNS.forEach(([fieldName, { label, width }]) => { + const isSorted = sort.sortField === fieldName; + headers.push( + onSort(fieldName)} + isSorted={isSorted} + isSortAscending={sort.isSortAscending} + data-test-subj={`policyTableHeaderCell-${fieldName}`} + className={'policyTable__header--' + fieldName} + width={width} + > + {label} + + ); + }); + headers.push( + + ); + + const buildActionPanelTree = (policy: PolicyFromES): EuiContextMenuPanelDescriptor[] => { + const hasLinkedIndices = Boolean(policy.linkedIndices && policy.linkedIndices.length); + + const viewIndicesLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.policyTable.viewIndicesButtonText', + { + defaultMessage: 'View indices linked to policy', + } + ); + const addPolicyToTemplateLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.policyTable.addPolicyToTemplateButtonText', + { + defaultMessage: 'Add policy to index template', + } + ); + const deletePolicyLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonText', + { + defaultMessage: 'Delete policy', + } + ); + const deletePolicyTooltip = hasLinkedIndices + ? i18n.translate('xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonDisabledTooltip', { + defaultMessage: 'You cannot delete a policy that is being used by an index', + }) + : null; + const items = []; + if (hasLinkedIndices) { + items.push({ + name: viewIndicesLabel, + icon: 'list', + onClick: () => { + navigateToApp('management', { + path: `/data/index_management${getIndexListUri(`ilm.policy:${policy.name}`, true)}`, + }); + }, + }); + } + items.push({ + name: addPolicyToTemplateLabel, + icon: 'plusInCircle', + onClick: () => { + setConfirmModal(renderAddPolicyToTemplateConfirmModal(policy)); + }, + }); + items.push({ + name: deletePolicyLabel, + disabled: hasLinkedIndices, + icon: 'trash', + toolTipContent: deletePolicyTooltip, + onClick: () => { + setConfirmModal(renderDeleteConfirmModal(policy)); + }, + }); + const panelTree = { + id: 0, + title: i18n.translate('xpack.indexLifecycleMgmt.policyTable.policyActionsMenu.panelTitle', { + defaultMessage: 'Policy options', + }), + items, + }; + return [panelTree]; + }; + + const renderRowCell = (fieldName: string, value: string | number | string[]): ReactNode => { + if (fieldName === 'name') { + return ( + + trackUiMetric(METRIC_TYPE.CLICK, UIM_EDIT_CLICK) + )} + > + {value} + + ); + } else if (fieldName === 'linkedIndices') { + return ( + + {value ? (value as string[]).length : '0'} + + ); + } else if (fieldName === 'modified_date' && value) { + return moment(value).format('YYYY-MM-DD HH:mm:ss'); + } + return value; + }; + + const renderRowCells = (policy: PolicyFromES): ReactElement[] => { + const { name } = policy; + const cells = []; + COLUMNS.forEach(([fieldName, { width }]) => { + const value: any = policy[fieldName]; + + if (fieldName === 'name') { + cells.push( + +
+ {renderRowCell(fieldName, value)} +
+ + ); + } else { + cells.push( + + {renderRowCell(fieldName, value)} + + ); + } + }); + const button = ( + togglePolicyPopover(policy.name)} + color="primary" + > + {i18n.translate('xpack.indexLifecycleMgmt.policyTable.actionsButtonText', { + defaultMessage: 'Actions', + })} + + ); + cells.push( + + + + + + ); + return cells; + }; + + const rows = sortedPolicies.map((policy) => { + const { name } = policy; + return {renderRowCells(policy)}; + }); + + const renderAddPolicyToTemplateConfirmModal = (policy: PolicyFromES): ReactElement => { + return ( + setConfirmModal(null)} /> + ); + }; + + const renderDeleteConfirmModal = (policy: PolicyFromES): ReactElement => { + return ( + { + setConfirmModal(null); + }} + /> + ); + }; + + const renderPager = (): ReactNode => { + return ( + + ); + }; + + return ( + + + + + + + + {headers} + {rows} + + + {policies.length > 10 ? renderPager() : null} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/index.d.ts deleted file mode 100644 index fa1b1129523eb4..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/index.d.ts +++ /dev/null @@ -1,7 +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. - */ - -export declare const PolicyTable: any; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/index.js deleted file mode 100644 index c4aa32f1f7dc28..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { PolicyTable } from './components/policy_table'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/index.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/index.ts diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.container.tsx new file mode 100644 index 00000000000000..f6471ff2da4d3b --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.container.tsx @@ -0,0 +1,75 @@ +/* + * 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 { ApplicationStart } from 'kibana/public'; +import { RouteComponentProps } from 'react-router-dom'; +import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { PolicyTable as PresentationComponent } from './policy_table'; +import { useLoadPoliciesList } from '../../services/api'; + +interface Props { + navigateToApp: ApplicationStart['navigateToApp']; +} + +export const PolicyTable: React.FunctionComponent = ({ + navigateToApp, + history, +}) => { + const { data: policies, isLoading, error, sendRequest } = useLoadPoliciesList(true); + + if (isLoading) { + return ( + } + body={ + + } + /> + ); + } + if (error) { + const { statusCode, message } = error ? error : { statusCode: '', message: '' }; + return ( + + +

+ } + body={ +

+ {message} ({statusCode}) +

+ } + actions={ + + + + } + /> + ); + } + + return ( + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx new file mode 100644 index 00000000000000..048ab922a65b59 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx @@ -0,0 +1,183 @@ +/* + * 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, ReactElement, ReactNode, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + EuiButton, + EuiEmptyPrompt, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, + EuiText, + EuiPageBody, + EuiPageContent, +} from '@elastic/eui'; +import { ApplicationStart } from 'kibana/public'; +import { RouteComponentProps } from 'react-router-dom'; +import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; +import { PolicyFromES } from '../../services/policies/types'; +import { filterItems } from '../../services'; +import { TableContent } from './components/table_content'; + +interface Props { + policies: PolicyFromES[]; + history: RouteComponentProps['history']; + navigateToApp: ApplicationStart['navigateToApp']; + updatePolicies: () => void; +} + +export const PolicyTable: React.FunctionComponent = ({ + policies, + history, + navigateToApp, + updatePolicies, +}) => { + const [confirmModal, setConfirmModal] = useState(); + const [filter, setFilter] = useState(''); + + const createPolicyButton = ( + + + + ); + + let content: ReactElement; + + if (policies.length > 0) { + const filteredPolicies = filterItems('name', filter, policies); + let tableContent: ReactElement; + if (filteredPolicies.length > 0) { + tableContent = ( + { + updatePolicies(); + setConfirmModal(null); + }} + history={history} + /> + ); + } else { + tableContent = ( + + ); + } + + content = ( + + + + { + setFilter(event.target.value); + }} + data-test-subj="policyTableFilterInput" + placeholder={i18n.translate( + 'xpack.indexLifecycleMgmt.policyTable.systempoliciesSearchInputPlaceholder', + { + defaultMessage: 'Search', + } + )} + aria-label={i18n.translate( + 'xpack.indexLifecycleMgmt.policyTable.systempoliciesSearchInputAriaLabel', + { + defaultMessage: 'Search policies', + } + )} + /> + + + + {tableContent} + + ); + } else { + return ( + + + + + + } + body={ + +

+ +

+
+ } + actions={createPolicyButton} + /> +
+
+ ); + } + + return ( + + + {confirmModal} + + + + +

+ +

+
+
+ {createPolicyButton} +
+ + +

+ +

+
+ + + {content} +
+
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/filter_items.js b/x-pack/plugins/index_lifecycle_management/public/application/services/filter_items.js deleted file mode 100644 index dcc9036463b821..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/filter_items.js +++ /dev/null @@ -1,17 +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. - */ - -export const filterItems = (fields, filter = '', items = []) => { - const lowerFilter = filter.toLowerCase(); - return items.filter((item) => { - const actualFields = fields || Object.keys(item); - const indexOfMatch = actualFields.findIndex((field) => { - const normalizedField = String(item[field]).toLowerCase(); - return normalizedField.includes(lowerFilter); - }); - return indexOfMatch !== -1; - }); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/filter_items.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/filter_items.ts new file mode 100644 index 00000000000000..237ce567707bbe --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/filter_items.ts @@ -0,0 +1,13 @@ +/* + * 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 const filterItems = (field: keyof T, filter: string, items: T[] = []): T[] => { + const lowerFilter = filter.toLowerCase(); + return items.filter((item: T) => { + const normalizedValue = String(item[field]).toLowerCase(); + return normalizedValue.includes(lowerFilter); + }); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/flatten_panel_tree.js b/x-pack/plugins/index_lifecycle_management/public/application/services/flatten_panel_tree.js deleted file mode 100644 index 2bb3903a6ef45a..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/flatten_panel_tree.js +++ /dev/null @@ -1,20 +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. - */ - -export const flattenPanelTree = (tree, array = []) => { - array.push(tree); - - if (tree.items) { - tree.items.forEach((item) => { - if (item.panel) { - flattenPanelTree(item.panel, array); - item.panel = item.panel.id; - } - }); - } - - return array; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/index.js b/x-pack/plugins/index_lifecycle_management/public/application/services/index.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/public/application/services/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/index.ts diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts index 3d4c73cf4a82cf..c191f82cf05cc1 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts @@ -22,6 +22,7 @@ export interface PolicyFromES { name: string; policy: SerializedPolicy; version: number; + linkedIndices?: string[]; } export interface SerializedPhase { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.js b/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.js deleted file mode 100644 index 1b1446bb735c13..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.js +++ /dev/null @@ -1,21 +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 { sortBy } from 'lodash'; - -const stringSort = (fieldName) => (item) => item[fieldName]; -const arraySort = (fieldName) => (item) => (item[fieldName] || []).length; - -const sorters = { - version: stringSort('version'), - name: stringSort('name'), - linkedIndices: arraySort('linkedIndices'), - modified_date: stringSort('modified_date'), -}; -export const sortTable = (array = [], sortField, isSortAscending) => { - const sorted = sortBy(array, sorters[sortField]); - return isSortAscending ? sorted : sorted.reverse(); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.ts new file mode 100644 index 00000000000000..6b41d671b673f3 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.ts @@ -0,0 +1,23 @@ +/* + * 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 { sortBy } from 'lodash'; +import { PolicyFromES } from './policies/types'; + +export const sortTable = ( + array: PolicyFromES[] = [], + sortField: Extract, + isSortAscending: boolean +): PolicyFromES[] => { + let sorter; + if (sortField === 'linkedIndices') { + sorter = (item: PolicyFromES) => (item[sortField] || []).length; + } else { + sorter = (item: PolicyFromES) => item[sortField]; + } + const sorted = sortBy(array, sorter); + return isSortAscending ? sorted : sorted.reverse(); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js deleted file mode 100644 index fef79c7782bb05..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js +++ /dev/null @@ -1,7 +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. - */ - -export * from './policies'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js deleted file mode 100644 index d47136679604fd..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js +++ /dev/null @@ -1,42 +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 { i18n } from '@kbn/i18n'; -import { createAction } from 'redux-actions'; - -import { showApiError } from '../../services/api_errors'; -import { loadPolicies } from '../../services/api'; - -export const fetchedPolicies = createAction('FETCHED_POLICIES'); -export const setSelectedPolicy = createAction('SET_SELECTED_POLICY'); -export const unsetSelectedPolicy = createAction('UNSET_SELECTED_POLICY'); -export const setSelectedPolicyName = createAction('SET_SELECTED_POLICY_NAME'); -export const setSaveAsNewPolicy = createAction('SET_SAVE_AS_NEW_POLICY'); -export const policySortChanged = createAction('POLICY_SORT_CHANGED'); -export const policyPageSizeChanged = createAction('POLICY_PAGE_SIZE_CHANGED'); -export const policyPageChanged = createAction('POLICY_PAGE_CHANGED'); -export const policySortDirectionChanged = createAction('POLICY_SORT_DIRECTION_CHANGED'); -export const policyFilterChanged = createAction('POLICY_FILTER_CHANGED'); - -export const fetchPolicies = (withIndices, callback) => async (dispatch) => { - let policies; - try { - policies = await loadPolicies(withIndices); - } catch (err) { - const title = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.loadPolicyErrorMessage', { - defaultMessage: 'Error loading policies', - }); - showApiError(err, title); - return false; - } - - dispatch(fetchedPolicies(policies)); - if (policies.length === 0) { - dispatch(setSelectedPolicy()); - } - callback && callback(); - return policies; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/application/store/index.d.ts deleted file mode 100644 index 8617a7045a5c34..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/index.d.ts +++ /dev/null @@ -1,7 +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. - */ - -export declare const indexLifecycleManagementStore: any; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/index.js deleted file mode 100644 index 808eb489bf9137..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { indexLifecycleManagementStore } from './store'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js deleted file mode 100644 index 7fe7134f5f5db6..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js +++ /dev/null @@ -1,12 +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 { combineReducers } from 'redux'; -import { policies } from './policies'; - -export const indexLifecycleManagement = combineReducers({ - policies, -}); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js deleted file mode 100644 index ca9d59e295a29d..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js +++ /dev/null @@ -1,76 +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 { handleActions } from 'redux-actions'; -import { - fetchedPolicies, - policyFilterChanged, - policyPageChanged, - policyPageSizeChanged, - policySortChanged, -} from '../actions'; - -const defaultState = { - isLoading: false, - isLoaded: false, - originalPolicyName: undefined, - selectedPolicySet: false, - policies: [], - sort: { - sortField: 'name', - isSortAscending: true, - }, - pageSize: 10, - currentPage: 0, - filter: '', -}; - -export const policies = handleActions( - { - [fetchedPolicies](state, { payload: policies }) { - return { - ...state, - isLoading: false, - isLoaded: true, - policies, - }; - }, - [policyFilterChanged](state, action) { - const { filter } = action.payload; - return { - ...state, - filter, - currentPage: 0, - }; - }, - [policySortChanged](state, action) { - const { sortField, isSortAscending } = action.payload; - - return { - ...state, - sort: { - sortField, - isSortAscending, - }, - }; - }, - [policyPageChanged](state, action) { - const { pageNumber } = action.payload; - return { - ...state, - currentPage: pageNumber, - }; - }, - [policyPageSizeChanged](state, action) { - const { pageSize } = action.payload; - return { - ...state, - pageSize, - }; - }, - }, - defaultState -); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js deleted file mode 100644 index fef79c7782bb05..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js +++ /dev/null @@ -1,7 +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. - */ - -export * from './policies'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js deleted file mode 100644 index e1c89314a2ec56..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js +++ /dev/null @@ -1,42 +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 { createSelector } from 'reselect'; -import { Pager } from '@elastic/eui'; - -import { filterItems, sortTable } from '../../services'; - -export const getPolicies = (state) => state.policies.policies; -export const getPolicyFilter = (state) => state.policies.filter; -export const getPolicySort = (state) => state.policies.sort; -export const getPolicyCurrentPage = (state) => state.policies.currentPage; -export const getPolicyPageSize = (state) => state.policies.pageSize; -export const isPolicyListLoaded = (state) => state.policies.isLoaded; - -const getFilteredPolicies = createSelector(getPolicies, getPolicyFilter, (policies, filter) => { - return filterItems(['name'], filter, policies); -}); -export const getTotalPolicies = createSelector(getFilteredPolicies, (filteredPolicies) => { - return filteredPolicies.length; -}); -export const getPolicyPager = createSelector( - getPolicyCurrentPage, - getPolicyPageSize, - getTotalPolicies, - (currentPage, pageSize, totalPolicies) => { - return new Pager(totalPolicies, pageSize, currentPage); - } -); -export const getPageOfPolicies = createSelector( - getFilteredPolicies, - getPolicySort, - getPolicyPager, - (filteredPolicies, sort, pager) => { - const sortedPolicies = sortTable(filteredPolicies, sort.sortField, sort.isSortAscending); - const { firstItemIndex, lastItemIndex } = pager; - return sortedPolicies.slice(firstItemIndex, lastItemIndex + 1); - } -); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/store.js b/x-pack/plugins/index_lifecycle_management/public/application/store/store.js deleted file mode 100644 index c5774a3da238a5..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/store.js +++ /dev/null @@ -1,17 +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 { createStore, applyMiddleware, compose } from 'redux'; -import thunk from 'redux-thunk'; - -import { indexLifecycleManagement } from './reducers/'; - -export const indexLifecycleManagementStore = (initialState = {}) => { - const enhancers = [applyMiddleware(thunk)]; - - window.__REDUX_DEVTOOLS_EXTENSION__ && enhancers.push(window.__REDUX_DEVTOOLS_EXTENSION__()); - return createStore(indexLifecycleManagement, initialState, compose(...enhancers)); -}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index df78975d21b075..093ef4d6f1873d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8100,7 +8100,6 @@ "xpack.indexLifecycleMgmt.editPolicy.learnAboutShardAllocationLink": "シャード割り当ての詳細をご覧ください", "xpack.indexLifecycleMgmt.editPolicy.learnAboutTimingText": "タイミングの詳細をご覧ください", "xpack.indexLifecycleMgmt.editPolicy.lifecyclePolicyDescriptionText": "インデックスへのアクティブな書き込みから削除までの、インデックスライフサイクルの 4 つのフェーズを自動化するには、インデックスポリシーを使用します。", - "xpack.indexLifecycleMgmt.editPolicy.loadPolicyErrorMessage": "ポリシーの読み込み中にエラーが発生しました", "xpack.indexLifecycleMgmt.editPolicy.maximumAgeMissingError": "最高年齢が必要です。", "xpack.indexLifecycleMgmt.editPolicy.maximumDocumentsMissingError": "最高ドキュメント数が必要です。", "xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError": "最大インデックスサイズが必要です。", @@ -8239,7 +8238,6 @@ "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.title": "インデックステンプレートにポリシー「{name}」 を追加", "xpack.indexLifecycleMgmt.policyTable.addPolicyToTemplateButtonText": "インデックステンプレートにポリシーを追加", "xpack.indexLifecycleMgmt.policyTable.captionText": "以下は {total} 列中 {count, plural, one {# 列} other {# 列}} を含むインデックスライフサイクルポリシー表です。", - "xpack.indexLifecycleMgmt.policyTable.deletedPoliciesText": "{numSelected} 件の{numSelected, plural, one {ポリシー} other {ポリシー}}が削除されました", "xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonDisabledTooltip": "インデックスが使用中のポリシーは削除できません", "xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonText": "ポリシーを削除", "xpack.indexLifecycleMgmt.policyTable.emptyPrompt.createButtonLabel": "ポリシーを作成", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index cfee565a1da933..ea04b1e14d9591 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8103,7 +8103,6 @@ "xpack.indexLifecycleMgmt.editPolicy.learnAboutShardAllocationLink": "了解分片分配", "xpack.indexLifecycleMgmt.editPolicy.learnAboutTimingText": "了解计时", "xpack.indexLifecycleMgmt.editPolicy.lifecyclePolicyDescriptionText": "使用索引策略自动化索引生命周期的四个阶段,从频繁地写入到索引到删除索引。", - "xpack.indexLifecycleMgmt.editPolicy.loadPolicyErrorMessage": "加载策略时出错", "xpack.indexLifecycleMgmt.editPolicy.maximumAgeMissingError": "最大存在时间必填。", "xpack.indexLifecycleMgmt.editPolicy.maximumDocumentsMissingError": "最大文档数必填。", "xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError": "最大索引大小必填。", @@ -8242,7 +8241,6 @@ "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.title": "将策略 “{name}” 添加到索引模板", "xpack.indexLifecycleMgmt.policyTable.addPolicyToTemplateButtonText": "将策略添加到索引模板", "xpack.indexLifecycleMgmt.policyTable.captionText": "下面是包含 {count, plural, one {# 行} other {# 行}}(共 {total} 行)的索引生命周期策略表。", - "xpack.indexLifecycleMgmt.policyTable.deletedPoliciesText": "已删除 {numSelected} 个{numSelected, plural, one {策略} other {策略}}", "xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonDisabledTooltip": "您无法删除索引正在使用的策略", "xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonText": "删除策略", "xpack.indexLifecycleMgmt.policyTable.emptyPrompt.createButtonLabel": "创建策略", From febeb478757204da7b2088f91fa1c3be43e28af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Wed, 2 Sep 2020 11:30:17 +0200 Subject: [PATCH 170/216] [Security Solution] Refactor Network HTTP to use Search Strategy (#76243) --- .../security_solution/index.ts | 17 +- .../security_solution/network/http/index.ts | 58 ++++ .../security_solution/network/index.ts | 2 + .../network/containers/network_http/index.tsx | 286 ++++++++++-------- .../containers/network_http/translations.ts | 21 ++ .../ip_details/network_http_query_table.tsx | 70 ++--- .../pages/navigation/http_query_tab_body.tsx | 68 ++--- .../factory/network/http/helpers.ts | 34 +++ .../factory/network/http/index.ts | 59 ++++ .../network/http/query.http_network.dsl.ts | 116 +++++++ .../factory/network/index.ts | 2 + 11 files changed, 532 insertions(+), 201 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/search_strategy/security_solution/network/http/index.ts create mode 100644 x-pack/plugins/security_solution/public/network/containers/network_http/translations.ts create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/helpers.ts create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/index.ts create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/query.http_network.dsl.ts diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index 175784bc5ade57..6905f2be38966d 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -15,7 +15,13 @@ import { HostsRequestOptions, HostsStrategyResponse, } from './hosts'; -import { NetworkQueries, NetworkTlsStrategyResponse, NetworkTlsRequestOptions } from './network'; +import { + NetworkQueries, + NetworkTlsStrategyResponse, + NetworkTlsRequestOptions, + NetworkHttpStrategyResponse, + NetworkHttpRequestOptions, +} from './network'; export * from './hosts'; export * from './network'; @@ -116,6 +122,8 @@ export type StrategyResponseType = T extends HostsQ ? HostFirstLastSeenStrategyResponse : T extends NetworkQueries.tls ? NetworkTlsStrategyResponse + : T extends NetworkQueries.http + ? NetworkHttpStrategyResponse : never; export type StrategyRequestType = T extends HostsQueries.hosts @@ -126,4 +134,11 @@ export type StrategyRequestType = T extends HostsQu ? HostFirstLastSeenRequestOptions : T extends NetworkQueries.tls ? NetworkTlsRequestOptions + : T extends NetworkQueries.http + ? NetworkHttpRequestOptions : never; + +export interface GenericBuckets { + key: string; + doc_count: number; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/http/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/http/index.ts new file mode 100644 index 00000000000000..c42b3d2ab8db30 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/http/index.ts @@ -0,0 +1,58 @@ +/* + * 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 { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { + Maybe, + CursorType, + Inspect, + RequestOptionsPaginated, + PageInfoPaginated, + GenericBuckets, +} from '../..'; + +export interface NetworkHttpRequestOptions extends RequestOptionsPaginated { + ip?: string; + defaultIndex: string[]; +} + +export interface NetworkHttpStrategyResponse extends IEsSearchResponse { + edges: NetworkHttpEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface NetworkHttpEdges { + node: NetworkHttpItem; + cursor: CursorType; +} + +export interface NetworkHttpItem { + _id?: Maybe; + domains: string[]; + lastHost?: Maybe; + lastSourceIp?: Maybe; + methods: string[]; + path?: Maybe; + requestCount?: Maybe; + statuses: string[]; +} + +export interface NetworkHttpBuckets { + key: string; + doc_count: number; + domains: { + buckets: GenericBuckets[]; + }; + methods: { + buckets: GenericBuckets[]; + }; + source: object; + status: { + buckets: GenericBuckets[]; + }; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts index 680a3697ef0bd5..194bb5d057e3fc 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts @@ -5,7 +5,9 @@ */ export * from './tls'; +export * from './http'; export enum NetworkQueries { + http = 'http', tls = 'tls', } diff --git a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx index 60845d452d69e4..ae50f6919dce1f 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx @@ -4,29 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; +import { noop } from 'lodash/fp'; +import { useState, useEffect, useCallback, useRef } from 'react'; +import { shallowEqual, useSelector } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; +import { ESTermQuery } from '../../../../common/typed_json'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { - GetNetworkHttpQuery, - NetworkHttpEdges, - NetworkHttpSortField, - PageInfoPaginated, -} from '../../../graphql/types'; -import { inputsModel, inputsSelectors, State } from '../../../common/store'; -import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; +import { inputsModel, State } from '../../../common/store'; +import { useKibana } from '../../../common/lib/kibana'; +import { createFilter } from '../../../common/containers/helpers'; +import { NetworkHttpEdges, PageInfoPaginated } from '../../../graphql/types'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; -import { - QueryTemplatePaginated, - QueryTemplatePaginatedProps, -} from '../../../common/containers/query_template_paginated'; import { networkModel, networkSelectors } from '../../store'; -import { networkHttpQuery } from './index.gql_query'; +import { + NetworkQueries, + NetworkHttpRequestOptions, + NetworkHttpStrategyResponse, + SortField, +} from '../../../../common/search_strategy/security_solution'; +import { AbortError } from '../../../../../../../src/plugins/data/common'; +import * as i18n from './translations'; const ID = 'networkHttpQuery'; @@ -35,7 +33,6 @@ export interface NetworkHttpArgs { ip?: string; inspect: inputsModel.InspectQuery; isInspected: boolean; - loading: boolean; loadPage: (newActivePage: number) => void; networkHttp: NetworkHttpEdges[]; pageInfo: PageInfoPaginated; @@ -43,118 +40,161 @@ export interface NetworkHttpArgs { totalCount: number; } -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: NetworkHttpArgs) => React.ReactNode; +interface UseNetworkHttp { + id?: string; ip?: string; type: networkModel.NetworkType; + filterQuery?: ESTermQuery | string; + endDate: string; + startDate: string; + skip: boolean; } -export interface NetworkHttpComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sort: NetworkHttpSortField; -} +export const useNetworkHttp = ({ + endDate, + filterQuery, + id = ID, + ip, + skip, + startDate, + type, +}: UseNetworkHttp): [boolean, NetworkHttpArgs] => { + const getHttpSelector = networkSelectors.httpSelector(); + const { activePage, limit, sort } = useSelector( + (state: State) => getHttpSelector(state, type), + shallowEqual + ); + const { data, notifications, uiSettings } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); + + const [networkHttpRequest, setHostRequest] = useState({ + defaultIndex, + factoryQueryType: NetworkQueries.http, + filterQuery: createFilter(filterQuery), + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort: sort as SortField, + timerange: { + interval: '12h', + from: startDate ? startDate : '', + to: endDate ? endDate : new Date(Date.now()).toISOString(), + }, + }); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setHostRequest((prevRequest) => { + return { + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + }; + }); + }, + [limit] + ); -type NetworkHttpProps = OwnProps & NetworkHttpComponentReduxProps & WithKibanaProps; + const [networkHttpResponse, setNetworkHttpResponse] = useState({ + networkHttp: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + loadPage: wrappedLoadMore, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + refetch: refetch.current, + totalCount: -1, + }); -class NetworkHttpComponentQuery extends QueryTemplatePaginated< - NetworkHttpProps, - GetNetworkHttpQuery.Query, - GetNetworkHttpQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - id = ID, - ip, - isInspected, - kibana, - limit, - skip, - sourceId, - sort, - startDate, - } = this.props; - const variables: GetNetworkHttpQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={networkHttpQuery} - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const networkHttp = getOr([], `source.NetworkHttp.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), + const networkHttpSearch = useCallback( + (request: NetworkHttpRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + signal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setNetworkHttpResponse((prevResponse) => ({ + ...prevResponse, + networkHttp: response.edges, + inspect: response.inspect ?? prevResponse.inspect, + pageInfo: response.pageInfo, + refetch: refetch.current, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_NETWORK_HTTP); + searchSubscription$.unsubscribe(); + } }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: i18n.FAIL_NETWORK_HTTP, + text: msg.message, + }); } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - NetworkHttp: { - ...fetchMoreResult.source.NetworkHttp, - edges: [...fetchMoreResult.source.NetworkHttp.edges], - }, - }, - }; }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.NetworkHttp.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - networkHttp, - pageInfo: getOr({}, 'source.NetworkHttp.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.NetworkHttp.totalCount', data), }); - }} - - ); - } -} + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); -const makeMapStateToProps = () => { - const getHttpSelector = networkSelectors.httpSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - return (state: State, { id = ID, type }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getHttpSelector(state, type), - isInspected, - }; - }; -}; + useEffect(() => { + setHostRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + sort: sort as SortField, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }; + if (!skip && !deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [activePage, defaultIndex, endDate, filterQuery, limit, startDate, sort, skip]); + + useEffect(() => { + networkHttpSearch(networkHttpRequest); + }, [networkHttpRequest, networkHttpSearch]); -export const NetworkHttpQuery = compose>( - connect(makeMapStateToProps), - withKibana -)(NetworkHttpComponentQuery); + return [loading, networkHttpResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/network/containers/network_http/translations.ts b/x-pack/plugins/security_solution/public/network/containers/network_http/translations.ts new file mode 100644 index 00000000000000..7909a5e48b8c4e --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/network_http/translations.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_NETWORK_HTTP = i18n.translate( + 'xpack.securitySolution.networkHttp.errorSearchDescription', + { + defaultMessage: `An error has occurred on network http search`, + } +); + +export const FAIL_NETWORK_HTTP = i18n.translate( + 'xpack.securitySolution.networkHttp.failSearchDescription', + { + defaultMessage: `Failed to run search on network http`, + } +); diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/network_http_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/network_http_query_table.tsx index 551de698cfa08d..1b1b2b5f4f46e2 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/network_http_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/network_http_query_table.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { getOr } from 'lodash/fp'; import { manageQuery } from '../../../common/components/page/manage_query'; import { OwnProps } from './types'; -import { NetworkHttpQuery } from '../../containers/network_http'; +import { useNetworkHttp } from '../../containers/network_http'; import { NetworkHttpTable } from '../../components/network_http_table'; const NetworkHttpTableManage = manageQuery(NetworkHttpTable); @@ -21,43 +21,35 @@ export const NetworkHttpQueryTable = ({ skip, startDate, type, -}: OwnProps) => ( - - {({ - id, - inspect, - isInspected, - loading, - loadPage, - networkHttp, - pageInfo, - refetch, - totalCount, - }) => ( - - )} - -); +}: OwnProps) => { + const [ + loading, + { id, inspect, isInspected, loadPage, networkHttp, pageInfo, refetch, totalCount }, + ] = useNetworkHttp({ + endDate, + filterQuery, + ip, + skip, + startDate, + type, + }); + + return ( + + ); +}; NetworkHttpQueryTable.displayName = 'NetworkHttpQueryTable'; diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx index 7e0c4025d6cac1..3caff05734c1e4 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { getOr } from 'lodash/fp'; import { NetworkHttpTable } from '../../components/network_http_table'; -import { NetworkHttpQuery } from '../../containers/network_http'; +import { useNetworkHttp } from '../../containers/network_http'; import { networkModel } from '../../store'; import { manageQuery } from '../../../common/components/page/manage_query'; @@ -22,42 +22,34 @@ export const HttpQueryTabBody = ({ skip, startDate, setQuery, -}: HttpQueryTabBodyProps) => ( - - {({ - id, - inspect, - isInspected, - loading, - loadPage, - networkHttp, - pageInfo, - refetch, - totalCount, - }) => ( - - )} - -); +}: HttpQueryTabBodyProps) => { + const [ + loading, + { id, inspect, isInspected, loadPage, networkHttp, pageInfo, refetch, totalCount }, + ] = useNetworkHttp({ + endDate, + filterQuery, + skip, + startDate, + type: networkModel.NetworkType.page, + }); + + return ( + + ); +}; HttpQueryTabBody.displayName = 'HttpQueryTabBody'; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/helpers.ts new file mode 100644 index 00000000000000..b8a28441337c7b --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/helpers.ts @@ -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 { get, getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { + NetworkHttpBuckets, + NetworkHttpEdges, +} from '../../../../../../common/search_strategy/security_solution/network'; + +export const getHttpEdges = (response: IEsSearchResponse): NetworkHttpEdges[] => + formatHttpEdges(getOr([], `aggregations.url.buckets`, response.rawResponse)); + +const formatHttpEdges = (buckets: NetworkHttpBuckets[]): NetworkHttpEdges[] => + buckets.map((bucket: NetworkHttpBuckets) => ({ + node: { + _id: bucket.key, + domains: bucket.domains.buckets.map(({ key }) => key), + methods: bucket.methods.buckets.map(({ key }) => key), + statuses: bucket.status.buckets.map(({ key }) => `${key}`), + lastHost: get('source.hits.hits[0]._source.host.name', bucket), + lastSourceIp: get('source.hits.hits[0]._source.source.ip', bucket), + path: bucket.key, + requestCount: bucket.doc_count, + }, + cursor: { + value: bucket.key, + tiebreaker: null, + }, + })); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/index.ts new file mode 100644 index 00000000000000..b6c26cd533de23 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/index.ts @@ -0,0 +1,59 @@ +/* + * 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 { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { + NetworkHttpStrategyResponse, + NetworkQueries, + NetworkHttpRequestOptions, + NetworkHttpEdges, +} from '../../../../../../common/search_strategy/security_solution/network'; + +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../types'; + +import { getHttpEdges } from './helpers'; +import { buildHttpQuery } from './query.http_network.dsl'; + +export const networkHttp: SecuritySolutionFactory = { + buildDsl: (options: NetworkHttpRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildHttpQuery(options); + }, + parse: async ( + options: NetworkHttpRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.http_count.value', response.rawResponse); + const networkHttpEdges: NetworkHttpEdges[] = getHttpEdges(response); + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + const edges = networkHttpEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildHttpQuery(options))], + response: [inspectStringifyObject(response)], + }; + const showMorePagesIndicator = totalCount > fakeTotalCount; + + return { + ...response, + edges, + inspect, + pageInfo: { + activePage: activePage ? activePage : 0, + fakeTotalCount, + showMorePagesIndicator, + }, + totalCount, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/query.http_network.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/query.http_network.dsl.ts new file mode 100644 index 00000000000000..31d695d6a05910 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/query.http_network.dsl.ts @@ -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 { createQueryFilterClauses } from '../../../../../utils/build_query'; + +import { + NetworkHttpRequestOptions, + SortField, +} from '../../../../../../common/search_strategy/security_solution'; + +const getCountAgg = () => ({ + http_count: { + cardinality: { + field: 'url.path', + }, + }, +}); + +export const buildHttpQuery = ({ + defaultIndex, + filterQuery, + sort, + pagination: { querySize }, + timerange: { from, to }, + ip, +}: NetworkHttpRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { gte: from, lte: to, format: 'strict_date_optional_time' }, + }, + }, + { exists: { field: 'http.request.method' } }, + ]; + + const dslQuery = { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + body: { + aggregations: { + ...getCountAgg(), + ...getHttpAggs(sort, querySize), + }, + query: { + bool: ip + ? { + filter, + should: [ + { + term: { + 'source.ip': ip, + }, + }, + { + term: { + 'destination.ip': ip, + }, + }, + ], + minimum_should_match: 1, + } + : { + filter, + }, + }, + }, + size: 0, + track_total_hits: false, + }; + return dslQuery; +}; + +const getHttpAggs = (sortField: SortField, querySize: number) => ({ + url: { + terms: { + field: `url.path`, + size: querySize, + order: { + _count: sortField.direction, + }, + }, + aggs: { + methods: { + terms: { + field: 'http.request.method', + size: 4, + }, + }, + domains: { + terms: { + field: 'url.domain', + size: 4, + }, + }, + status: { + terms: { + field: 'http.response.status_code', + size: 4, + }, + }, + source: { + top_hits: { + size: 1, + _source: { + includes: ['host.name', 'source.ip'], + }, + }, + }, + }, + }, +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts index 2c21d9741d648f..7d40b034c66bb0 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts @@ -8,8 +8,10 @@ import { FactoryQueryTypes } from '../../../../../common/search_strategy/securit import { NetworkQueries } from '../../../../../common/search_strategy/security_solution/network'; import { SecuritySolutionFactory } from '../types'; +import { networkHttp } from './http'; import { networkTls } from './tls'; export const networkFactory: Record> = { + [NetworkQueries.http]: networkHttp, [NetworkQueries.tls]: networkTls, }; From a656b96e25be33c17620d99425777f7ff4a4ad1f Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Wed, 2 Sep 2020 06:22:20 -0400 Subject: [PATCH 171/216] [Ingest Manager] support multiple kibana urls (#75712) * let the user specify multiple kibana urls * add validation to kibana urls so paths and protocols cannot differ * update i18n message * only send the first url to the instructions * udpate all agent configs' revision when settings is updated * fix jest test * update endpoint full agent policy test * fix type * dont add settings if standalone mode * fix ui not handling errors from /{agentPolicyId}/full endpoint * fix formatted message id * only return needed fields * fill in updated_by and updated_at attributes of the ingest-agent-policies when revision is bumped * throw error if kibana_urls not set and update tests * change ingest_manager_settings SO attribute kibana_url: string to kibana_urls: string[] and add migration * leave instructions single kibana url * make kibana_url and other attributes created during setup required, fix types --- .../ingest_manager/common/services/index.ts | 1 + .../services/is_diff_path_protocol.test.ts | 39 ++++++++++++++ .../common/services/is_diff_path_protocol.ts | 24 +++++++++ .../ingest_manager/common/types/index.ts | 2 +- .../common/types/models/agent_policy.ts | 5 ++ .../common/types/models/settings.ts | 8 +-- .../components/settings_flyout.tsx | 37 ++++++++----- .../components/agent_policy_yaml_flyout.tsx | 32 +++++++---- .../managed_instructions.tsx | 7 ++- x-pack/plugins/ingest_manager/server/index.ts | 7 ++- .../server/routes/install_script/index.ts | 8 +-- .../server/routes/settings/index.ts | 6 ++- .../server/saved_objects/index.ts | 6 ++- .../saved_objects/migrations/to_v7_10_0.ts | 22 +++++++- .../server/services/agent_policy.test.ts | 35 +++++++++++- .../server/services/agent_policy.ts | 39 +++++++++++++- .../server/services/settings.ts | 44 ++++++++++++--- .../ingest_manager/server/services/setup.ts | 26 ++------- .../server/types/rest_spec/settings.ts | 11 +++- .../apis/index.js | 3 ++ .../apis/settings/index.js | 11 ++++ .../apis/settings/update.ts | 53 +++++++++++++++++++ .../apps/endpoint/policy_details.ts | 15 ++++++ 23 files changed, 371 insertions(+), 70 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/common/services/is_diff_path_protocol.test.ts create mode 100644 x-pack/plugins/ingest_manager/common/services/is_diff_path_protocol.ts create mode 100644 x-pack/test/ingest_manager_api_integration/apis/settings/index.js create mode 100644 x-pack/test/ingest_manager_api_integration/apis/settings/update.ts diff --git a/x-pack/plugins/ingest_manager/common/services/index.ts b/x-pack/plugins/ingest_manager/common/services/index.ts index ad739bf9ff844d..46a1c65872d1b6 100644 --- a/x-pack/plugins/ingest_manager/common/services/index.ts +++ b/x-pack/plugins/ingest_manager/common/services/index.ts @@ -11,3 +11,4 @@ export { fullAgentPolicyToYaml } from './full_agent_policy_to_yaml'; export { isPackageLimited, doesAgentPolicyAlreadyIncludePackage } from './limited_package'; export { decodeCloudId } from './decode_cloud_id'; export { isValidNamespace } from './is_valid_namespace'; +export { isDiffPathProtocol } from './is_diff_path_protocol'; diff --git a/x-pack/plugins/ingest_manager/common/services/is_diff_path_protocol.test.ts b/x-pack/plugins/ingest_manager/common/services/is_diff_path_protocol.test.ts new file mode 100644 index 00000000000000..c488d552d7676a --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/services/is_diff_path_protocol.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { isDiffPathProtocol } from './is_diff_path_protocol'; + +describe('Ingest Manager - isDiffPathProtocol', () => { + it('returns true for different paths', () => { + expect( + isDiffPathProtocol([ + 'http://localhost:8888/abc', + 'http://localhost:8888/abc', + 'http://localhost:8888/efg', + ]) + ).toBe(true); + }); + it('returns true for different protocols', () => { + expect( + isDiffPathProtocol([ + 'http://localhost:8888/abc', + 'https://localhost:8888/abc', + 'http://localhost:8888/abc', + ]) + ).toBe(true); + }); + it('returns false for same paths and protocols and different host or port', () => { + expect( + isDiffPathProtocol([ + 'http://localhost:8888/abc', + 'http://localhost2:8888/abc', + 'http://localhost:8883/abc', + ]) + ).toBe(false); + }); + it('returns false for one url', () => { + expect(isDiffPathProtocol(['http://localhost:8888/abc'])).toBe(false); + }); +}); diff --git a/x-pack/plugins/ingest_manager/common/services/is_diff_path_protocol.ts b/x-pack/plugins/ingest_manager/common/services/is_diff_path_protocol.ts new file mode 100644 index 00000000000000..666e886d745b15 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/services/is_diff_path_protocol.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +/* + * 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. + */ + +// validates an array of urls have the same path and protocol +export function isDiffPathProtocol(kibanaUrls: string[]) { + const urlCompare = new URL(kibanaUrls[0]); + const compareProtocol = urlCompare.protocol; + const comparePathname = urlCompare.pathname; + return kibanaUrls.some((v) => { + const url = new URL(v); + const protocol = url.protocol; + const pathname = url.pathname; + return compareProtocol !== protocol || comparePathname !== pathname; + }); +} diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts index cafd0f03f66e29..d62f4fbb023dc0 100644 --- a/x-pack/plugins/ingest_manager/common/types/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/index.ts @@ -15,7 +15,7 @@ export interface IngestManagerConfigType { pollingRequestTimeout: number; maxConcurrentConnections: number; kibana: { - host?: string; + host?: string[] | string; ca_sha256?: string; }; elasticsearch: { diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent_policy.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_policy.ts index c626c85d3fb24f..263e10e9d34b14 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent_policy.ts @@ -60,6 +60,11 @@ export interface FullAgentPolicy { [key: string]: any; }; }; + fleet?: { + kibana: { + hosts: string[]; + }; + }; inputs: FullAgentPolicyInput[]; revision?: number; agent?: { diff --git a/x-pack/plugins/ingest_manager/common/types/models/settings.ts b/x-pack/plugins/ingest_manager/common/types/models/settings.ts index 98d99911f1b3fc..f554f4b392ad6e 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/settings.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/settings.ts @@ -5,10 +5,10 @@ */ import { SavedObjectAttributes } from 'src/core/public'; -interface BaseSettings { - agent_auto_upgrade?: boolean; - package_auto_upgrade?: boolean; - kibana_url?: string; +export interface BaseSettings { + agent_auto_upgrade: boolean; + package_auto_upgrade: boolean; + kibana_urls: string[]; kibana_ca_sha256?: string; has_seen_add_data_notice?: boolean; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx index 9a9557f77c40c3..e0d843ad773b86 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx @@ -18,14 +18,14 @@ import { EuiFlyoutFooter, EuiForm, EuiFormRow, - EuiFieldText, EuiRadioGroup, EuiComboBox, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText } from '@elastic/eui'; -import { useInput, useComboInput, useCore, useGetSettings, sendPutSettings } from '../hooks'; +import { useComboInput, useCore, useGetSettings, sendPutSettings } from '../hooks'; import { useGetOutputs, sendPutOutput } from '../hooks/use_request/outputs'; +import { isDiffPathProtocol } from '../../../../common/'; const URL_REGEX = /^(https?):\/\/[^\s$.?#].[^\s]*$/gm; @@ -36,14 +36,28 @@ interface Props { function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { const [isLoading, setIsloading] = React.useState(false); const { notifications } = useCore(); - const kibanaUrlInput = useInput('', (value) => { - if (!value.match(URL_REGEX)) { + const kibanaUrlsInput = useComboInput([], (value) => { + if (value.length === 0) { + return [ + i18n.translate('xpack.ingestManager.settings.kibanaUrlEmptyError', { + defaultMessage: 'At least one URL is required', + }), + ]; + } + if (value.some((v) => !v.match(URL_REGEX))) { return [ i18n.translate('xpack.ingestManager.settings.kibanaUrlError', { defaultMessage: 'Invalid URL', }), ]; } + if (isDiffPathProtocol(value)) { + return [ + i18n.translate('xpack.ingestManager.settings.kibanaUrlDifferentPathOrProtocolError', { + defaultMessage: 'Protocol and path must be the same for each URL', + }), + ]; + } }); const elasticsearchUrlInput = useComboInput([], (value) => { if (value.some((v) => !v.match(URL_REGEX))) { @@ -58,7 +72,7 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { return { isLoading, onSubmit: async () => { - if (!kibanaUrlInput.validate() || !elasticsearchUrlInput.validate()) { + if (!kibanaUrlsInput.validate() || !elasticsearchUrlInput.validate()) { return; } @@ -74,7 +88,7 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { throw outputResponse.error; } const settingsResponse = await sendPutSettings({ - kibana_url: kibanaUrlInput.value, + kibana_urls: kibanaUrlsInput.value, }); if (settingsResponse.error) { throw settingsResponse.error; @@ -94,14 +108,13 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { } }, inputs: { - kibanaUrl: kibanaUrlInput, + kibanaUrls: kibanaUrlsInput, elasticsearchUrl: elasticsearchUrlInput, }, }; } export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { - const core = useCore(); const settingsRequest = useGetSettings(); const settings = settingsRequest?.data?.item; const outputsRequest = useGetOutputs(); @@ -117,9 +130,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { useEffect(() => { if (settings) { - inputs.kibanaUrl.setValue( - settings.kibana_url || `${window.location.origin}${core.http.basePath.get()}` - ); + inputs.kibanaUrls.setValue(settings.kibana_urls); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [settings]); @@ -220,9 +231,9 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { label={i18n.translate('xpack.ingestManager.settings.kibanaUrlLabel', { defaultMessage: 'Kibana URL', })} - {...inputs.kibanaUrl.formRowProps} + {...inputs.kibanaUrls.formRowProps} > - + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_yaml_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_yaml_flyout.tsx index 919bb49f69aaee..5d485a6e210865 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_yaml_flyout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_yaml_flyout.tsx @@ -18,6 +18,7 @@ import { EuiFlyoutFooter, EuiButtonEmpty, EuiButton, + EuiCallOut, } from '@elastic/eui'; import { useGetOneAgentPolicyFull, useGetOneAgentPolicy, useCore } from '../../../hooks'; import { Loading } from '../../../components'; @@ -32,17 +33,28 @@ const FlyoutBody = styled(EuiFlyoutBody)` export const AgentPolicyYamlFlyout = memo<{ policyId: string; onClose: () => void }>( ({ policyId, onClose }) => { const core = useCore(); - const { isLoading: isLoadingYaml, data: yamlData } = useGetOneAgentPolicyFull(policyId); + const { isLoading: isLoadingYaml, data: yamlData, error } = useGetOneAgentPolicyFull(policyId); const { data: agentPolicyData } = useGetOneAgentPolicy(policyId); - - const body = - isLoadingYaml && !yamlData ? ( - - ) : ( - - {fullAgentPolicyToYaml(yamlData!.item)} - - ); + const body = isLoadingYaml ? ( + + ) : error ? ( + + } + color="danger" + iconType="alert" + > + {error.message} + + ) : ( + + {fullAgentPolicyToYaml(yamlData!.item)} + + ); const downloadLink = core.http.basePath.prepend( agentPolicyRouteService.getInfoFullDownloadPath(policyId) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx index b02893057c9c33..9307229cdc2582 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx @@ -34,8 +34,11 @@ export const ManagedInstructions: React.FunctionComponent = ({ agentPolic const settings = useGetSettings(); const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId); - const kibanaUrl = - settings.data?.item?.kibana_url ?? `${window.location.origin}${core.http.basePath.get()}`; + const kibanaUrlsSettings = settings.data?.item?.kibana_urls; + const kibanaUrl = kibanaUrlsSettings + ? kibanaUrlsSettings[0] + : `${window.location.origin}${core.http.basePath.get()}`; + const kibanaCASha256 = settings.data?.item?.kibana_ca_sha256; const steps: EuiContainedStepProps[] = [ diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 962cddb2e411e6..f7b923aebb48bd 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -32,7 +32,12 @@ export const config = { pollingRequestTimeout: schema.number({ defaultValue: 60000 }), maxConcurrentConnections: schema.number({ defaultValue: 0 }), kibana: schema.object({ - host: schema.maybe(schema.string()), + host: schema.maybe( + schema.oneOf([ + schema.uri({ scheme: ['http', 'https'] }), + schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }), { minSize: 1 }), + ]) + ), ca_sha256: schema.maybe(schema.string()), }), elasticsearch: schema.object({ diff --git a/x-pack/plugins/ingest_manager/server/routes/install_script/index.ts b/x-pack/plugins/ingest_manager/server/routes/install_script/index.ts index c2a5d77a39eb16..c767d3e80d2b7d 100644 --- a/x-pack/plugins/ingest_manager/server/routes/install_script/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/install_script/index.ts @@ -38,16 +38,16 @@ export const registerRoutes = ({ const http = appContextService.getHttpSetup(); const serverInfo = http.getServerInfo(); const basePath = http.basePath; - const kibanaUrl = - (await settingsService.getSettings(soClient)).kibana_url || + const kibanaUrls = (await settingsService.getSettings(soClient)).kibana_urls || [ url.format({ protocol: serverInfo.protocol, hostname: serverInfo.hostname, port: serverInfo.port, pathname: basePath.serverBasePath, - }); + }), + ]; - const script = getScript(request.params.osType, kibanaUrl); + const script = getScript(request.params.osType, kibanaUrls[0]); return response.ok({ body: script }); } diff --git a/x-pack/plugins/ingest_manager/server/routes/settings/index.ts b/x-pack/plugins/ingest_manager/server/routes/settings/index.ts index 56e666056e8d0a..aabb85dadabc20 100644 --- a/x-pack/plugins/ingest_manager/server/routes/settings/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/settings/index.ts @@ -8,7 +8,7 @@ import { TypeOf } from '@kbn/config-schema'; import { PLUGIN_ID, SETTINGS_API_ROUTES } from '../../constants'; import { PutSettingsRequestSchema, GetSettingsRequestSchema } from '../../types'; -import { settingsService } from '../../services'; +import { settingsService, agentPolicyService, appContextService } from '../../services'; export const getSettingsHandler: RequestHandler = async (context, request, response) => { const soClient = context.core.savedObjects.client; @@ -40,8 +40,12 @@ export const putSettingsHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const user = await appContextService.getSecurity()?.authc.getCurrentUser(request); try { const settings = await settingsService.saveSettings(soClient, request.body); + await agentPolicyService.bumpAllAgentPolicies(soClient, { + user: user || undefined, + }); const body = { success: true, item: settings, diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts index 1bbe3b71bf9191..aff8e607622d47 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts @@ -23,6 +23,7 @@ import { migrateAgentPolicyToV7100, migrateEnrollmentApiKeysToV7100, migratePackagePolicyToV7100, + migrateSettingsToV7100, } from './migrations/to_v7_10_0'; /* @@ -43,11 +44,14 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { properties: { agent_auto_upgrade: { type: 'keyword' }, package_auto_upgrade: { type: 'keyword' }, - kibana_url: { type: 'keyword' }, + kibana_urls: { type: 'keyword' }, kibana_ca_sha256: { type: 'keyword' }, has_seen_add_data_notice: { type: 'boolean', index: false }, }, }, + migrations: { + '7.10.0': migrateSettingsToV7100, + }, }, [AGENT_SAVED_OBJECT_TYPE]: { name: AGENT_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/migrations/to_v7_10_0.ts b/x-pack/plugins/ingest_manager/server/saved_objects/migrations/to_v7_10_0.ts index b60903dbd2bd06..5e36ce46c099be 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects/migrations/to_v7_10_0.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects/migrations/to_v7_10_0.ts @@ -5,7 +5,14 @@ */ import { SavedObjectMigrationFn } from 'kibana/server'; -import { Agent, AgentEvent, AgentPolicy, PackagePolicy, EnrollmentAPIKey } from '../../types'; +import { + Agent, + AgentEvent, + AgentPolicy, + PackagePolicy, + EnrollmentAPIKey, + Settings, +} from '../../types'; export const migrateAgentToV7100: SavedObjectMigrationFn< Exclude & { @@ -72,3 +79,16 @@ export const migratePackagePolicyToV7100: SavedObjectMigrationFn< return packagePolicyDoc; }; + +export const migrateSettingsToV7100: SavedObjectMigrationFn< + Exclude & { + kibana_url: string; + }, + Settings +> = (settingsDoc) => { + settingsDoc.attributes.kibana_urls = [settingsDoc.attributes.kibana_url]; + // @ts-expect-error + delete settingsDoc.attributes.kibana_url; + + return settingsDoc; +}; diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy.test.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy.test.ts index dc2a89c661ac3d..d9dffa03b2290f 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_policy.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_policy.test.ts @@ -19,6 +19,24 @@ function getSavedObjectMock(agentPolicyAttributes: any) { attributes: agentPolicyAttributes, }; }); + mock.find.mockImplementation(async (options) => { + return { + saved_objects: [ + { + id: '93f74c0-e876-11ea-b7d3-8b2acec6f75c', + attributes: { + kibana_urls: ['http://localhost:5603'], + }, + type: 'ingest_manager_settings', + score: 1, + references: [], + }, + ], + total: 1, + page: 1, + per_page: 1, + }; + }); return mock; } @@ -43,7 +61,7 @@ jest.mock('./output', () => { describe('agent policy', () => { describe('getFullAgentPolicy', () => { - it('should return a policy without monitoring if not monitoring is not enabled', async () => { + it('should return a policy without monitoring if monitoring is not enabled', async () => { const soClient = getSavedObjectMock({ revision: 1, }); @@ -61,6 +79,11 @@ describe('agent policy', () => { }, inputs: [], revision: 1, + fleet: { + kibana: { + hosts: ['http://localhost:5603'], + }, + }, agent: { monitoring: { enabled: false, @@ -90,6 +113,11 @@ describe('agent policy', () => { }, inputs: [], revision: 1, + fleet: { + kibana: { + hosts: ['http://localhost:5603'], + }, + }, agent: { monitoring: { use_output: 'default', @@ -120,6 +148,11 @@ describe('agent policy', () => { }, inputs: [], revision: 1, + fleet: { + kibana: { + hosts: ['http://localhost:5603'], + }, + }, agent: { monitoring: { use_output: 'default', diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts index 21bc7b021e83a2..2c97bba0cac459 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { uniq } from 'lodash'; -import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract, SavedObjectsBulkUpdateResponse } from 'src/core/server'; import { AuthenticatedUser } from '../../../security/server'; import { DEFAULT_AGENT_POLICY, @@ -25,6 +25,7 @@ import { listAgents } from './agents'; import { packagePolicyService } from './package_policy'; import { outputService } from './output'; import { agentPolicyUpdateEventHandler } from './agent_policy_update'; +import { getSettings } from './settings'; const SAVED_OBJECT_TYPE = AGENT_POLICY_SAVED_OBJECT_TYPE; @@ -260,6 +261,25 @@ class AgentPolicyService { ): Promise { return this._update(soClient, id, {}, options?.user); } + public async bumpAllAgentPolicies( + soClient: SavedObjectsClientContract, + options?: { user?: AuthenticatedUser } + ): Promise>> { + const currentPolicies = await soClient.find({ + type: SAVED_OBJECT_TYPE, + fields: ['revision'], + }); + const bumpedPolicies = currentPolicies.saved_objects.map((policy) => { + policy.attributes = { + ...policy.attributes, + revision: policy.attributes.revision + 1, + updated_at: new Date().toISOString(), + updated_by: options?.user ? options.user.username : 'system', + }; + return policy; + }); + return soClient.bulkUpdate(bumpedPolicies); + } public async assignPackagePolicies( soClient: SavedObjectsClientContract, @@ -370,6 +390,7 @@ class AgentPolicyService { options?: { standalone: boolean } ): Promise { let agentPolicy; + const standalone = options?.standalone; try { agentPolicy = await this.get(soClient, id); @@ -435,6 +456,22 @@ class AgentPolicyService { }), }; + // only add settings if not in standalone + if (!standalone) { + let settings; + try { + settings = await getSettings(soClient); + } catch (error) { + throw new Error('Default settings is not setup'); + } + if (!settings.kibana_urls) throw new Error('kibana_urls is missing'); + fullAgentPolicy.fleet = { + kibana: { + hosts: settings.kibana_urls, + }, + }; + } + return fullAgentPolicy; } } diff --git a/x-pack/plugins/ingest_manager/server/services/settings.ts b/x-pack/plugins/ingest_manager/server/services/settings.ts index f1c09746d9abd1..25223fbc08535a 100644 --- a/x-pack/plugins/ingest_manager/server/services/settings.ts +++ b/x-pack/plugins/ingest_manager/server/services/settings.ts @@ -5,7 +5,15 @@ */ import Boom from 'boom'; import { SavedObjectsClientContract } from 'kibana/server'; -import { GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, SettingsSOAttributes, Settings } from '../../common'; +import url from 'url'; +import { + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + SettingsSOAttributes, + Settings, + decodeCloudId, + BaseSettings, +} from '../../common'; +import { appContextService } from './app_context'; export async function getSettings(soClient: SavedObjectsClientContract): Promise { const res = await soClient.find({ @@ -25,7 +33,7 @@ export async function getSettings(soClient: SavedObjectsClientContract): Promise export async function saveSettings( soClient: SavedObjectsClientContract, newData: Partial> -): Promise { +): Promise & Pick> { try { const settings = await getSettings(soClient); @@ -41,10 +49,11 @@ export async function saveSettings( }; } catch (e) { if (e.isBoom && e.output.statusCode === 404) { - const res = await soClient.create( - GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, - newData - ); + const defaultSettings = createDefaultSettings(); + const res = await soClient.create(GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, { + ...defaultSettings, + ...newData, + }); return { id: res.id, @@ -55,3 +64,26 @@ export async function saveSettings( throw e; } } + +export function createDefaultSettings(): BaseSettings { + const http = appContextService.getHttpSetup(); + const serverInfo = http.getServerInfo(); + const basePath = http.basePath; + + const cloud = appContextService.getCloud(); + const cloudId = cloud?.isCloudEnabled && cloud.cloudId; + const cloudUrl = cloudId && decodeCloudId(cloudId)?.kibanaUrl; + const flagsUrl = appContextService.getConfig()?.fleet?.kibana?.host; + const defaultUrl = url.format({ + protocol: serverInfo.protocol, + hostname: serverInfo.hostname, + port: serverInfo.port, + pathname: basePath.serverBasePath, + }); + + return { + agent_auto_upgrade: true, + package_auto_upgrade: true, + kibana_urls: [cloudUrl || flagsUrl || defaultUrl].flat(), + }; +} diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index fd5d94a71d672c..ec3a05a4fa390d 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import url from 'url'; import uuid from 'uuid'; import { SavedObjectsClientContract } from 'src/core/server'; import { CallESAsCurrentUser } from '../types'; @@ -22,14 +21,13 @@ import { Installation, Output, DEFAULT_AGENT_POLICIES_PACKAGES, - decodeCloudId, } from '../../common'; import { getPackageInfo } from './epm/packages'; import { packagePolicyService } from './package_policy'; import { generateEnrollmentAPIKey } from './api_keys'; import { settingsService } from '.'; -import { appContextService } from './app_context'; import { awaitIfPending } from './setup_utils'; +import { createDefaultSettings } from './settings'; const FLEET_ENROLL_USERNAME = 'fleet_enroll'; const FLEET_ENROLL_ROLE = 'fleet_enroll'; @@ -58,26 +56,8 @@ async function createSetupSideEffects( ensureDefaultIndices(callCluster), settingsService.getSettings(soClient).catch((e: any) => { if (e.isBoom && e.output.statusCode === 404) { - const http = appContextService.getHttpSetup(); - const serverInfo = http.getServerInfo(); - const basePath = http.basePath; - - const cloud = appContextService.getCloud(); - const cloudId = cloud?.isCloudEnabled && cloud.cloudId; - const cloudUrl = cloudId && decodeCloudId(cloudId)?.kibanaUrl; - const flagsUrl = appContextService.getConfig()?.fleet?.kibana?.host; - const defaultUrl = url.format({ - protocol: serverInfo.protocol, - hostname: serverInfo.hostname, - port: serverInfo.port, - pathname: basePath.serverBasePath, - }); - - return settingsService.saveSettings(soClient, { - agent_auto_upgrade: true, - package_auto_upgrade: true, - kibana_url: cloudUrl || flagsUrl || defaultUrl, - }); + const defaultSettings = createDefaultSettings(); + return settingsService.saveSettings(soClient, defaultSettings); } return Promise.reject(e); diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/settings.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/settings.ts index baee9f79d9317d..35718491c92247 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/settings.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/settings.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { schema } from '@kbn/config-schema'; +import { isDiffPathProtocol } from '../../../common'; export const GetSettingsRequestSchema = {}; @@ -11,7 +12,15 @@ export const PutSettingsRequestSchema = { body: schema.object({ agent_auto_upgrade: schema.maybe(schema.boolean()), package_auto_upgrade: schema.maybe(schema.boolean()), - kibana_url: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), + kibana_urls: schema.maybe( + schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }), { + validate: (value) => { + if (isDiffPathProtocol(value)) { + return 'Protocol and path must be the same for each URL'; + } + }, + }) + ), kibana_ca_sha256: schema.maybe(schema.string()), has_seen_add_data_notice: schema.maybe(schema.boolean()), }), diff --git a/x-pack/test/ingest_manager_api_integration/apis/index.js b/x-pack/test/ingest_manager_api_integration/apis/index.js index fac8a26fd6aec4..7c1ebef337baa2 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/index.js @@ -22,5 +22,8 @@ export default function ({ loadTestFile }) { // Agent policies loadTestFile(require.resolve('./agent_policy/index')); + + // Settings + loadTestFile(require.resolve('./settings/index')); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/settings/index.js b/x-pack/test/ingest_manager_api_integration/apis/settings/index.js new file mode 100644 index 00000000000000..99346fcabeff4f --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/settings/index.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function loadTests({ loadTestFile }) { + describe('Settings Endpoints', () => { + loadTestFile(require.resolve('./update')); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/settings/update.ts b/x-pack/test/ingest_manager_api_integration/apis/settings/update.ts new file mode 100644 index 00000000000000..86292b535db2d4 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/settings/update.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + + describe('Settings - update', async function () { + skipIfNoDockerRegistry(providerContext); + + it("should bump all agent policy's revision", async function () { + const { body: testPolicy1PostRes } = await supertest + .post(`/api/ingest_manager/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'test', + description: '', + namespace: 'default', + }); + const { body: testPolicy2PostRes } = await supertest + .post(`/api/ingest_manager/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'test2', + description: '', + namespace: 'default', + }); + await supertest + .put(`/api/ingest_manager/settings`) + .set('kbn-xsrf', 'xxxx') + .send({ kibana_urls: ['http://localhost:1232/abc', 'http://localhost:1232/abc'] }); + + const getTestPolicy1Res = await kibanaServer.savedObjects.get({ + type: 'ingest-agent-policies', + id: testPolicy1PostRes.item.id, + }); + const getTestPolicy2Res = await kibanaServer.savedObjects.get({ + type: 'ingest-agent-policies', + id: testPolicy2PostRes.item.id, + }); + expect(getTestPolicy1Res.attributes.revision).equal(2); + expect(getTestPolicy2Res.attributes.revision).equal(2); + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index a0998f1a838baf..9a3489e9309bfb 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import Url from 'url'; import { FtrProviderContext } from '../../ftr_provider_context'; import { PolicyTestResourceInfo } from '../../services/endpoint_policy'; @@ -18,6 +19,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ]); const testSubjects = getService('testSubjects'); const policyTestResources = getService('policyTestResources'); + const config = getService('config'); + const kbnTestServer = config.get('servers.kibana'); + const { protocol, hostname, port } = kbnTestServer; + + const kibanaUrl = Url.format({ + protocol, + hostname, + port, + }); describe('When on the Endpoint Policy Details Page', function () { this.tags(['ciGroup7']); @@ -222,6 +232,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { type: 'elasticsearch', }, }, + fleet: { + kibana: { + hosts: [kibanaUrl], + }, + }, revision: 3, agent: { monitoring: { From 4a4d7ce5a414891f9b46879d4dfdb5cc088772cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Casper=20H=C3=BCbertz?= Date: Wed, 2 Sep 2020 13:15:32 +0200 Subject: [PATCH 172/216] [APM] Minor style improvements to stacktraces (#76375) - Added some more spacing for the indention inside a nested accordion - Added additional spacing to the CausedBy accordions that created some space between the content and the containers. --- .../public/components/shared/Stacktrace/CauseStacktrace.tsx | 3 ++- .../apm/public/components/shared/Stacktrace/Stackframe.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx index 977bf2dd09cc09..8cabb820ed7f3b 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx @@ -8,13 +8,14 @@ import React from 'react'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { EuiAccordion, EuiTitle } from '@elastic/eui'; -import { px, unit } from '../../../style/variables'; +import { px, unit, units } from '../../../style/variables'; import { Stacktrace } from '.'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; // @ts-ignore Styled Components has trouble inferring the types of the default props here. const Accordion = styled(EuiAccordion)` border-top: ${({ theme }) => theme.eui.euiBorderThin}; + margin-top: ${px(units.half)}; `; const CausedByContainer = styled('h5')` diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx index a552491297d550..f72eb209f23299 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx @@ -35,7 +35,7 @@ const ContextContainer = styled.div<{ isLibraryFrame: boolean }>` // Indent the non-context frames the same amount as the accordion control const NoContextFrameHeadingWrapper = styled.div` - margin-left: ${px(units.unit + units.half + units.eighth)}; + margin-left: ${px(units.unit + units.half + units.quarter)}; `; interface Props { From 946e9f09140ac7b5683eb3c85e78c2a39851212f Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Wed, 2 Sep 2020 08:56:23 -0400 Subject: [PATCH 173/216] [Maps] Remove obsolete link (#76419) --- .../routing/routes/list/maps_list_view.js | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.js b/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.js index e9229883d708d2..8fe6866cd78348 100644 --- a/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.js +++ b/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.js @@ -12,6 +12,7 @@ import { getUiSettings, getToasts, getCoreChrome, + getNavigateToApp, } from '../../../kibana_services'; import { EuiTitle, @@ -32,11 +33,18 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { addHelpMenuToAppChrome } from '../../../help_menu_util'; -import { Link } from 'react-router-dom'; import { goToSpecifiedPath } from '../../maps_router'; +import { APP_ID, MAP_PATH } from '../../../../common/constants'; export const EMPTY_FILTER = ''; +function navigateToNewMap() { + const navigateToApp = getNavigateToApp(); + navigateToApp(APP_ID, { + path: MAP_PATH, + }); +} + export class MapsListView extends React.Component { state = { hasInitialFetchReturned: false, @@ -388,14 +396,12 @@ export class MapsListView extends React.Component { let createButton; if (!this.state.readOnly) { createButton = ( - - - - - + + + ); } return ( From 4762cf56f54b5f6fba17dbe4cdc8b18607b2dc61 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 2 Sep 2020 14:07:57 +0100 Subject: [PATCH 174/216] [ML] Switching to new es client (#74965) * [ML] Switching to new es client * further conversions * fixing tests * updating responses * test commit * refactoring shared services to removed context parameter * removing last scoped clients * removing ml js client * udating file data viz errors * fixing jest tests * fixing types after merge with master * error response changes * adding default sizes to some requests * adding custom error types for license checks * tidying up shared function checks * removing test data * removing need for DummyKibanaRequest * updating comment * fixing functional api tests * removing comments * fixing types after master merge * throw error rather than return it * removing placeholder error * changes based on review comments * fixing types after merge with master * fixing missing return Co-authored-by: Elastic Machine --- ...transaction_duration_anomaly_alert_type.ts | 7 +- .../apm/server/lib/helpers/setup_request.ts | 11 +- x-pack/plugins/infra/server/plugin.ts | 5 +- .../components/import_errors/errors.tsx | 18 +- .../common/chart_loader/chart_loader.ts | 8 +- .../ml/server/client/elasticsearch_ml.test.ts | 60 -- .../ml/server/client/elasticsearch_ml.ts | 929 ------------------ .../capabilities/check_capabilities.test.ts | 30 +- .../lib/capabilities/check_capabilities.ts | 6 +- .../ml/server/lib/capabilities/upgrade.ts | 8 +- .../ml/server/lib/check_annotations/index.ts | 12 +- .../server/lib/license/ml_server_license.ts | 3 - .../annotation_service/annotation.test.ts | 43 +- .../models/annotation_service/annotation.ts | 24 +- .../server/models/annotation_service/index.ts | 6 +- .../bucket_span_estimator.d.ts | 8 +- .../bucket_span_estimator.js | 90 +- .../bucket_span_estimator.test.ts | 17 +- .../polled_data_checker.js | 11 +- .../single_series_checker.js | 11 +- .../calculate_model_memory_limit.ts | 23 +- .../models/calendar/calendar_manager.ts | 39 +- .../server/models/calendar/event_manager.ts | 29 +- .../analytics_audit_messages.ts | 38 +- .../data_recognizer/data_recognizer.test.ts | 11 +- .../models/data_recognizer/data_recognizer.ts | 105 +- .../models/data_visualizer/data_visualizer.ts | 100 +- .../models/fields_service/fields_service.ts | 46 +- .../file_data_visualizer.ts | 12 +- .../file_data_visualizer/import_data.ts | 19 +- .../ml/server/models/filter/filter_manager.ts | 47 +- .../job_audit_messages.d.ts | 4 +- .../job_audit_messages/job_audit_messages.js | 248 +++-- .../ml/server/models/job_service/datafeeds.ts | 41 +- .../server/models/job_service/error_utils.ts | 6 +- .../ml/server/models/job_service/groups.ts | 19 +- .../ml/server/models/job_service/index.ts | 18 +- .../ml/server/models/job_service/jobs.ts | 135 ++- .../models/job_service/model_snapshots.ts | 34 +- .../new_job/categorization/examples.ts | 16 +- .../new_job/categorization/top_categories.ts | 19 +- .../categorization/validation_results.ts | 4 +- .../models/job_service/new_job/charts.ts | 8 +- .../models/job_service/new_job/line_chart.ts | 8 +- .../job_service/new_job/population_chart.ts | 18 +- .../job_service/new_job_caps/field_service.ts | 15 +- .../new_job_caps/new_job_caps.test.ts | 31 +- .../job_service/new_job_caps/new_job_caps.ts | 11 +- .../models/job_service/new_job_caps/rollup.ts | 8 +- .../job_validation/job_validation.test.ts | 47 +- .../models/job_validation/job_validation.ts | 18 +- .../job_validation/validate_bucket_span.js | 4 +- .../validate_bucket_span.test.ts | 6 +- .../validate_cardinality.test.ts | 28 +- .../job_validation/validate_cardinality.ts | 17 +- .../validate_model_memory_limit.test.ts | 44 +- .../validate_model_memory_limit.ts | 14 +- .../validate_time_range.test.ts | 19 +- .../job_validation/validate_time_range.ts | 11 +- .../get_partition_fields_values.ts | 12 +- .../models/results_service/results_service.ts | 48 +- x-pack/plugins/ml/server/plugin.ts | 38 +- .../plugins/ml/server/routes/annotations.ts | 16 +- .../ml/server/routes/anomaly_detectors.ts | 159 ++- x-pack/plugins/ml/server/routes/calendars.ts | 52 +- .../ml/server/routes/data_frame_analytics.ts | 133 +-- .../ml/server/routes/data_visualizer.ts | 26 +- x-pack/plugins/ml/server/routes/datafeeds.ts | 99 +- .../ml/server/routes/fields_service.ts | 18 +- .../ml/server/routes/file_data_visualizer.ts | 22 +- x-pack/plugins/ml/server/routes/filters.ts | 54 +- x-pack/plugins/ml/server/routes/indices.ts | 6 +- .../ml/server/routes/job_audit_messages.ts | 8 +- .../plugins/ml/server/routes/job_service.ts | 80 +- .../ml/server/routes/job_validation.ts | 22 +- x-pack/plugins/ml/server/routes/modules.ts | 38 +- .../ml/server/routes/notification_settings.ts | 13 +- .../ml/server/routes/results_service.ts | 66 +- x-pack/plugins/ml/server/routes/system.ts | 64 +- .../ml/server/shared_services/errors.ts | 12 + .../shared_services/license_checks/errors.ts | 21 + .../shared_services/license_checks/index.ts | 8 + .../{ => license_checks}/license_checks.ts | 7 +- .../providers/anomaly_detectors.ts | 35 +- .../shared_services/providers/job_service.ts | 30 +- .../shared_services/providers/modules.ts | 103 +- .../providers/results_service.ts | 31 +- .../shared_services/providers/system.ts | 95 +- .../server/shared_services/shared_services.ts | 105 +- .../signals/find_ml_signals.ts | 6 +- .../signals/signal_rule_alert_type.ts | 9 +- .../server/lib/machine_learning/authz.ts | 3 +- .../server/lib/machine_learning/index.ts | 5 +- .../usage/detections/detections_helpers.ts | 14 +- .../server/routes/api/field_histograms.ts | 2 +- .../server/lib/alerts/duration_anomaly.ts | 23 +- .../apis/ml/data_frame_analytics/update.ts | 2 +- .../data_visualizer/get_field_histograms.ts | 3 +- .../ml/data_visualizer/get_field_stats.ts | 3 +- .../ml/data_visualizer/get_overall_stats.ts | 3 +- .../ml/fields_service/field_cardinality.ts | 3 +- .../ml/fields_service/time_field_range.ts | 3 +- .../apis/ml/filters/create_filters.ts | 2 +- .../apis/ml/filters/get_filters.ts | 2 +- .../apis/ml/filters/update_filters.ts | 2 +- .../apis/ml/jobs/close_jobs.ts | 8 +- 106 files changed, 1573 insertions(+), 2568 deletions(-) delete mode 100644 x-pack/plugins/ml/server/client/elasticsearch_ml.test.ts delete mode 100644 x-pack/plugins/ml/server/client/elasticsearch_ml.ts create mode 100644 x-pack/plugins/ml/server/shared_services/errors.ts create mode 100644 x-pack/plugins/ml/server/shared_services/license_checks/errors.ts create mode 100644 x-pack/plugins/ml/server/shared_services/license_checks/index.ts rename x-pack/plugins/ml/server/shared_services/{ => license_checks}/license_checks.ts (66%) diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index 3abc89c470b21e..e7eb7b8de65e35 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -73,10 +73,9 @@ export function registerTransactionDurationAnomalyAlertType({ return; } const alertParams = params as TypeOf; - const mlClient = services.getLegacyScopedClusterClient(ml.mlClient); - const request = { params: 'DummyKibanaRequest' } as KibanaRequest; - const { mlAnomalySearch } = ml.mlSystemProvider(mlClient, request); - const anomalyDetectors = ml.anomalyDetectorsProvider(mlClient, request); + const request = {} as KibanaRequest; + const { mlAnomalySearch } = ml.mlSystemProvider(request); + const anomalyDetectors = ml.anomalyDetectorsProvider(request); const mlJobIds = await getMLJobIds( anomalyDetectors, diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index a242a0adb6d4c7..6b69e57389dffd 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -122,13 +122,10 @@ function getMlSetup( if (!ml) { return; } - const mlClient = ml.mlClient.asScoped(request); - const mlSystem = ml.mlSystemProvider(mlClient, request); + return { - mlClient, - mlSystem, - modules: ml.modulesProvider(mlClient, request, savedObjectsClient), - anomalyDetectors: ml.anomalyDetectorsProvider(mlClient, request), - mlAnomalySearch: mlSystem.mlAnomalySearch, + mlSystem: ml.mlSystemProvider(request), + anomalyDetectors: ml.anomalyDetectorsProvider(request), + modules: ml.modulesProvider(request, savedObjectsClient), }; } diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 7cd6383a9b2e5e..51f91d7189db70 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -152,9 +152,8 @@ export class InfraServerPlugin { core.http.registerRouteHandlerContext( 'infra', (context, request): InfraRequestHandlerContext => { - const mlSystem = context.ml && plugins.ml?.mlSystemProvider(context.ml?.mlClient, request); - const mlAnomalyDetectors = - context.ml && plugins.ml?.anomalyDetectorsProvider(context.ml?.mlClient, request); + const mlSystem = plugins.ml?.mlSystemProvider(request); + const mlAnomalyDetectors = plugins.ml?.anomalyDetectorsProvider(request); const spaceId = plugins.spaces?.spacesService.getSpaceId(request) || 'default'; return { diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_errors/errors.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_errors/errors.tsx index f723ad1a752bf4..bcbba67e9cc60c 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_errors/errors.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_errors/errors.tsx @@ -93,11 +93,11 @@ function title(statuses: Statuses) { } } -function ImportError(error: any, key: number) { +const ImportError: FC<{ error: any }> = ({ error }) => { const errorObj = toString(error); return ( - -

{errorObj.msg}

+ <> +

{errorObj.msg}

{errorObj.more !== undefined && ( )} -
+ ); -} +}; function toString(error: any): ImportError { if (typeof error === 'string') { @@ -127,11 +127,11 @@ function toString(error: any): ImportError { return { msg: error.msg }; } else if (error.error !== undefined) { if (typeof error.error === 'object') { - if (error.error.msg !== undefined) { + if (error.error.reason !== undefined) { // this will catch a bulk ingest failure - const errorObj: ImportError = { msg: error.error.msg }; - if (error.error.body !== undefined) { - errorObj.more = error.error.response; + const errorObj: ImportError = { msg: error.error.reason }; + if (error.error.root_cause !== undefined) { + errorObj.more = JSON.stringify(error.error.root_cause); } return errorObj; } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts index 328cd1a5ef8d7d..d61e75fd21b5a6 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts @@ -71,9 +71,7 @@ export class ChartLoader { splitFieldName, splitFieldValue ); - if (resp.error !== undefined) { - throw resp.error; - } + return resp.results; } return {}; @@ -105,9 +103,7 @@ export class ChartLoader { aggFieldPairNames, splitFieldName ); - if (resp.error !== undefined) { - throw resp.error; - } + return resp.results; } return {}; diff --git a/x-pack/plugins/ml/server/client/elasticsearch_ml.test.ts b/x-pack/plugins/ml/server/client/elasticsearch_ml.test.ts deleted file mode 100644 index 5ad0db3c58ce41..00000000000000 --- a/x-pack/plugins/ml/server/client/elasticsearch_ml.test.ts +++ /dev/null @@ -1,60 +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 { elasticsearchJsPlugin } from './elasticsearch_ml'; - -interface Endpoint { - fmt: string; -} - -interface ClientAction { - urls?: Endpoint[]; - url: Endpoint; -} - -describe('ML - Endpoints', () => { - // Check all paths in the ML elasticsearchJsPlugin start with a leading forward slash - // so they work if Kibana is run behind a reverse proxy - const PATH_START: string = '/'; - const urls: string[] = []; - - // Stub objects - const Client = { - prototype: {}, - }; - - const components = { - clientAction: { - factory(obj: ClientAction) { - // add each endpoint URL to a list - if (obj.urls) { - obj.urls.forEach((url) => { - urls.push(url.fmt); - }); - } - if (obj.url) { - urls.push(obj.url.fmt); - } - }, - namespaceFactory() { - return { - prototype: {}, - }; - }, - }, - }; - - // Stub elasticsearchJsPlugin - elasticsearchJsPlugin(Client, null, components); - - describe('paths', () => { - it(`should start with ${PATH_START}`, () => { - urls.forEach((url) => { - expect(url[0]).toEqual(PATH_START); - }); - }); - }); -}); diff --git a/x-pack/plugins/ml/server/client/elasticsearch_ml.ts b/x-pack/plugins/ml/server/client/elasticsearch_ml.ts deleted file mode 100644 index 63153d18cb10b9..00000000000000 --- a/x-pack/plugins/ml/server/client/elasticsearch_ml.ts +++ /dev/null @@ -1,929 +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. - */ - -export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { - const ca = components.clientAction.factory; - - Client.prototype.ml = components.clientAction.namespaceFactory(); - const ml = Client.prototype.ml.prototype; - - /** - * Perform a [ml.authenticate](Retrieve details about the currently authenticated user) request - * - * @param {Object} params - An object with parameters used to carry out this action - */ - ml.jobs = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>', - req: { - jobId: { - type: 'list', - }, - }, - }, - { - fmt: '/_ml/anomaly_detectors/', - }, - ], - method: 'GET', - }); - - ml.jobStats = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/_stats', - req: { - jobId: { - type: 'list', - }, - }, - }, - { - fmt: '/_ml/anomaly_detectors/_stats', - }, - ], - method: 'GET', - }); - - ml.addJob = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>', - req: { - jobId: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'PUT', - }); - - ml.openJob = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/_open', - req: { - jobId: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ml.closeJob = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/_close?force=<%=force%>', - req: { - jobId: { - type: 'string', - }, - force: { - type: 'boolean', - }, - }, - }, - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/_close', - req: { - jobId: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - // Currently the endpoint uses a default size of 100 unless a size is supplied. - // So until paging is supported in the UI, explicitly supply a size of 1000 - // to match the max number of docs that the endpoint can return. - ml.getDataFrameAnalytics = ca({ - urls: [ - { - fmt: '/_ml/data_frame/analytics/<%=analyticsId%>', - req: { - analyticsId: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/data_frame/analytics/_all?size=1000', - }, - ], - method: 'GET', - }); - - ml.getDataFrameAnalyticsStats = ca({ - urls: [ - { - fmt: '/_ml/data_frame/analytics/<%=analyticsId%>/_stats', - req: { - analyticsId: { - type: 'string', - }, - }, - }, - { - // Currently the endpoint uses a default size of 100 unless a size is supplied. - // So until paging is supported in the UI, explicitly supply a size of 1000 - // to match the max number of docs that the endpoint can return. - fmt: '/_ml/data_frame/analytics/_all/_stats?size=1000', - }, - ], - method: 'GET', - }); - - ml.createDataFrameAnalytics = ca({ - urls: [ - { - fmt: '/_ml/data_frame/analytics/<%=analyticsId%>', - req: { - analyticsId: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'PUT', - }); - - ml.evaluateDataFrameAnalytics = ca({ - urls: [ - { - fmt: '/_ml/data_frame/_evaluate', - }, - ], - needBody: true, - method: 'POST', - }); - - ml.explainDataFrameAnalytics = ca({ - urls: [ - { - fmt: '/_ml/data_frame/analytics/_explain', - }, - ], - needBody: true, - method: 'POST', - }); - - ml.deleteDataFrameAnalytics = ca({ - urls: [ - { - fmt: '/_ml/data_frame/analytics/<%=analyticsId%>', - req: { - analyticsId: { - type: 'string', - }, - }, - }, - ], - method: 'DELETE', - }); - - ml.startDataFrameAnalytics = ca({ - urls: [ - { - fmt: '/_ml/data_frame/analytics/<%=analyticsId%>/_start', - req: { - analyticsId: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ml.stopDataFrameAnalytics = ca({ - urls: [ - { - fmt: '/_ml/data_frame/analytics/<%=analyticsId%>/_stop?&force=<%=force%>', - req: { - analyticsId: { - type: 'string', - }, - force: { - type: 'boolean', - }, - }, - }, - ], - method: 'POST', - }); - - ml.updateDataFrameAnalytics = ca({ - urls: [ - { - fmt: '/_ml/data_frame/analytics/<%=analyticsId%>/_update', - req: { - analyticsId: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'POST', - }); - - ml.deleteJob = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>?&force=<%=force%>&wait_for_completion=false', - req: { - jobId: { - type: 'string', - }, - force: { - type: 'boolean', - }, - }, - }, - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>?&wait_for_completion=false', - req: { - jobId: { - type: 'string', - }, - }, - }, - ], - method: 'DELETE', - }); - - ml.updateJob = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/_update', - req: { - jobId: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'POST', - }); - - ml.datafeeds = ca({ - urls: [ - { - fmt: '/_ml/datafeeds/<%=datafeedId%>', - req: { - datafeedId: { - type: 'list', - }, - }, - }, - { - fmt: '/_ml/datafeeds/', - }, - ], - method: 'GET', - }); - - ml.datafeedStats = ca({ - urls: [ - { - fmt: '/_ml/datafeeds/<%=datafeedId%>/_stats', - req: { - datafeedId: { - type: 'list', - }, - }, - }, - { - fmt: '/_ml/datafeeds/_stats', - }, - ], - method: 'GET', - }); - - ml.addDatafeed = ca({ - urls: [ - { - fmt: '/_ml/datafeeds/<%=datafeedId%>', - req: { - datafeedId: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'PUT', - }); - - ml.updateDatafeed = ca({ - urls: [ - { - fmt: '/_ml/datafeeds/<%=datafeedId%>/_update', - req: { - datafeedId: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'POST', - }); - - ml.deleteDatafeed = ca({ - urls: [ - { - fmt: '/_ml/datafeeds/<%=datafeedId%>?force=<%=force%>', - req: { - datafeedId: { - type: 'string', - }, - force: { - type: 'boolean', - }, - }, - }, - { - fmt: '/_ml/datafeeds/<%=datafeedId%>', - req: { - datafeedId: { - type: 'string', - }, - }, - }, - ], - method: 'DELETE', - }); - - ml.startDatafeed = ca({ - urls: [ - { - fmt: '/_ml/datafeeds/<%=datafeedId%>/_start?&start=<%=start%>&end=<%=end%>', - req: { - datafeedId: { - type: 'string', - }, - start: { - type: 'string', - }, - end: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/datafeeds/<%=datafeedId%>/_start?&start=<%=start%>', - req: { - datafeedId: { - type: 'string', - }, - start: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/datafeeds/<%=datafeedId%>/_start', - req: { - datafeedId: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ml.stopDatafeed = ca({ - urls: [ - { - fmt: '/_ml/datafeeds/<%=datafeedId%>/_stop?force=<%=force%>', - req: { - datafeedId: { - type: 'string', - }, - force: { - type: 'boolean', - }, - }, - }, - { - fmt: '/_ml/datafeeds/<%=datafeedId%>/_stop', - req: { - datafeedId: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ml.validateDetector = ca({ - url: { - fmt: '/_ml/anomaly_detectors/_validate/detector', - }, - needBody: true, - method: 'POST', - }); - - ml.estimateModelMemory = ca({ - url: { - fmt: '/_ml/anomaly_detectors/_estimate_model_memory', - }, - needBody: true, - method: 'POST', - }); - - ml.datafeedPreview = ca({ - url: { - fmt: '/_ml/datafeeds/<%=datafeedId%>/_preview', - req: { - datafeedId: { - type: 'string', - }, - }, - }, - method: 'GET', - }); - - ml.forecast = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/_forecast?&duration=<%=duration%>', - req: { - jobId: { - type: 'string', - }, - duration: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/_forecast', - req: { - jobId: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ml.records = ca({ - url: { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/results/records', - req: { - jobId: { - type: 'string', - }, - }, - }, - method: 'POST', - }); - - ml.buckets = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/results/buckets', - req: { - jobId: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/results/buckets/<%=timestamp%>', - req: { - jobId: { - type: 'string', - }, - timestamp: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ml.overallBuckets = ca({ - url: { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/results/overall_buckets', - req: { - jobId: { - type: 'string', - }, - }, - }, - method: 'POST', - }); - - ml.privilegeCheck = ca({ - url: { - fmt: '/_security/user/_has_privileges', - }, - needBody: true, - method: 'POST', - }); - // Currently the endpoint uses a default size of 100 unless a size is supplied. So until paging is supported in the UI, explicitly supply a size of 1000 - ml.calendars = ca({ - urls: [ - { - fmt: '/_ml/calendars/<%=calendarId%>', - req: { - calendarId: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/calendars?size=1000', - }, - ], - method: 'GET', - }); - - ml.deleteCalendar = ca({ - url: { - fmt: '/_ml/calendars/<%=calendarId%>', - req: { - calendarId: { - type: 'string', - }, - }, - }, - method: 'DELETE', - }); - - ml.addCalendar = ca({ - url: { - fmt: '/_ml/calendars/<%=calendarId%>', - req: { - calendarId: { - type: 'string', - }, - }, - }, - needBody: true, - method: 'PUT', - }); - - ml.addJobToCalendar = ca({ - url: { - fmt: '/_ml/calendars/<%=calendarId%>/jobs/<%=jobId%>', - req: { - calendarId: { - type: 'string', - }, - jobId: { - type: 'string', - }, - }, - }, - method: 'PUT', - }); - - ml.removeJobFromCalendar = ca({ - url: { - fmt: '/_ml/calendars/<%=calendarId%>/jobs/<%=jobId%>', - req: { - calendarId: { - type: 'string', - }, - jobId: { - type: 'string', - }, - }, - }, - method: 'DELETE', - }); - - ml.events = ca({ - urls: [ - { - fmt: '/_ml/calendars/<%=calendarId%>/events', - req: { - calendarId: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/calendars/<%=calendarId%>/events?&job_id=<%=jobId%>', - req: { - calendarId: { - type: 'string', - }, - jobId: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/calendars/<%=calendarId%>/events?&after=<%=start%>&before=<%=end%>', - req: { - calendarId: { - type: 'string', - }, - start: { - type: 'string', - }, - end: { - type: 'string', - }, - }, - }, - { - fmt: - '/_ml/calendars/<%=calendarId%>/events?&after=<%=start%>&before=<%=end%>&job_id=<%=jobId%>', - req: { - calendarId: { - type: 'string', - }, - start: { - type: 'string', - }, - end: { - type: 'string', - }, - jobId: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - ml.addEvent = ca({ - url: { - fmt: '/_ml/calendars/<%=calendarId%>/events', - req: { - calendarId: { - type: 'string', - }, - }, - }, - needBody: true, - method: 'POST', - }); - - ml.deleteEvent = ca({ - url: { - fmt: '/_ml/calendars/<%=calendarId%>/events/<%=eventId%>', - req: { - calendarId: { - type: 'string', - }, - eventId: { - type: 'string', - }, - }, - }, - method: 'DELETE', - }); - // Currently the endpoint uses a default size of 100 unless a size is supplied. So until paging is supported in the UI, explicitly supply a size of 1000 - ml.filters = ca({ - urls: [ - { - fmt: '/_ml/filters/<%=filterId%>', - req: { - filterId: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/filters?size=1000', - }, - ], - method: 'GET', - }); - - ml.addFilter = ca({ - url: { - fmt: '/_ml/filters/<%=filterId%>', - req: { - filterId: { - type: 'string', - }, - }, - }, - needBody: true, - method: 'PUT', - }); - - ml.updateFilter = ca({ - urls: [ - { - fmt: '/_ml/filters/<%=filterId%>/_update', - req: { - filterId: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'POST', - }); - - ml.deleteFilter = ca({ - url: { - fmt: '/_ml/filters/<%=filterId%>', - req: { - filterId: { - type: 'string', - }, - }, - }, - method: 'DELETE', - }); - - ml.info = ca({ - url: { - fmt: '/_ml/info', - }, - method: 'GET', - }); - - ml.fileStructure = ca({ - urls: [ - { - fmt: - '/_ml/find_file_structure?&explain=true&charset=<%=charset%>&format=<%=format%>&has_header_row=<%=has_header_row%>&column_names=<%=column_names%>&delimiter=<%=delimiter%>"e=<%=quote%>&should_trim_fields=<%=should_trim_fields%>&grok_pattern=<%=grok_pattern%>×tamp_field=<%=timestamp_field%>×tamp_format=<%=timestamp_format%>&lines_to_sample=<%=lines_to_sample%>', - req: { - charset: { - type: 'string', - }, - format: { - type: 'string', - }, - has_header_row: { - type: 'string', - }, - column_names: { - type: 'string', - }, - delimiter: { - type: 'string', - }, - quote: { - type: 'string', - }, - should_trim_fields: { - type: 'string', - }, - grok_pattern: { - type: 'string', - }, - timestamp_field: { - type: 'string', - }, - timestamp_format: { - type: 'string', - }, - lines_to_sample: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/find_file_structure?&explain=true', - }, - ], - needBody: true, - method: 'POST', - }); - - ml.rollupIndexCapabilities = ca({ - urls: [ - { - fmt: '/<%=indexPattern%>/_rollup/data', - req: { - indexPattern: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - ml.categories = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/results/categories/<%=categoryId%>', - req: { - jobId: { - type: 'string', - }, - categoryId: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/results/categories', - req: { - jobId: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - ml.modelSnapshots = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/model_snapshots/<%=snapshotId%>', - req: { - jobId: { - type: 'string', - }, - snapshotId: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/model_snapshots', - req: { - jobId: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - ml.updateModelSnapshot = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/model_snapshots/<%=snapshotId%>/_update', - req: { - jobId: { - type: 'string', - }, - snapshotId: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - needBody: true, - }); - - ml.deleteModelSnapshot = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/model_snapshots/<%=snapshotId%>', - req: { - jobId: { - type: 'string', - }, - snapshotId: { - type: 'string', - }, - }, - }, - ], - method: 'DELETE', - }); - - ml.revertModelSnapshot = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/model_snapshots/<%=snapshotId%>/_revert', - req: { - jobId: { - type: 'string', - }, - snapshotId: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); -}; diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts index 21d32813c0d511..4dd17f8cf48896 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { getAdminCapabilities, getUserCapabilities } from './__mocks__/ml_capabilities'; import { capabilitiesProvider } from './check_capabilities'; import { MlLicense } from '../../../common/license'; @@ -24,16 +24,28 @@ const mlIsEnabled = async () => true; const mlIsNotEnabled = async () => false; const mlClusterClientNonUpgrade = ({ - callAsInternalUser: async () => ({ - upgrade_mode: false, - }), -} as unknown) as ILegacyScopedClusterClient; + asInternalUser: { + ml: { + info: async () => ({ + body: { + upgrade_mode: false, + }, + }), + }, + }, +} as unknown) as IScopedClusterClient; const mlClusterClientUpgrade = ({ - callAsInternalUser: async () => ({ - upgrade_mode: true, - }), -} as unknown) as ILegacyScopedClusterClient; + asInternalUser: { + ml: { + info: async () => ({ + body: { + upgrade_mode: true, + }, + }), + }, + }, +} as unknown) as IScopedClusterClient; describe('check_capabilities', () => { describe('getCapabilities() - right number of capabilities', () => { diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts index c976ab598b28c6..c591ec07c7c3bd 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient, KibanaRequest } from 'kibana/server'; +import { IScopedClusterClient, KibanaRequest } from 'kibana/server'; import { mlLog } from '../../client/log'; import { MlCapabilities, @@ -22,12 +22,12 @@ import { } from './errors'; export function capabilitiesProvider( - mlClusterClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, capabilities: MlCapabilities, mlLicense: MlLicense, isMlEnabledInSpace: () => Promise ) { - const { isUpgradeInProgress } = upgradeCheckProvider(mlClusterClient); + const { isUpgradeInProgress } = upgradeCheckProvider(client); async function getCapabilities(): Promise { const upgradeInProgress = await isUpgradeInProgress(); const isPlatinumOrTrialLicense = mlLicense.isFullLicense(); diff --git a/x-pack/plugins/ml/server/lib/capabilities/upgrade.ts b/x-pack/plugins/ml/server/lib/capabilities/upgrade.ts index 6df4d0c87ecf54..defb70429fa0c7 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/upgrade.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/upgrade.ts @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { mlLog } from '../../client/log'; -export function upgradeCheckProvider({ callAsInternalUser }: ILegacyScopedClusterClient) { +export function upgradeCheckProvider({ asInternalUser }: IScopedClusterClient) { async function isUpgradeInProgress(): Promise { let upgradeInProgress = false; try { - const info = await callAsInternalUser('ml.info'); + const { body } = await asInternalUser.ml.info(); // if ml indices are currently being migrated, upgrade_mode will be set to true // pass this back with the privileges to allow for the disabling of UI controls. - upgradeInProgress = info.upgrade_mode === true; + upgradeInProgress = body.upgrade_mode === true; } catch (error) { // if the ml.info check fails, it could be due to the user having insufficient privileges // most likely they do not have the ml_user role and therefore will be blocked from using diff --git a/x-pack/plugins/ml/server/lib/check_annotations/index.ts b/x-pack/plugins/ml/server/lib/check_annotations/index.ts index de19f0ead67910..964f9d0b922612 100644 --- a/x-pack/plugins/ml/server/lib/check_annotations/index.ts +++ b/x-pack/plugins/ml/server/lib/check_annotations/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { mlLog } from '../../client/log'; import { @@ -17,18 +17,16 @@ import { // - ML_ANNOTATIONS_INDEX_PATTERN index is present // - ML_ANNOTATIONS_INDEX_ALIAS_READ alias is present // - ML_ANNOTATIONS_INDEX_ALIAS_WRITE alias is present -export async function isAnnotationsFeatureAvailable({ - callAsInternalUser, -}: ILegacyScopedClusterClient) { +export async function isAnnotationsFeatureAvailable({ asInternalUser }: IScopedClusterClient) { try { const indexParams = { index: ML_ANNOTATIONS_INDEX_PATTERN }; - const annotationsIndexExists = await callAsInternalUser('indices.exists', indexParams); + const { body: annotationsIndexExists } = await asInternalUser.indices.exists(indexParams); if (!annotationsIndexExists) { return false; } - const annotationsReadAliasExists = await callAsInternalUser('indices.existsAlias', { + const { body: annotationsReadAliasExists } = await asInternalUser.indices.existsAlias({ index: ML_ANNOTATIONS_INDEX_ALIAS_READ, name: ML_ANNOTATIONS_INDEX_ALIAS_READ, }); @@ -37,7 +35,7 @@ export async function isAnnotationsFeatureAvailable({ return false; } - const annotationsWriteAliasExists = await callAsInternalUser('indices.existsAlias', { + const { body: annotationsWriteAliasExists } = await asInternalUser.indices.existsAlias({ index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, name: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, }); diff --git a/x-pack/plugins/ml/server/lib/license/ml_server_license.ts b/x-pack/plugins/ml/server/lib/license/ml_server_license.ts index bd0a29721248a1..6e3019b303b887 100644 --- a/x-pack/plugins/ml/server/lib/license/ml_server_license.ts +++ b/x-pack/plugins/ml/server/lib/license/ml_server_license.ts @@ -7,7 +7,6 @@ import { KibanaRequest, KibanaResponseFactory, RequestHandlerContext, - ILegacyScopedClusterClient, IScopedClusterClient, RequestHandler, } from 'kibana/server'; @@ -15,7 +14,6 @@ import { import { MlLicense } from '../../../common/license'; type Handler = (handlerParams: { - legacyClient: ILegacyScopedClusterClient; client: IScopedClusterClient; request: KibanaRequest; response: KibanaResponseFactory; @@ -42,7 +40,6 @@ function guard(check: () => boolean, handler: Handler) { } return handler({ - legacyClient: context.ml!.mlClient, client: context.core.elasticsearch.client, request, response, diff --git a/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts b/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts index 5be443266ffe1c..4c511b567615de 100644 --- a/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts +++ b/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts @@ -22,19 +22,15 @@ describe('annotation_service', () => { let mlClusterClientSpy = {} as any; beforeEach(() => { - const callAs = jest.fn((action: string) => { - switch (action) { - case 'delete': - case 'index': - return Promise.resolve(acknowledgedResponseMock); - case 'search': - return Promise.resolve(getAnnotationsResponseMock); - } - }); + const callAs = { + delete: jest.fn(() => Promise.resolve({ body: acknowledgedResponseMock })), + index: jest.fn(() => Promise.resolve({ body: acknowledgedResponseMock })), + search: jest.fn(() => Promise.resolve({ body: getAnnotationsResponseMock })), + }; mlClusterClientSpy = { - callAsCurrentUser: callAs, - callAsInternalUser: callAs, + asCurrentUser: callAs, + asInternalUser: callAs, }; }); @@ -52,8 +48,7 @@ describe('annotation_service', () => { const response = await deleteAnnotation(annotationMockId); - expect(mockFunct.callAsInternalUser.mock.calls[0][0]).toBe('delete'); - expect(mockFunct.callAsInternalUser.mock.calls[0][1]).toEqual(deleteParamsMock); + expect(mockFunct.asInternalUser.delete.mock.calls[0][0]).toStrictEqual(deleteParamsMock); expect(response).toBe(acknowledgedResponseMock); done(); }); @@ -73,8 +68,9 @@ describe('annotation_service', () => { const response: GetResponse = await getAnnotations(indexAnnotationArgsMock); - expect(mockFunct.callAsInternalUser.mock.calls[0][0]).toBe('search'); - expect(mockFunct.callAsInternalUser.mock.calls[0][1]).toEqual(getAnnotationsRequestMock); + expect(mockFunct.asInternalUser.search.mock.calls[0][0]).toStrictEqual( + getAnnotationsRequestMock + ); expect(Object.keys(response.annotations)).toHaveLength(1); expect(response.annotations[jobIdMock]).toHaveLength(2); expect(isAnnotations(response.annotations[jobIdMock])).toBeTruthy(); @@ -89,9 +85,9 @@ describe('annotation_service', () => { }; const mlClusterClientSpyError: any = { - callAsInternalUser: jest.fn(() => { - return Promise.resolve(mockEsError); - }), + asInternalUser: { + search: jest.fn(() => Promise.resolve({ body: mockEsError })), + }, }; const { getAnnotations } = annotationServiceProvider(mlClusterClientSpyError); @@ -124,10 +120,8 @@ describe('annotation_service', () => { const response = await indexAnnotation(annotationMock, usernameMock); - expect(mockFunct.callAsInternalUser.mock.calls[0][0]).toBe('index'); - // test if the annotation has been correctly augmented - const indexParamsCheck = mockFunct.callAsInternalUser.mock.calls[0][1]; + const indexParamsCheck = mockFunct.asInternalUser.index.mock.calls[0][0]; const annotation = indexParamsCheck.body; expect(annotation.create_username).toBe(usernameMock); expect(annotation.modified_username).toBe(usernameMock); @@ -154,10 +148,8 @@ describe('annotation_service', () => { const response = await indexAnnotation(annotationMock, usernameMock); - expect(mockFunct.callAsInternalUser.mock.calls[0][0]).toBe('index'); - // test if the annotation has been correctly augmented - const indexParamsCheck = mockFunct.callAsInternalUser.mock.calls[0][1]; + const indexParamsCheck = mockFunct.asInternalUser.index.mock.calls[0][0]; const annotation = indexParamsCheck.body; expect(annotation.create_username).toBe(usernameMock); expect(annotation.modified_username).toBe(usernameMock); @@ -196,9 +188,8 @@ describe('annotation_service', () => { await indexAnnotation(annotation, modifiedUsernameMock); - expect(mockFunct.callAsInternalUser.mock.calls[1][0]).toBe('index'); // test if the annotation has been correctly updated - const indexParamsCheck = mockFunct.callAsInternalUser.mock.calls[1][1]; + const indexParamsCheck = mockFunct.asInternalUser.index.mock.calls[0][0]; const modifiedAnnotation = indexParamsCheck.body; expect(modifiedAnnotation.annotation).toBe(modifiedAnnotationText); expect(modifiedAnnotation.create_username).toBe(originalUsernameMock); diff --git a/x-pack/plugins/ml/server/models/annotation_service/annotation.ts b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts index a585449db0a255..24f1d6951c9400 100644 --- a/x-pack/plugins/ml/server/models/annotation_service/annotation.ts +++ b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts @@ -7,7 +7,7 @@ import Boom from 'boom'; import each from 'lodash/each'; import get from 'lodash/get'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { ANNOTATION_EVENT_USER, ANNOTATION_TYPE } from '../../../common/constants/annotations'; import { PARTITION_FIELDS } from '../../../common/constants/anomalies'; @@ -67,17 +67,17 @@ export interface GetResponse { export interface IndexParams { index: string; body: Annotation; - refresh?: string; + refresh: boolean | 'wait_for' | undefined; id?: string; } export interface DeleteParams { index: string; - refresh?: string; + refresh: boolean | 'wait_for' | undefined; id: string; } -export function annotationProvider({ callAsInternalUser }: ILegacyScopedClusterClient) { +export function annotationProvider({ asInternalUser }: IScopedClusterClient) { async function indexAnnotation(annotation: Annotation, username: string) { if (isAnnotation(annotation) === false) { // No need to translate, this will not be exposed in the UI. @@ -104,7 +104,8 @@ export function annotationProvider({ callAsInternalUser }: ILegacyScopedClusterC delete params.body.key; } - return await callAsInternalUser('index', params); + const { body } = await asInternalUser.index(params); + return body; } async function getAnnotations({ @@ -287,14 +288,14 @@ export function annotationProvider({ callAsInternalUser }: ILegacyScopedClusterC }; try { - const resp = await callAsInternalUser('search', params); + const { body } = await asInternalUser.search(params); - if (resp.error !== undefined && resp.message !== undefined) { + if (body.error !== undefined && body.message !== undefined) { // No need to translate, this will not be exposed in the UI. throw new Error(`Annotations couldn't be retrieved from Elasticsearch.`); } - const docs: Annotations = get(resp, ['hits', 'hits'], []).map((d: EsResult) => { + const docs: Annotations = get(body, ['hits', 'hits'], []).map((d: EsResult) => { // get the original source document and the document id, we need it // to identify the annotation when editing/deleting it. // if original `event` is undefined then substitute with 'user` by default @@ -306,7 +307,7 @@ export function annotationProvider({ callAsInternalUser }: ILegacyScopedClusterC } as Annotation; }); - const aggregations = get(resp, ['aggregations'], {}) as EsAggregationResult; + const aggregations = get(body, ['aggregations'], {}) as EsAggregationResult; if (fields) { obj.aggregations = aggregations; } @@ -330,13 +331,14 @@ export function annotationProvider({ callAsInternalUser }: ILegacyScopedClusterC } async function deleteAnnotation(id: string) { - const param: DeleteParams = { + const params: DeleteParams = { index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, id, refresh: 'wait_for', }; - return await callAsInternalUser('delete', param); + const { body } = await asInternalUser.delete(params); + return body; } return { diff --git a/x-pack/plugins/ml/server/models/annotation_service/index.ts b/x-pack/plugins/ml/server/models/annotation_service/index.ts index e17af2a154b876..9fcb84e2938ae3 100644 --- a/x-pack/plugins/ml/server/models/annotation_service/index.ts +++ b/x-pack/plugins/ml/server/models/annotation_service/index.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { annotationProvider } from './annotation'; -export function annotationServiceProvider(mlClusterClient: ILegacyScopedClusterClient) { +export function annotationServiceProvider(client: IScopedClusterClient) { return { - ...annotationProvider(mlClusterClient), + ...annotationProvider(client), }; } diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts index eeabb24d9be3b7..5b52414d6753a2 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { ES_AGGREGATION } from '../../../common/constants/aggregation_types'; export interface BucketSpanEstimatorData { @@ -21,6 +21,6 @@ export interface BucketSpanEstimatorData { } export function estimateBucketSpanFactory({ - callAsCurrentUser, - callAsInternalUser, -}: ILegacyScopedClusterClient): (config: BucketSpanEstimatorData) => Promise; + asCurrentUser, + asInternalUser, +}: IScopedClusterClient): (config: BucketSpanEstimatorData) => Promise; diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js index 381c615051e3b3..1d59db8fa564f4 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js @@ -16,10 +16,10 @@ import { INTERVALS } from './intervals'; import { singleSeriesCheckerFactory } from './single_series_checker'; import { polledDataCheckerFactory } from './polled_data_checker'; -export function estimateBucketSpanFactory(mlClusterClient) { - const { callAsCurrentUser, callAsInternalUser } = mlClusterClient; - const PolledDataChecker = polledDataCheckerFactory(mlClusterClient); - const SingleSeriesChecker = singleSeriesCheckerFactory(mlClusterClient); +export function estimateBucketSpanFactory(client) { + const { asCurrentUser, asInternalUser } = client; + const PolledDataChecker = polledDataCheckerFactory(client); + const SingleSeriesChecker = singleSeriesCheckerFactory(client); class BucketSpanEstimator { constructor( @@ -246,21 +246,22 @@ export function estimateBucketSpanFactory(mlClusterClient) { const getFieldCardinality = function (index, field) { return new Promise((resolve, reject) => { - callAsCurrentUser('search', { - index, - size: 0, - body: { - aggs: { - field_count: { - cardinality: { - field, + asCurrentUser + .search({ + index, + size: 0, + body: { + aggs: { + field_count: { + cardinality: { + field, + }, }, }, }, - }, - }) - .then((resp) => { - const value = get(resp, ['aggregations', 'field_count', 'value'], 0); + }) + .then(({ body }) => { + const value = get(body, ['aggregations', 'field_count', 'value'], 0); resolve(value); }) .catch((resp) => { @@ -278,28 +279,29 @@ export function estimateBucketSpanFactory(mlClusterClient) { getFieldCardinality(index, field) .then((value) => { const numPartitions = Math.floor(value / NUM_PARTITIONS) || 1; - callAsCurrentUser('search', { - index, - size: 0, - body: { - query, - aggs: { - fields_bucket_counts: { - terms: { - field, - include: { - partition: 0, - num_partitions: numPartitions, + asCurrentUser + .search({ + index, + size: 0, + body: { + query, + aggs: { + fields_bucket_counts: { + terms: { + field, + include: { + partition: 0, + num_partitions: numPartitions, + }, }, }, }, }, - }, - }) - .then((partitionResp) => { + }) + .then(({ body }) => { // eslint-disable-next-line camelcase - if (partitionResp.aggregations?.fields_bucket_counts?.buckets !== undefined) { - const buckets = partitionResp.aggregations.fields_bucket_counts.buckets; + if (body.aggregations?.fields_bucket_counts?.buckets !== undefined) { + const buckets = body.aggregations.fields_bucket_counts.buckets; fieldValues = buckets.map((b) => b.key); } resolve(fieldValues); @@ -338,21 +340,21 @@ export function estimateBucketSpanFactory(mlClusterClient) { return new Promise((resolve, reject) => { // fetch the `search.max_buckets` cluster setting so we're able to // adjust aggregations to not exceed that limit. - callAsInternalUser('cluster.getSettings', { - flatSettings: true, - includeDefaults: true, - filterPath: '*.*max_buckets', - }) - .then((settings) => { - if (typeof settings !== 'object') { + asInternalUser.cluster + .getSettings({ + flat_settings: true, + include_defaults: true, + filter_path: '*.*max_buckets', + }) + .then(({ body }) => { + if (typeof body !== 'object') { reject('Unable to retrieve cluster settings'); } // search.max_buckets could exist in default, persistent or transient cluster settings - const maxBucketsSetting = (settings.defaults || - settings.persistent || - settings.transient || - {})['search.max_buckets']; + const maxBucketsSetting = (body.defaults || body.persistent || body.transient || {})[ + 'search.max_buckets' + ]; if (maxBucketsSetting === undefined) { reject('Unable to retrieve cluster setting search.max_buckets'); diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts index f7c7dd8172ea5a..35c4f1a0a741b4 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts @@ -4,22 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { ES_AGGREGATION } from '../../../common/constants/aggregation_types'; import { estimateBucketSpanFactory, BucketSpanEstimatorData } from './bucket_span_estimator'; -const callAs = () => { - return new Promise((resolve) => { - resolve({}); - }) as Promise; +const callAs = { + search: () => Promise.resolve({ body: {} }), + cluster: { getSettings: () => Promise.resolve({ body: {} }) }, }; -const mlClusterClient: ILegacyScopedClusterClient = { - callAsCurrentUser: callAs, - callAsInternalUser: callAs, -}; +const mlClusterClient = ({ + asCurrentUser: callAs, + asInternalUser: callAs, +} as unknown) as IScopedClusterClient; // mock configuration to be passed to the estimator const formConfig: BucketSpanEstimatorData = { diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js index d3bbd59f3cf9bc..fd0cab7c0625d7 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js @@ -12,7 +12,7 @@ import get from 'lodash/get'; -export function polledDataCheckerFactory({ callAsCurrentUser }) { +export function polledDataCheckerFactory({ asCurrentUser }) { class PolledDataChecker { constructor(index, timeField, duration, query) { this.index = index; @@ -65,14 +65,15 @@ export function polledDataCheckerFactory({ callAsCurrentUser }) { return search; } - performSearch(intervalMs) { - const body = this.createSearch(intervalMs); + async performSearch(intervalMs) { + const searchBody = this.createSearch(intervalMs); - return callAsCurrentUser('search', { + const { body } = await asCurrentUser.search({ index: this.index, size: 0, - body, + body: searchBody, }); + return body; } // test that the coefficient of variation of time difference between non-empty buckets is small diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js index a5449395501dcb..750f0cfc0b4a87 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js @@ -13,7 +13,7 @@ import { mlLog } from '../../client/log'; import { INTERVALS, LONG_INTERVALS } from './intervals'; -export function singleSeriesCheckerFactory({ callAsCurrentUser }) { +export function singleSeriesCheckerFactory({ asCurrentUser }) { const REF_DATA_INTERVAL = { name: '1h', ms: 3600000 }; class SingleSeriesChecker { @@ -184,14 +184,15 @@ export function singleSeriesCheckerFactory({ callAsCurrentUser }) { return search; } - performSearch(intervalMs) { - const body = this.createSearch(intervalMs); + async performSearch(intervalMs) { + const searchBody = this.createSearch(intervalMs); - return callAsCurrentUser('search', { + const { body } = await asCurrentUser.search({ index: this.index, size: 0, - body, + body: searchBody, }); + return body; } getFullBuckets(buckets) { diff --git a/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts index bc3c326e7d0705..2a2c30601e2ca7 100644 --- a/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts +++ b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts @@ -5,13 +5,13 @@ */ import numeral from '@elastic/numeral'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { MLCATEGORY } from '../../../common/constants/field_types'; import { AnalysisConfig } from '../../../common/types/anomaly_detection_jobs'; import { fieldsServiceProvider } from '../fields_service'; import { MlInfoResponse } from '../../../common/types/ml_server_info'; -interface ModelMemoryEstimationResult { +export interface ModelMemoryEstimationResult { /** * Result model memory limit */ @@ -29,15 +29,15 @@ interface ModelMemoryEstimationResult { /** * Response of the _estimate_model_memory endpoint. */ -export interface ModelMemoryEstimate { +export interface ModelMemoryEstimateResponse { model_memory_estimate: string; } /** * Retrieves overall and max bucket cardinalities. */ -const cardinalityCheckProvider = (mlClusterClient: ILegacyScopedClusterClient) => { - const fieldsService = fieldsServiceProvider(mlClusterClient); +const cardinalityCheckProvider = (client: IScopedClusterClient) => { + const fieldsService = fieldsServiceProvider(client); return async ( analysisConfig: AnalysisConfig, @@ -123,9 +123,9 @@ const cardinalityCheckProvider = (mlClusterClient: ILegacyScopedClusterClient) = }; }; -export function calculateModelMemoryLimitProvider(mlClusterClient: ILegacyScopedClusterClient) { - const { callAsInternalUser } = mlClusterClient; - const getCardinalities = cardinalityCheckProvider(mlClusterClient); +export function calculateModelMemoryLimitProvider(client: IScopedClusterClient) { + const { asInternalUser } = client; + const getCardinalities = cardinalityCheckProvider(client); /** * Retrieves an estimated size of the model memory limit used in the job config @@ -141,7 +141,7 @@ export function calculateModelMemoryLimitProvider(mlClusterClient: ILegacyScoped latestMs: number, allowMMLGreaterThanMax = false ): Promise { - const info = (await callAsInternalUser('ml.info')) as MlInfoResponse; + const { body: info } = await asInternalUser.ml.info(); const maxModelMemoryLimit = info.limits.max_model_memory_limit?.toUpperCase(); const effectiveMaxModelMemoryLimit = info.limits.effective_max_model_memory_limit?.toUpperCase(); @@ -154,13 +154,14 @@ export function calculateModelMemoryLimitProvider(mlClusterClient: ILegacyScoped latestMs ); - const estimatedModelMemoryLimit = ((await callAsInternalUser('ml.estimateModelMemory', { + const { body } = await asInternalUser.ml.estimateModelMemory({ body: { analysis_config: analysisConfig, overall_cardinality: overallCardinality, max_bucket_cardinality: maxBucketCardinality, }, - })) as ModelMemoryEstimate).model_memory_estimate.toUpperCase(); + }); + const estimatedModelMemoryLimit = body.model_memory_estimate.toUpperCase(); let modelMemoryLimit = estimatedModelMemoryLimit; let mmlCappedAtMax = false; diff --git a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts index b461e71c006ff0..95684e790636ff 100644 --- a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts @@ -5,7 +5,7 @@ */ import { difference } from 'lodash'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { EventManager, CalendarEvent } from './event_manager'; interface BasicCalendar { @@ -23,30 +23,30 @@ export interface FormCalendar extends BasicCalendar { } export class CalendarManager { - private _callAsInternalUser: ILegacyScopedClusterClient['callAsInternalUser']; + private _asInternalUser: IScopedClusterClient['asInternalUser']; private _eventManager: EventManager; - constructor(mlClusterClient: ILegacyScopedClusterClient) { - this._callAsInternalUser = mlClusterClient.callAsInternalUser; - this._eventManager = new EventManager(mlClusterClient); + constructor(client: IScopedClusterClient) { + this._asInternalUser = client.asInternalUser; + this._eventManager = new EventManager(client); } async getCalendar(calendarId: string) { - const resp = await this._callAsInternalUser('ml.calendars', { - calendarId, + const { body } = await this._asInternalUser.ml.getCalendars({ + calendar_id: calendarId, }); - const calendars = resp.calendars; + const calendars = body.calendars; const calendar = calendars[0]; // Endpoint throws a 404 if calendar is not found. calendar.events = await this._eventManager.getCalendarEvents(calendarId); return calendar; } async getAllCalendars() { - const calendarsResp = await this._callAsInternalUser('ml.calendars'); + const { body } = await this._asInternalUser.ml.getCalendars({ size: 1000 }); const events: CalendarEvent[] = await this._eventManager.getAllEvents(); - const calendars: Calendar[] = calendarsResp.calendars; + const calendars: Calendar[] = body.calendars; calendars.forEach((cal) => (cal.events = [])); // loop events and combine with related calendars @@ -71,8 +71,8 @@ export class CalendarManager { async newCalendar(calendar: FormCalendar) { const { calendarId, events, ...newCalendar } = calendar; - await this._callAsInternalUser('ml.addCalendar', { - calendarId, + await this._asInternalUser.ml.putCalendar({ + calendar_id: calendarId, body: newCalendar, }); @@ -106,17 +106,17 @@ export class CalendarManager { // add all new jobs if (jobsToAdd.length) { - await this._callAsInternalUser('ml.addJobToCalendar', { - calendarId, - jobId: jobsToAdd.join(','), + await this._asInternalUser.ml.putCalendarJob({ + calendar_id: calendarId, + job_id: jobsToAdd.join(','), }); } // remove all removed jobs if (jobsToRemove.length) { - await this._callAsInternalUser('ml.removeJobFromCalendar', { - calendarId, - jobId: jobsToRemove.join(','), + await this._asInternalUser.ml.deleteCalendarJob({ + calendar_id: calendarId, + job_id: jobsToRemove.join(','), }); } @@ -137,6 +137,7 @@ export class CalendarManager { } async deleteCalendar(calendarId: string) { - return this._callAsInternalUser('ml.deleteCalendar', { calendarId }); + const { body } = await this._asInternalUser.ml.deleteCalendar({ calendar_id: calendarId }); + return body; } } diff --git a/x-pack/plugins/ml/server/models/calendar/event_manager.ts b/x-pack/plugins/ml/server/models/calendar/event_manager.ts index b670bbe187544c..b42068e748394d 100644 --- a/x-pack/plugins/ml/server/models/calendar/event_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/event_manager.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; export interface CalendarEvent { @@ -16,39 +16,42 @@ export interface CalendarEvent { } export class EventManager { - private _callAsInternalUser: ILegacyScopedClusterClient['callAsInternalUser']; - constructor({ callAsInternalUser }: ILegacyScopedClusterClient) { - this._callAsInternalUser = callAsInternalUser; + private _asInternalUser: IScopedClusterClient['asInternalUser']; + constructor({ asInternalUser }: IScopedClusterClient) { + this._asInternalUser = asInternalUser; } async getCalendarEvents(calendarId: string) { - const resp = await this._callAsInternalUser('ml.events', { calendarId }); + const { body } = await this._asInternalUser.ml.getCalendarEvents({ calendar_id: calendarId }); - return resp.events; + return body.events; } // jobId is optional async getAllEvents(jobId?: string) { const calendarId = GLOBAL_CALENDAR; - const resp = await this._callAsInternalUser('ml.events', { - calendarId, - jobId, + const { body } = await this._asInternalUser.ml.getCalendarEvents({ + calendar_id: calendarId, + job_id: jobId, }); - return resp.events; + return body.events; } async addEvents(calendarId: string, events: CalendarEvent[]) { const body = { events }; - return await this._callAsInternalUser('ml.addEvent', { - calendarId, + return await this._asInternalUser.ml.postCalendarEvents({ + calendar_id: calendarId, body, }); } async deleteEvent(calendarId: string, eventId: string) { - return this._callAsInternalUser('ml.deleteEvent', { calendarId, eventId }); + return this._asInternalUser.ml.deleteCalendarEvent({ + calendar_id: calendarId, + event_id: eventId, + }); } isEqual(ev1: CalendarEvent, ev2: CalendarEvent) { diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts index 1cb0656e88a0bb..0f4cac37d2e8f8 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { ML_NOTIFICATION_INDEX_PATTERN } from '../../../common/constants/index_patterns'; import { JobMessage } from '../../../common/types/audit_message'; @@ -23,7 +23,7 @@ interface BoolQuery { bool: { [key: string]: any }; } -export function analyticsAuditMessagesProvider({ callAsInternalUser }: ILegacyScopedClusterClient) { +export function analyticsAuditMessagesProvider({ asInternalUser }: IScopedClusterClient) { // search for audit messages, // analyticsId is optional. without it, all analytics will be listed. async function getAnalyticsAuditMessages(analyticsId: string) { @@ -68,27 +68,23 @@ export function analyticsAuditMessagesProvider({ callAsInternalUser }: ILegacySc }); } - try { - const resp = await callAsInternalUser('search', { - index: ML_NOTIFICATION_INDEX_PATTERN, - ignore_unavailable: true, - rest_total_hits_as_int: true, - size: SIZE, - body: { - sort: [{ timestamp: { order: 'desc' } }, { job_id: { order: 'asc' } }], - query, - }, - }); + const { body } = await asInternalUser.search({ + index: ML_NOTIFICATION_INDEX_PATTERN, + ignore_unavailable: true, + rest_total_hits_as_int: true, + size: SIZE, + body: { + sort: [{ timestamp: { order: 'desc' } }, { job_id: { order: 'asc' } }], + query, + }, + }); - let messages = []; - if (resp.hits.total !== 0) { - messages = resp.hits.hits.map((hit: Message) => hit._source); - messages.reverse(); - } - return messages; - } catch (e) { - throw e; + let messages = []; + if (body.hits.total !== 0) { + messages = body.hits.hits.map((hit: Message) => hit._source); + messages.reverse(); } + return messages; } return { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts index 82d7707464308a..ab879d4c93b292 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts @@ -4,13 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract, KibanaRequest } from 'kibana/server'; +import { SavedObjectsClientContract, KibanaRequest, IScopedClusterClient } from 'kibana/server'; import { Module } from '../../../common/types/modules'; import { DataRecognizer } from '../data_recognizer'; +const callAs = () => Promise.resolve({ body: {} }); + +const mlClusterClient = ({ + asCurrentUser: callAs, + asInternalUser: callAs, +} as unknown) as IScopedClusterClient; + describe('ML - data recognizer', () => { const dr = new DataRecognizer( - { callAsCurrentUser: jest.fn(), callAsInternalUser: jest.fn() }, + mlClusterClient, ({ find: jest.fn(), bulkCreate: jest.fn(), diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 206baacd983223..820fcfa9253b63 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -7,15 +7,11 @@ import fs from 'fs'; import Boom from 'boom'; import numeral from '@elastic/numeral'; -import { - KibanaRequest, - ILegacyScopedClusterClient, - SavedObjectsClientContract, -} from 'kibana/server'; +import { KibanaRequest, IScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; import moment from 'moment'; import { IndexPatternAttributes } from 'src/plugins/data/server'; import { merge } from 'lodash'; -import { AnalysisLimits, CombinedJobWithStats } from '../../../common/types/anomaly_detection_jobs'; +import { AnalysisLimits } from '../../../common/types/anomaly_detection_jobs'; import { getAuthorizationHeader } from '../../lib/request_authorization'; import { MlInfoResponse } from '../../../common/types/ml_server_info'; import { @@ -46,6 +42,7 @@ import { fieldsServiceProvider } from '../fields_service'; import { jobServiceProvider } from '../job_service'; import { resultsServiceProvider } from '../results_service'; import { JobExistResult, JobStat } from '../../../common/types/data_recognizer'; +import { MlJobsStatsResponse } from '../job_service/jobs'; const ML_DIR = 'ml'; const KIBANA_DIR = 'kibana'; @@ -74,10 +71,6 @@ interface RawModuleConfig { }; } -interface MlJobStats { - jobs: CombinedJobWithStats[]; -} - interface Config { dirName: any; json: RawModuleConfig; @@ -111,9 +104,9 @@ interface SaveResults { } export class DataRecognizer { - private _callAsCurrentUser: ILegacyScopedClusterClient['callAsCurrentUser']; - private _callAsInternalUser: ILegacyScopedClusterClient['callAsInternalUser']; - private _mlClusterClient: ILegacyScopedClusterClient; + private _asCurrentUser: IScopedClusterClient['asCurrentUser']; + private _asInternalUser: IScopedClusterClient['asInternalUser']; + private _client: IScopedClusterClient; private _authorizationHeader: object; private _modulesDir = `${__dirname}/modules`; private _indexPatternName: string = ''; @@ -124,13 +117,13 @@ export class DataRecognizer { jobsForModelMemoryEstimation: Array<{ job: ModuleJob; query: any }> = []; constructor( - mlClusterClient: ILegacyScopedClusterClient, + mlClusterClient: IScopedClusterClient, private savedObjectsClient: SavedObjectsClientContract, request: KibanaRequest ) { - this._mlClusterClient = mlClusterClient; - this._callAsCurrentUser = mlClusterClient.callAsCurrentUser; - this._callAsInternalUser = mlClusterClient.callAsInternalUser; + this._client = mlClusterClient; + this._asCurrentUser = mlClusterClient.asCurrentUser; + this._asInternalUser = mlClusterClient.asInternalUser; this._authorizationHeader = getAuthorizationHeader(request); } @@ -249,18 +242,18 @@ export class DataRecognizer { const index = indexPattern; const size = 0; - const body = { + const searchBody = { query: moduleConfig.query, }; - const resp = await this._callAsCurrentUser('search', { + const { body } = await this._asCurrentUser.search({ index, rest_total_hits_as_int: true, size, - body, + body: searchBody, }); - return resp.hits.total !== 0; + return body.hits.total !== 0; } async listModules() { @@ -518,7 +511,7 @@ export class DataRecognizer { // Add a wildcard at the front of each of the job IDs in the module, // as a prefix may have been supplied when creating the jobs in the module. const jobIds = module.jobs.map((job) => `*${job.id}`); - const { jobsExist } = jobServiceProvider(this._mlClusterClient); + const { jobsExist } = jobServiceProvider(this._client); const jobInfo = await jobsExist(jobIds); // Check if the value for any of the jobs is false. @@ -527,16 +520,16 @@ export class DataRecognizer { if (doJobsExist === true) { // Get the IDs of the jobs created from the module, and their earliest / latest timestamps. - const jobStats: MlJobStats = await this._callAsInternalUser('ml.jobStats', { - jobId: jobIds, + const { body } = await this._asInternalUser.ml.getJobStats({ + job_id: jobIds.join(), }); const jobStatsJobs: JobStat[] = []; - if (jobStats.jobs && jobStats.jobs.length > 0) { - const foundJobIds = jobStats.jobs.map((job) => job.job_id); - const { getLatestBucketTimestampByJob } = resultsServiceProvider(this._mlClusterClient); + if (body.jobs && body.jobs.length > 0) { + const foundJobIds = body.jobs.map((job) => job.job_id); + const { getLatestBucketTimestampByJob } = resultsServiceProvider(this._client); const latestBucketTimestampsByJob = await getLatestBucketTimestampByJob(foundJobIds); - jobStats.jobs.forEach((job) => { + body.jobs.forEach((job) => { const jobStat = { id: job.job_id, } as JobStat; @@ -704,16 +697,15 @@ export class DataRecognizer { job.id = jobId; await this.saveJob(job); return { id: jobId, success: true }; - } catch (error) { - return { id: jobId, success: false, error }; + } catch ({ body }) { + return { id: jobId, success: false, error: body }; } }) ); } async saveJob(job: ModuleJob) { - const { id: jobId, config: body } = job; - return this._callAsInternalUser('ml.addJob', { jobId, body }); + return this._asInternalUser.ml.putJob({ job_id: job.id, body: job.config }); } // save the datafeeds. @@ -725,20 +717,21 @@ export class DataRecognizer { try { await this.saveDatafeed(datafeed); return { id: datafeed.id, success: true, started: false }; - } catch (error) { - return { id: datafeed.id, success: false, started: false, error }; + } catch ({ body }) { + return { id: datafeed.id, success: false, started: false, error: body }; } }) ); } async saveDatafeed(datafeed: ModuleDataFeed) { - const { id: datafeedId, config: body } = datafeed; - return this._callAsInternalUser('ml.addDatafeed', { - datafeedId, - body, - ...this._authorizationHeader, - }); + return this._asInternalUser.ml.putDatafeed( + { + datafeed_id: datafeed.id, + body: datafeed.config, + }, + this._authorizationHeader + ); } async startDatafeeds( @@ -761,10 +754,10 @@ export class DataRecognizer { const result = { started: false } as DatafeedResponse; let opened = false; try { - const openResult = await this._callAsInternalUser('ml.openJob', { - jobId: datafeed.config.job_id, + const { body } = await this._asInternalUser.ml.openJob({ + job_id: datafeed.config.job_id, }); - opened = openResult.opened; + opened = body.opened; } catch (error) { // if the job is already open, a 409 will be returned. if (error.statusCode === 409) { @@ -772,27 +765,27 @@ export class DataRecognizer { } else { opened = false; result.started = false; - result.error = error; + result.error = error.body; } } if (opened) { try { - const duration: { start: number; end?: number } = { start: 0 }; + const duration: { start: string; end?: string } = { start: '0' }; if (start !== undefined) { - duration.start = start; + duration.start = (start as unknown) as string; } if (end !== undefined) { - duration.end = end; + duration.end = (end as unknown) as string; } - await this._callAsInternalUser('ml.startDatafeed', { - datafeedId: datafeed.id, + await this._asInternalUser.ml.startDatafeed({ + datafeed_id: datafeed.id, ...duration, }); result.started = true; - } catch (error) { + } catch ({ body }) { result.started = false; - result.error = error; + result.error = body; } } return result; @@ -995,7 +988,7 @@ export class DataRecognizer { timeField: string, query?: any ): Promise<{ start: number; end: number }> { - const fieldsService = fieldsServiceProvider(this._mlClusterClient); + const fieldsService = fieldsServiceProvider(this._client); const timeFieldRange = await fieldsService.getTimeFieldRange( this._indexPatternName, @@ -1025,7 +1018,7 @@ export class DataRecognizer { if (estimateMML && this.jobsForModelMemoryEstimation.length > 0) { try { - const calculateModelMemoryLimit = calculateModelMemoryLimitProvider(this._mlClusterClient); + const calculateModelMemoryLimit = calculateModelMemoryLimitProvider(this._client); // Checks if all jobs in the module have the same time field configured const firstJobTimeField = this.jobsForModelMemoryEstimation[0].job.config.data_description @@ -1074,11 +1067,13 @@ export class DataRecognizer { job.config.analysis_limits.model_memory_limit = modelMemoryLimit; } } catch (error) { - mlLog.warn(`Data recognizer could not estimate model memory limit ${error}`); + mlLog.warn(`Data recognizer could not estimate model memory limit ${error.body}`); } } - const { limits } = (await this._callAsInternalUser('ml.info')) as MlInfoResponse; + const { + body: { limits }, + } = await this._asInternalUser.ml.info(); const maxMml = limits.max_model_memory_limit; if (!maxMml) { diff --git a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts index 838315d8d272ca..dbfa4b5656e5f9 100644 --- a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import get from 'lodash/get'; import each from 'lodash/each'; import last from 'lodash/last'; @@ -183,7 +183,7 @@ type BatchStats = | FieldExamples; const getAggIntervals = async ( - { callAsCurrentUser }: ILegacyScopedClusterClient, + { asCurrentUser }: IScopedClusterClient, indexPatternTitle: string, query: any, fields: HistogramField[], @@ -207,7 +207,7 @@ const getAggIntervals = async ( return aggs; }, {} as Record); - const respStats = await callAsCurrentUser('search', { + const { body } = await asCurrentUser.search({ index: indexPatternTitle, size: 0, body: { @@ -218,8 +218,7 @@ const getAggIntervals = async ( }); const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); - const aggregations = - aggsPath.length > 0 ? get(respStats.aggregations, aggsPath) : respStats.aggregations; + const aggregations = aggsPath.length > 0 ? get(body.aggregations, aggsPath) : body.aggregations; return Object.keys(aggregations).reduce((p, aggName) => { const stats = [aggregations[aggName].min, aggregations[aggName].max]; @@ -241,15 +240,15 @@ const getAggIntervals = async ( // export for re-use by transforms plugin export const getHistogramsForFields = async ( - mlClusterClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, indexPatternTitle: string, query: any, fields: HistogramField[], samplerShardSize: number ) => { - const { callAsCurrentUser } = mlClusterClient; + const { asCurrentUser } = client; const aggIntervals = await getAggIntervals( - mlClusterClient, + client, indexPatternTitle, query, fields, @@ -291,7 +290,7 @@ export const getHistogramsForFields = async ( return []; } - const respChartsData = await callAsCurrentUser('search', { + const { body } = await asCurrentUser.search({ index: indexPatternTitle, size: 0, body: { @@ -302,8 +301,7 @@ export const getHistogramsForFields = async ( }); const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); - const aggregations = - aggsPath.length > 0 ? get(respChartsData.aggregations, aggsPath) : respChartsData.aggregations; + const aggregations = aggsPath.length > 0 ? get(body.aggregations, aggsPath) : body.aggregations; const chartsData: ChartData[] = fields.map( (field): ChartData => { @@ -350,12 +348,12 @@ export const getHistogramsForFields = async ( }; export class DataVisualizer { - private _mlClusterClient: ILegacyScopedClusterClient; - private _callAsCurrentUser: ILegacyScopedClusterClient['callAsCurrentUser']; + private _client: IScopedClusterClient; + private _asCurrentUser: IScopedClusterClient['asCurrentUser']; - constructor(mlClusterClient: ILegacyScopedClusterClient) { - this._callAsCurrentUser = mlClusterClient.callAsCurrentUser; - this._mlClusterClient = mlClusterClient; + constructor(client: IScopedClusterClient) { + this._asCurrentUser = client.asCurrentUser; + this._client = client; } // Obtains overall stats on the fields in the supplied index pattern, returning an object @@ -451,7 +449,7 @@ export class DataVisualizer { samplerShardSize: number ): Promise { return await getHistogramsForFields( - this._mlClusterClient, + this._client, indexPatternTitle, query, fields, @@ -621,7 +619,7 @@ export class DataVisualizer { }; }); - const body = { + const searchBody = { query: { bool: { filter: filterCriteria, @@ -630,14 +628,14 @@ export class DataVisualizer { aggs: buildSamplerAggregation(aggs, samplerShardSize), }; - const resp = await this._callAsCurrentUser('search', { + const { body } = await this._asCurrentUser.search({ index, rest_total_hits_as_int: true, size, - body, + body: searchBody, }); - const aggregations = resp.aggregations; - const totalCount = get(resp, ['hits', 'total'], 0); + const aggregations = body.aggregations; + const totalCount = get(body, ['hits', 'total'], 0); const stats = { totalCount, aggregatableExistsFields: [] as FieldData[], @@ -688,7 +686,7 @@ export class DataVisualizer { const size = 0; const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); - const body = { + const searchBody = { query: { bool: { filter: filterCriteria, @@ -697,13 +695,13 @@ export class DataVisualizer { }; filterCriteria.push({ exists: { field } }); - const resp = await this._callAsCurrentUser('search', { + const { body } = await this._asCurrentUser.search({ index, rest_total_hits_as_int: true, size, - body, + body: searchBody, }); - return resp.hits.total > 0; + return body.hits.total > 0; } async getDocumentCountStats( @@ -730,7 +728,7 @@ export class DataVisualizer { }, }; - const body = { + const searchBody = { query: { bool: { filter: filterCriteria, @@ -739,15 +737,15 @@ export class DataVisualizer { aggs, }; - const resp = await this._callAsCurrentUser('search', { + const { body } = await this._asCurrentUser.search({ index, size, - body, + body: searchBody, }); const buckets: { [key: string]: number } = {}; const dataByTimeBucket: Array<{ key: string; doc_count: number }> = get( - resp, + body, ['aggregations', 'eventRate', 'buckets'], [] ); @@ -833,7 +831,7 @@ export class DataVisualizer { } }); - const body = { + const searchBody = { query: { bool: { filter: filterCriteria, @@ -842,12 +840,12 @@ export class DataVisualizer { aggs: buildSamplerAggregation(aggs, samplerShardSize), }; - const resp = await this._callAsCurrentUser('search', { + const { body } = await this._asCurrentUser.search({ index, size, - body, + body: searchBody, }); - const aggregations = resp.aggregations; + const aggregations = body.aggregations; const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); const batchStats: NumericFieldStats[] = []; fields.forEach((field, i) => { @@ -954,7 +952,7 @@ export class DataVisualizer { } }); - const body = { + const searchBody = { query: { bool: { filter: filterCriteria, @@ -963,12 +961,12 @@ export class DataVisualizer { aggs: buildSamplerAggregation(aggs, samplerShardSize), }; - const resp = await this._callAsCurrentUser('search', { + const { body } = await this._asCurrentUser.search({ index, size, - body, + body: searchBody, }); - const aggregations = resp.aggregations; + const aggregations = body.aggregations; const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); const batchStats: StringFieldStats[] = []; fields.forEach((field, i) => { @@ -1028,7 +1026,7 @@ export class DataVisualizer { }; }); - const body = { + const searchBody = { query: { bool: { filter: filterCriteria, @@ -1037,12 +1035,12 @@ export class DataVisualizer { aggs: buildSamplerAggregation(aggs, samplerShardSize), }; - const resp = await this._callAsCurrentUser('search', { + const { body } = await this._asCurrentUser.search({ index, size, - body, + body: searchBody, }); - const aggregations = resp.aggregations; + const aggregations = body.aggregations; const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); const batchStats: DateFieldStats[] = []; fields.forEach((field, i) => { @@ -1095,7 +1093,7 @@ export class DataVisualizer { }; }); - const body = { + const searchBody = { query: { bool: { filter: filterCriteria, @@ -1104,12 +1102,12 @@ export class DataVisualizer { aggs: buildSamplerAggregation(aggs, samplerShardSize), }; - const resp = await this._callAsCurrentUser('search', { + const { body } = await this._asCurrentUser.search({ index, size, - body, + body: searchBody, }); - const aggregations = resp.aggregations; + const aggregations = body.aggregations; const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); const batchStats: BooleanFieldStats[] = []; fields.forEach((field, i) => { @@ -1157,7 +1155,7 @@ export class DataVisualizer { exists: { field }, }); - const body = { + const searchBody = { _source: field, query: { bool: { @@ -1166,18 +1164,18 @@ export class DataVisualizer { }, }; - const resp = await this._callAsCurrentUser('search', { + const { body } = await this._asCurrentUser.search({ index, rest_total_hits_as_int: true, size, - body, + body: searchBody, }); const stats = { fieldName: field, examples: [] as any[], }; - if (resp.hits.total !== 0) { - const hits = resp.hits.hits; + if (body.hits.total !== 0) { + const hits = body.hits.hits; for (let i = 0; i < hits.length; i++) { // Look in the _source for the field value. // If the field is not in the _source (as will happen if the diff --git a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts index 43a6876f76c49e..68427e98750ebb 100644 --- a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts +++ b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { duration } from 'moment'; import { parseInterval } from '../../../common/util/parse_interval'; import { initCardinalityFieldsCache } from './fields_aggs_cache'; @@ -14,7 +14,7 @@ import { initCardinalityFieldsCache } from './fields_aggs_cache'; * Service for carrying out queries to obtain data * specific to fields in Elasticsearch indices. */ -export function fieldsServiceProvider({ callAsCurrentUser }: ILegacyScopedClusterClient) { +export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) { const fieldsAggsCache = initCardinalityFieldsCache(); /** @@ -37,13 +37,13 @@ export function fieldsServiceProvider({ callAsCurrentUser }: ILegacyScopedCluste index: string | string[], fieldNames: string[] ): Promise { - const fieldCapsResp = await callAsCurrentUser('fieldCaps', { + const { body } = await asCurrentUser.fieldCaps({ index, fields: fieldNames, }); const aggregatableFields: string[] = []; fieldNames.forEach((fieldName) => { - const fieldInfo = fieldCapsResp.fields[fieldName]; + const fieldInfo = body.fields[fieldName]; const typeKeys = fieldInfo !== undefined ? Object.keys(fieldInfo) : []; if (typeKeys.length > 0) { const fieldType = typeKeys[0]; @@ -130,12 +130,12 @@ export function fieldsServiceProvider({ callAsCurrentUser }: ILegacyScopedCluste aggs, }; - const aggregations = ( - await callAsCurrentUser('search', { - index, - body, - }) - )?.aggregations; + const { + body: { aggregations }, + } = await asCurrentUser.search({ + index, + body, + }); if (!aggregations) { return {}; @@ -170,7 +170,9 @@ export function fieldsServiceProvider({ callAsCurrentUser }: ILegacyScopedCluste }> { const obj = { success: true, start: { epoch: 0, string: '' }, end: { epoch: 0, string: '' } }; - const resp = await callAsCurrentUser('search', { + const { + body: { aggregations }, + } = await asCurrentUser.search({ index, size: 0, body: { @@ -190,12 +192,12 @@ export function fieldsServiceProvider({ callAsCurrentUser }: ILegacyScopedCluste }, }); - if (resp.aggregations && resp.aggregations.earliest && resp.aggregations.latest) { - obj.start.epoch = resp.aggregations.earliest.value; - obj.start.string = resp.aggregations.earliest.value_as_string; + if (aggregations && aggregations.earliest && aggregations.latest) { + obj.start.epoch = aggregations.earliest.value; + obj.start.string = aggregations.earliest.value_as_string; - obj.end.epoch = resp.aggregations.latest.value; - obj.end.string = resp.aggregations.latest.value_as_string; + obj.end.epoch = aggregations.latest.value; + obj.end.string = aggregations.latest.value_as_string; } return obj; } @@ -338,12 +340,12 @@ export function fieldsServiceProvider({ callAsCurrentUser }: ILegacyScopedCluste }, }; - const aggregations = ( - await callAsCurrentUser('search', { - index, - body, - }) - )?.aggregations; + const { + body: { aggregations }, + } = await asCurrentUser.search({ + index, + body, + }); if (!aggregations) { return cachedValues; diff --git a/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts b/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts index 9cd71c046b66c7..9e502c04fbb7ba 100644 --- a/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts @@ -4,18 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { AnalysisResult, FormattedOverrides, InputOverrides, + FindFileStructureResponse, } from '../../../common/types/file_datavisualizer'; export type InputData = any[]; -export function fileDataVisualizerProvider({ callAsInternalUser }: ILegacyScopedClusterClient) { - async function analyzeFile(data: any, overrides: any): Promise { - const results = await callAsInternalUser('ml.fileStructure', { +export function fileDataVisualizerProvider({ asInternalUser }: IScopedClusterClient) { + async function analyzeFile(data: InputData, overrides: InputOverrides): Promise { + overrides.explain = overrides.explain === undefined ? 'true' : overrides.explain; + const { body } = await asInternalUser.ml.findFileStructure({ body: data, ...overrides, }); @@ -24,7 +26,7 @@ export function fileDataVisualizerProvider({ callAsInternalUser }: ILegacyScoped return { ...(hasOverrides && { overrides: reducedOverrides }), - results, + results: body, }; } diff --git a/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts b/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts index fc9b333298c9dd..6108454c08aa75 100644 --- a/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts +++ b/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { INDEX_META_DATA_CREATED_BY } from '../../../common/constants/file_datavisualizer'; import { ImportResponse, @@ -15,7 +15,7 @@ import { } from '../../../common/types/file_datavisualizer'; import { InputData } from './file_data_visualizer'; -export function importDataProvider({ callAsCurrentUser }: ILegacyScopedClusterClient) { +export function importDataProvider({ asCurrentUser }: IScopedClusterClient) { async function importData( id: string, index: string, @@ -40,9 +40,9 @@ export function importDataProvider({ callAsCurrentUser }: ILegacyScopedClusterCl // create the pipeline if one has been supplied if (pipelineId !== undefined) { - const success = await createPipeline(pipelineId, pipeline); - if (success.acknowledged !== true) { - throw success; + const resp = await createPipeline(pipelineId, pipeline); + if (resp.acknowledged !== true) { + throw resp; } } createdPipelineId = pipelineId; @@ -80,7 +80,7 @@ export function importDataProvider({ callAsCurrentUser }: ILegacyScopedClusterCl id, index: createdIndex, pipelineId: createdPipelineId, - error: error.error !== undefined ? error.error : error, + error: error.body !== undefined ? error.body : error, docCount, ingestError: error.ingestError, failures: error.failures || [], @@ -102,7 +102,7 @@ export function importDataProvider({ callAsCurrentUser }: ILegacyScopedClusterCl body.settings = settings; } - await callAsCurrentUser('indices.create', { index, body }); + await asCurrentUser.indices.create({ index, body }); } async function indexData(index: string, pipelineId: string, data: InputData) { @@ -118,7 +118,7 @@ export function importDataProvider({ callAsCurrentUser }: ILegacyScopedClusterCl settings.pipeline = pipelineId; } - const resp = await callAsCurrentUser('bulk', settings); + const { body: resp } = await asCurrentUser.bulk(settings); if (resp.errors) { throw resp; } else { @@ -151,7 +151,8 @@ export function importDataProvider({ callAsCurrentUser }: ILegacyScopedClusterCl } async function createPipeline(id: string, pipeline: any) { - return await callAsCurrentUser('ingest.putPipeline', { id, body: pipeline }); + const { body } = await asCurrentUser.ingest.putPipeline({ id, body: pipeline }); + return body; } function getFailures(items: any[], data: InputData): ImportFailure[] { diff --git a/x-pack/plugins/ml/server/models/filter/filter_manager.ts b/x-pack/plugins/ml/server/models/filter/filter_manager.ts index 768ca1f893b68a..19ba1b76f8a600 100644 --- a/x-pack/plugins/ml/server/models/filter/filter_manager.ts +++ b/x-pack/plugins/ml/server/models/filter/filter_manager.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { DetectorRule, DetectorRuleScope } from '../../../common/types/detector_rules'; @@ -58,26 +58,26 @@ interface PartialJob { } export class FilterManager { - private _callAsInternalUser: ILegacyScopedClusterClient['callAsInternalUser']; - constructor({ callAsInternalUser }: ILegacyScopedClusterClient) { - this._callAsInternalUser = callAsInternalUser; + private _asInternalUser: IScopedClusterClient['asInternalUser']; + constructor({ asInternalUser }: IScopedClusterClient) { + this._asInternalUser = asInternalUser; } async getFilter(filterId: string) { try { const [JOBS, FILTERS] = [0, 1]; const results = await Promise.all([ - this._callAsInternalUser('ml.jobs'), - this._callAsInternalUser('ml.filters', { filterId }), + this._asInternalUser.ml.getJobs(), + this._asInternalUser.ml.getFilters({ filter_id: filterId }), ]); - if (results[FILTERS] && results[FILTERS].filters.length) { + if (results[FILTERS] && results[FILTERS].body.filters.length) { let filtersInUse: FiltersInUse = {}; - if (results[JOBS] && results[JOBS].jobs) { - filtersInUse = this.buildFiltersInUse(results[JOBS].jobs); + if (results[JOBS] && results[JOBS].body.jobs) { + filtersInUse = this.buildFiltersInUse(results[JOBS].body.jobs); } - const filter = results[FILTERS].filters[0]; + const filter = results[FILTERS].body.filters[0]; filter.used_by = filtersInUse[filter.filter_id]; return filter; } else { @@ -90,8 +90,8 @@ export class FilterManager { async getAllFilters() { try { - const filtersResp = await this._callAsInternalUser('ml.filters'); - return filtersResp.filters; + const { body } = await this._asInternalUser.ml.getFilters({ size: 1000 }); + return body.filters; } catch (error) { throw Boom.badRequest(error); } @@ -101,14 +101,14 @@ export class FilterManager { try { const [JOBS, FILTERS] = [0, 1]; const results = await Promise.all([ - this._callAsInternalUser('ml.jobs'), - this._callAsInternalUser('ml.filters'), + this._asInternalUser.ml.getJobs(), + this._asInternalUser.ml.getFilters({ size: 1000 }), ]); // Build a map of filter_ids against jobs and detectors using that filter. let filtersInUse: FiltersInUse = {}; - if (results[JOBS] && results[JOBS].jobs) { - filtersInUse = this.buildFiltersInUse(results[JOBS].jobs); + if (results[JOBS] && results[JOBS].body.jobs) { + filtersInUse = this.buildFiltersInUse(results[JOBS].body.jobs); } // For each filter, return just @@ -117,8 +117,8 @@ export class FilterManager { // item_count // jobs using the filter const filterStats: FilterStats[] = []; - if (results[FILTERS] && results[FILTERS].filters) { - results[FILTERS].filters.forEach((filter: Filter) => { + if (results[FILTERS] && results[FILTERS].body.filters) { + results[FILTERS].body.filters.forEach((filter: Filter) => { const stats: FilterStats = { filter_id: filter.filter_id, description: filter.description, @@ -139,7 +139,8 @@ export class FilterManager { const { filterId, ...body } = filter; try { // Returns the newly created filter. - return await this._callAsInternalUser('ml.addFilter', { filterId, body }); + const { body: resp } = await this._asInternalUser.ml.putFilter({ filter_id: filterId, body }); + return resp; } catch (error) { throw Boom.badRequest(error); } @@ -159,17 +160,19 @@ export class FilterManager { } // Returns the newly updated filter. - return await this._callAsInternalUser('ml.updateFilter', { - filterId, + const { body: resp } = await this._asInternalUser.ml.updateFilter({ + filter_id: filterId, body, }); + return resp; } catch (error) { throw Boom.badRequest(error); } } async deleteFilter(filterId: string) { - return this._callAsInternalUser('ml.deleteFilter', { filterId }); + const { body } = await this._asInternalUser.ml.deleteFilter({ filter_id: filterId }); + return body; } buildFiltersInUse(jobsList: PartialJob[]) { diff --git a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts index d72552b548b827..afdd3e9bb8ce90 100644 --- a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts +++ b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; export function jobAuditMessagesProvider( - mlClusterClient: ILegacyScopedClusterClient + client: IScopedClusterClient ): { getJobAuditMessages: (jobId?: string, from?: string) => any; getAuditMessagesSummary: (jobIds?: string[]) => any; diff --git a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js index 86d80c394137f8..3fd5ebf3f68f40 100644 --- a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js +++ b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js @@ -34,14 +34,14 @@ const anomalyDetectorTypeFilter = { }, }; -export function jobAuditMessagesProvider({ callAsInternalUser }) { +export function jobAuditMessagesProvider({ asInternalUser }) { // search for audit messages, // jobId is optional. without it, all jobs will be listed. // from is optional and should be a string formatted in ES time units. e.g. 12h, 1d, 7d async function getJobAuditMessages(jobId, from) { let gte = null; if (jobId !== undefined && from === undefined) { - const jobs = await callAsInternalUser('ml.jobs', { jobId }); + const jobs = await asInternalUser.ml.getJobs({ job_id: jobId }); if (jobs.count > 0 && jobs.jobs !== undefined) { gte = moment(jobs.jobs[0].create_time).valueOf(); } @@ -99,26 +99,22 @@ export function jobAuditMessagesProvider({ callAsInternalUser }) { }); } - try { - const resp = await callAsInternalUser('search', { - index: ML_NOTIFICATION_INDEX_PATTERN, - ignore_unavailable: true, - rest_total_hits_as_int: true, - size: SIZE, - body: { - sort: [{ timestamp: { order: 'desc' } }, { job_id: { order: 'asc' } }], - query, - }, - }); + const { body } = await asInternalUser.search({ + index: ML_NOTIFICATION_INDEX_PATTERN, + ignore_unavailable: true, + rest_total_hits_as_int: true, + size: SIZE, + body: { + sort: [{ timestamp: { order: 'desc' } }, { job_id: { order: 'asc' } }], + query, + }, + }); - let messages = []; - if (resp.hits.total !== 0) { - messages = resp.hits.hits.map((hit) => hit._source); - } - return messages; - } catch (e) { - throw e; + let messages = []; + if (body.hits.total !== 0) { + messages = body.hits.hits.map((hit) => hit._source); } + return messages; } // search highest, most recent audit messages for all jobs for the last 24hrs. @@ -128,65 +124,63 @@ export function jobAuditMessagesProvider({ callAsInternalUser }) { const maxBuckets = 10000; let levelsPerJobAggSize = maxBuckets; - try { - const query = { - bool: { - filter: [ - { - range: { - timestamp: { - gte: 'now-1d', - }, + const query = { + bool: { + filter: [ + { + range: { + timestamp: { + gte: 'now-1d', }, }, - anomalyDetectorTypeFilter, - ], - }, - }; - - // If the jobIds arg is supplied, add a query filter - // to only include those jobIds in the aggregations. - if (Array.isArray(jobIds) && jobIds.length > 0) { - query.bool.filter.push({ - terms: { - job_id: jobIds, }, - }); - levelsPerJobAggSize = jobIds.length; - } + anomalyDetectorTypeFilter, + ], + }, + }; - const resp = await callAsInternalUser('search', { - index: ML_NOTIFICATION_INDEX_PATTERN, - ignore_unavailable: true, - rest_total_hits_as_int: true, - size: 0, - body: { - query, - aggs: { - levelsPerJob: { - terms: { - field: 'job_id', - size: levelsPerJobAggSize, - }, - aggs: { - levels: { - terms: { - field: 'level', - }, - aggs: { - latestMessage: { - terms: { - field: 'message.raw', - size: 1, - order: { - latestMessage: 'desc', - }, + // If the jobIds arg is supplied, add a query filter + // to only include those jobIds in the aggregations. + if (Array.isArray(jobIds) && jobIds.length > 0) { + query.bool.filter.push({ + terms: { + job_id: jobIds, + }, + }); + levelsPerJobAggSize = jobIds.length; + } + + const { body } = await asInternalUser.search({ + index: ML_NOTIFICATION_INDEX_PATTERN, + ignore_unavailable: true, + rest_total_hits_as_int: true, + size: 0, + body: { + query, + aggs: { + levelsPerJob: { + terms: { + field: 'job_id', + size: levelsPerJobAggSize, + }, + aggs: { + levels: { + terms: { + field: 'level', + }, + aggs: { + latestMessage: { + terms: { + field: 'message.raw', + size: 1, + order: { + latestMessage: 'desc', }, - aggs: { - latestMessage: { - max: { - field: 'timestamp', - }, + }, + aggs: { + latestMessage: { + max: { + field: 'timestamp', }, }, }, @@ -196,67 +190,65 @@ export function jobAuditMessagesProvider({ callAsInternalUser }) { }, }, }, - }); + }, + }); - let messagesPerJob = []; - const jobMessages = []; - if ( - resp.hits.total !== 0 && - resp.aggregations && - resp.aggregations.levelsPerJob && - resp.aggregations.levelsPerJob.buckets && - resp.aggregations.levelsPerJob.buckets.length - ) { - messagesPerJob = resp.aggregations.levelsPerJob.buckets; - } + let messagesPerJob = []; + const jobMessages = []; + if ( + body.hits.total !== 0 && + body.aggregations && + body.aggregations.levelsPerJob && + body.aggregations.levelsPerJob.buckets && + body.aggregations.levelsPerJob.buckets.length + ) { + messagesPerJob = body.aggregations.levelsPerJob.buckets; + } - messagesPerJob.forEach((job) => { - // ignore system messages (id==='') - if (job.key !== '' && job.levels && job.levels.buckets && job.levels.buckets.length) { - let highestLevel = 0; - let highestLevelText = ''; - let msgTime = 0; + messagesPerJob.forEach((job) => { + // ignore system messages (id==='') + if (job.key !== '' && job.levels && job.levels.buckets && job.levels.buckets.length) { + let highestLevel = 0; + let highestLevelText = ''; + let msgTime = 0; - job.levels.buckets.forEach((level) => { - const label = level.key; - // note the highest message level - if (LEVEL[label] > highestLevel) { - highestLevel = LEVEL[label]; - if ( - level.latestMessage && - level.latestMessage.buckets && - level.latestMessage.buckets.length - ) { - level.latestMessage.buckets.forEach((msg) => { - // there should only be one result here. - highestLevelText = msg.key; + job.levels.buckets.forEach((level) => { + const label = level.key; + // note the highest message level + if (LEVEL[label] > highestLevel) { + highestLevel = LEVEL[label]; + if ( + level.latestMessage && + level.latestMessage.buckets && + level.latestMessage.buckets.length + ) { + level.latestMessage.buckets.forEach((msg) => { + // there should only be one result here. + highestLevelText = msg.key; - // note the time in ms for the highest level - // so we can filter them out later if they're earlier than the - // job's create time. - if (msg.latestMessage && msg.latestMessage.value_as_string) { - const time = moment(msg.latestMessage.value_as_string); - msgTime = time.valueOf(); - } - }); - } + // note the time in ms for the highest level + // so we can filter them out later if they're earlier than the + // job's create time. + if (msg.latestMessage && msg.latestMessage.value_as_string) { + const time = moment(msg.latestMessage.value_as_string); + msgTime = time.valueOf(); + } + }); } - }); - - if (msgTime !== 0 && highestLevel !== 0) { - jobMessages.push({ - job_id: job.key, - highestLevelText, - highestLevel: levelToText(highestLevel), - msgTime, - }); } + }); + + if (msgTime !== 0 && highestLevel !== 0) { + jobMessages.push({ + job_id: job.key, + highestLevelText, + highestLevel: levelToText(highestLevel), + msgTime, + }); } - }); - return jobMessages; - } catch (e) { - throw e; - } + } + }); + return jobMessages; } function levelToText(level) { diff --git a/x-pack/plugins/ml/server/models/job_service/datafeeds.ts b/x-pack/plugins/ml/server/models/job_service/datafeeds.ts index 98e1be48bb766e..c0eb1b72825df8 100644 --- a/x-pack/plugins/ml/server/models/job_service/datafeeds.ts +++ b/x-pack/plugins/ml/server/models/job_service/datafeeds.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { i18n } from '@kbn/i18n'; import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; import { fillResultsWithTimeouts, isRequestTimeout } from './error_utils'; @@ -26,7 +26,7 @@ interface Results { }; } -export function datafeedsProvider({ callAsInternalUser }: ILegacyScopedClusterClient) { +export function datafeedsProvider({ asInternalUser }: IScopedClusterClient) { async function forceStartDatafeeds(datafeedIds: string[], start?: number, end?: number) { const jobIds = await getJobIdsByDatafeedId(); const doStartsCalled = datafeedIds.reduce((acc, cur) => { @@ -42,8 +42,8 @@ export function datafeedsProvider({ callAsInternalUser }: ILegacyScopedClusterCl try { await startDatafeed(datafeedId, start, end); return { started: true }; - } catch (error) { - return { started: false, error }; + } catch ({ body }) { + return { started: false, error: body }; } } else { return { started: true }; @@ -66,7 +66,7 @@ export function datafeedsProvider({ callAsInternalUser }: ILegacyScopedClusterCl results[datafeedId] = await doStart(datafeedId); return fillResultsWithTimeouts(results, datafeedId, datafeedIds, JOB_STATE.OPENED); } - results[datafeedId] = { started: false, error }; + results[datafeedId] = { started: false, error: error.body }; } } else { results[datafeedId] = { @@ -84,8 +84,8 @@ export function datafeedsProvider({ callAsInternalUser }: ILegacyScopedClusterCl async function openJob(jobId: string) { let opened = false; try { - const resp = await callAsInternalUser('ml.openJob', { jobId }); - opened = resp.opened; + const { body } = await asInternalUser.ml.openJob({ job_id: jobId }); + opened = body.opened; } catch (error) { if (error.statusCode === 409) { opened = true; @@ -97,7 +97,11 @@ export function datafeedsProvider({ callAsInternalUser }: ILegacyScopedClusterCl } async function startDatafeed(datafeedId: string, start?: number, end?: number) { - return callAsInternalUser('ml.startDatafeed', { datafeedId, start, end }); + return asInternalUser.ml.startDatafeed({ + datafeed_id: datafeedId, + start: (start as unknown) as string, + end: (end as unknown) as string, + }); } async function stopDatafeeds(datafeedIds: string[]) { @@ -105,7 +109,12 @@ export function datafeedsProvider({ callAsInternalUser }: ILegacyScopedClusterCl for (const datafeedId of datafeedIds) { try { - results[datafeedId] = await callAsInternalUser('ml.stopDatafeed', { datafeedId }); + const { body } = await asInternalUser.ml.stopDatafeed<{ + started: boolean; + }>({ + datafeed_id: datafeedId, + }); + results[datafeedId] = body; } catch (error) { if (isRequestTimeout(error)) { return fillResultsWithTimeouts(results, datafeedId, datafeedIds, DATAFEED_STATE.STOPPED); @@ -117,11 +126,17 @@ export function datafeedsProvider({ callAsInternalUser }: ILegacyScopedClusterCl } async function forceDeleteDatafeed(datafeedId: string) { - return callAsInternalUser('ml.deleteDatafeed', { datafeedId, force: true }); + const { body } = await asInternalUser.ml.deleteDatafeed({ + datafeed_id: datafeedId, + force: true, + }); + return body; } async function getDatafeedIdsByJobId() { - const { datafeeds } = (await callAsInternalUser('ml.datafeeds')) as MlDatafeedsResponse; + const { + body: { datafeeds }, + } = await asInternalUser.ml.getDatafeeds(); return datafeeds.reduce((acc, cur) => { acc[cur.job_id] = cur.datafeed_id; return acc; @@ -129,7 +144,9 @@ export function datafeedsProvider({ callAsInternalUser }: ILegacyScopedClusterCl } async function getJobIdsByDatafeedId() { - const { datafeeds } = (await callAsInternalUser('ml.datafeeds')) as MlDatafeedsResponse; + const { + body: { datafeeds }, + } = await asInternalUser.ml.getDatafeeds(); return datafeeds.reduce((acc, cur) => { acc[cur.datafeed_id] = cur.job_id; return acc; diff --git a/x-pack/plugins/ml/server/models/job_service/error_utils.ts b/x-pack/plugins/ml/server/models/job_service/error_utils.ts index 8a47993546fb85..dc871a9dce8057 100644 --- a/x-pack/plugins/ml/server/models/job_service/error_utils.ts +++ b/x-pack/plugins/ml/server/models/job_service/error_utils.ts @@ -7,11 +7,11 @@ import { i18n } from '@kbn/i18n'; import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; -const REQUEST_TIMEOUT = 'RequestTimeout'; +const REQUEST_TIMEOUT_NAME = 'RequestTimeout'; type ACTION_STATE = DATAFEED_STATE | JOB_STATE; -export function isRequestTimeout(error: { displayName: string }) { - return error.displayName === REQUEST_TIMEOUT; +export function isRequestTimeout(error: { name: string }) { + return error.name === REQUEST_TIMEOUT_NAME; } interface Results { diff --git a/x-pack/plugins/ml/server/models/job_service/groups.ts b/x-pack/plugins/ml/server/models/job_service/groups.ts index c4ea854c14f872..0f53d27f2eddf4 100644 --- a/x-pack/plugins/ml/server/models/job_service/groups.ts +++ b/x-pack/plugins/ml/server/models/job_service/groups.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { CalendarManager } from '../calendar'; import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; import { Job } from '../../../common/types/anomaly_detection_jobs'; @@ -23,18 +23,19 @@ interface Results { }; } -export function groupsProvider(mlClusterClient: ILegacyScopedClusterClient) { - const calMngr = new CalendarManager(mlClusterClient); - const { callAsInternalUser } = mlClusterClient; +export function groupsProvider(client: IScopedClusterClient) { + const calMngr = new CalendarManager(client); + const { asInternalUser } = client; async function getAllGroups() { const groups: { [id: string]: Group } = {}; const jobIds: { [id: string]: undefined | null } = {}; - const [{ jobs }, calendars] = await Promise.all([ - callAsInternalUser('ml.jobs') as Promise, + const [{ body }, calendars] = await Promise.all([ + asInternalUser.ml.getJobs(), calMngr.getAllCalendars(), ]); + const { jobs } = body; if (jobs) { jobs.forEach((job) => { jobIds[job.job_id] = null; @@ -80,10 +81,10 @@ export function groupsProvider(mlClusterClient: ILegacyScopedClusterClient) { for (const job of jobs) { const { job_id: jobId, groups } = job; try { - await callAsInternalUser('ml.updateJob', { jobId, body: { groups } }); + await asInternalUser.ml.updateJob({ job_id: jobId, body: { groups } }); results[jobId] = { success: true }; - } catch (error) { - results[jobId] = { success: false, error }; + } catch ({ body }) { + results[jobId] = { success: false, error: body }; } } return results; diff --git a/x-pack/plugins/ml/server/models/job_service/index.ts b/x-pack/plugins/ml/server/models/job_service/index.ts index 1ff33a7b00f0b5..6fea5d3b5a491b 100644 --- a/x-pack/plugins/ml/server/models/job_service/index.ts +++ b/x-pack/plugins/ml/server/models/job_service/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { datafeedsProvider } from './datafeeds'; import { jobsProvider } from './jobs'; import { groupsProvider } from './groups'; @@ -12,14 +12,14 @@ import { newJobCapsProvider } from './new_job_caps'; import { newJobChartsProvider, topCategoriesProvider } from './new_job'; import { modelSnapshotProvider } from './model_snapshots'; -export function jobServiceProvider(mlClusterClient: ILegacyScopedClusterClient) { +export function jobServiceProvider(client: IScopedClusterClient) { return { - ...datafeedsProvider(mlClusterClient), - ...jobsProvider(mlClusterClient), - ...groupsProvider(mlClusterClient), - ...newJobCapsProvider(mlClusterClient), - ...newJobChartsProvider(mlClusterClient), - ...topCategoriesProvider(mlClusterClient), - ...modelSnapshotProvider(mlClusterClient), + ...datafeedsProvider(client), + ...jobsProvider(client), + ...groupsProvider(client), + ...newJobCapsProvider(client), + ...newJobChartsProvider(client), + ...topCategoriesProvider(client), + ...modelSnapshotProvider(client), }; } diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index 0aa1cfdae13c7f..e047d31ba6eb79 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { uniq } from 'lodash'; import Boom from 'boom'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { parseTimeIntervalForJob } from '../../../common/util/job_utils'; import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; import { @@ -22,7 +22,7 @@ import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; import { datafeedsProvider, MlDatafeedsResponse, MlDatafeedsStatsResponse } from './datafeeds'; import { jobAuditMessagesProvider } from '../job_audit_messages'; import { resultsServiceProvider } from '../results_service'; -import { CalendarManager, Calendar } from '../calendar'; +import { CalendarManager } from '../calendar'; import { fillResultsWithTimeouts, isRequestTimeout } from './error_utils'; import { getEarliestDatafeedStartTime, @@ -47,16 +47,16 @@ interface Results { }; } -export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { - const { callAsInternalUser } = mlClusterClient; +export function jobsProvider(client: IScopedClusterClient) { + const { asInternalUser } = client; - const { forceDeleteDatafeed, getDatafeedIdsByJobId } = datafeedsProvider(mlClusterClient); - const { getAuditMessagesSummary } = jobAuditMessagesProvider(mlClusterClient); - const { getLatestBucketTimestampByJob } = resultsServiceProvider(mlClusterClient); - const calMngr = new CalendarManager(mlClusterClient); + const { forceDeleteDatafeed, getDatafeedIdsByJobId } = datafeedsProvider(client); + const { getAuditMessagesSummary } = jobAuditMessagesProvider(client); + const { getLatestBucketTimestampByJob } = resultsServiceProvider(client); + const calMngr = new CalendarManager(client); async function forceDeleteJob(jobId: string) { - return callAsInternalUser('ml.deleteJob', { jobId, force: true }); + return asInternalUser.ml.deleteJob({ job_id: jobId, force: true, wait_for_completion: false }); } async function deleteJobs(jobIds: string[]) { @@ -78,7 +78,7 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { if (isRequestTimeout(error)) { return fillResultsWithTimeouts(results, jobId, jobIds, DATAFEED_STATE.DELETED); } - results[jobId] = { deleted: false, error }; + results[jobId] = { deleted: false, error: error.body }; } } } catch (error) { @@ -90,7 +90,7 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { DATAFEED_STATE.DELETED ); } - results[jobId] = { deleted: false, error }; + results[jobId] = { deleted: false, error: error.body }; } } return results; @@ -100,7 +100,7 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { const results: Results = {}; for (const jobId of jobIds) { try { - await callAsInternalUser('ml.closeJob', { jobId }); + await asInternalUser.ml.closeJob({ job_id: jobId }); results[jobId] = { closed: true }; } catch (error) { if (isRequestTimeout(error)) { @@ -109,23 +109,23 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { if ( error.statusCode === 409 && - error.response && - error.response.includes('datafeed') === false + error.body.error?.reason && + error.body.error.reason.includes('datafeed') === false ) { // the close job request may fail (409) if the job has failed or if the datafeed hasn't been stopped. // if the job has failed we want to attempt a force close. // however, if we received a 409 due to the datafeed being started we should not attempt a force close. try { - await callAsInternalUser('ml.closeJob', { jobId, force: true }); + await asInternalUser.ml.closeJob({ job_id: jobId, force: true }); results[jobId] = { closed: true }; } catch (error2) { - if (isRequestTimeout(error)) { + if (isRequestTimeout(error2)) { return fillResultsWithTimeouts(results, jobId, jobIds, JOB_STATE.CLOSED); } - results[jobId] = { closed: false, error: error2 }; + results[jobId] = { closed: false, error: error2.body }; } } else { - results[jobId] = { closed: false, error }; + results[jobId] = { closed: false, error: error.body }; } } } @@ -139,12 +139,12 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { throw Boom.notFound(`Cannot find datafeed for job ${jobId}`); } - const dfResult = await callAsInternalUser('ml.stopDatafeed', { datafeedId, force: true }); - if (!dfResult || dfResult.stopped !== true) { + const { body } = await asInternalUser.ml.stopDatafeed({ datafeed_id: datafeedId, force: true }); + if (body.stopped !== true) { return { success: false }; } - await callAsInternalUser('ml.closeJob', { jobId, force: true }); + await asInternalUser.ml.closeJob({ job_id: jobId, force: true }); return { success: true }; } @@ -256,41 +256,26 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { const calendarsByJobId: { [jobId: string]: string[] } = {}; const globalCalendars: string[] = []; - const requests: [ - Promise, - Promise, - Promise, - Promise, - Promise, - Promise<{ [id: string]: number | undefined }> - ] = [ - jobIds.length > 0 - ? (callAsInternalUser('ml.jobs', { jobId: jobIds }) as Promise) // move length check in side call - : (callAsInternalUser('ml.jobs') as Promise), - jobIds.length > 0 - ? (callAsInternalUser('ml.jobStats', { jobId: jobIds }) as Promise) - : (callAsInternalUser('ml.jobStats') as Promise), - callAsInternalUser('ml.datafeeds') as Promise, - callAsInternalUser('ml.datafeedStats') as Promise, - calMngr.getAllCalendars(), - getLatestBucketTimestampByJob(), - ]; - + const jobIdsString = jobIds.join(); const [ - jobResults, - jobStatsResults, - datafeedResults, - datafeedStatsResults, + { body: jobResults }, + { body: jobStatsResults }, + { body: datafeedResults }, + { body: datafeedStatsResults }, calendarResults, latestBucketTimestampByJob, - ] = await Promise.all< - MlJobsResponse, - MlJobsStatsResponse, - MlDatafeedsResponse, - MlDatafeedsStatsResponse, - Calendar[], - { [id: string]: number | undefined } - >(requests); + ] = await Promise.all([ + asInternalUser.ml.getJobs( + jobIds.length > 0 ? { job_id: jobIdsString } : undefined + ), + asInternalUser.ml.getJobStats( + jobIds.length > 0 ? { job_id: jobIdsString } : undefined + ), + asInternalUser.ml.getDatafeeds(), + asInternalUser.ml.getDatafeedStats(), + calMngr.getAllCalendars(), + getLatestBucketTimestampByJob(), + ]); if (datafeedResults && datafeedResults.datafeeds) { datafeedResults.datafeeds.forEach((datafeed) => { @@ -400,9 +385,9 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { const detailed = true; const jobIds = []; try { - const tasksList = await callAsInternalUser('tasks.list', { actions, detailed }); - Object.keys(tasksList.nodes).forEach((nodeId) => { - const tasks = tasksList.nodes[nodeId].tasks; + const { body } = await asInternalUser.tasks.list({ actions, detailed }); + Object.keys(body.nodes).forEach((nodeId) => { + const tasks = body.nodes[nodeId].tasks; Object.keys(tasks).forEach((taskId) => { jobIds.push(tasks[taskId].description.replace(/^delete-job-/, '')); }); @@ -410,7 +395,9 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { } catch (e) { // if the user doesn't have permission to load the task list, // use the jobs list to get the ids of deleting jobs - const { jobs } = (await callAsInternalUser('ml.jobs')) as MlJobsResponse; + const { + body: { jobs }, + } = await asInternalUser.ml.getJobs(); jobIds.push(...jobs.filter((j) => j.deleting === true).map((j) => j.job_id)); } return { jobIds }; @@ -421,13 +408,13 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { // e.g. *_low_request_rate_ecs async function jobsExist(jobIds: string[] = []) { // Get the list of job IDs. - const jobsInfo = (await callAsInternalUser('ml.jobs', { - jobId: jobIds, - })) as MlJobsResponse; + const { body } = await asInternalUser.ml.getJobs({ + job_id: jobIds.join(), + }); const results: { [id: string]: boolean } = {}; - if (jobsInfo.count > 0) { - const allJobIds = jobsInfo.jobs.map((job) => job.job_id); + if (body.count > 0) { + const allJobIds = body.jobs.map((job) => job.job_id); // Check if each of the supplied IDs match existing jobs. jobIds.forEach((jobId) => { @@ -446,9 +433,9 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { } async function getAllJobAndGroupIds() { - const { getAllGroups } = groupsProvider(mlClusterClient); - const jobs = (await callAsInternalUser('ml.jobs')) as MlJobsResponse; - const jobIds = jobs.jobs.map((job) => job.job_id); + const { getAllGroups } = groupsProvider(client); + const { body } = await asInternalUser.ml.getJobs(); + const jobIds = body.jobs.map((job) => job.job_id); const groups = await getAllGroups(); const groupIds = groups.map((group) => group.id); @@ -460,13 +447,13 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { async function getLookBackProgress(jobId: string, start: number, end: number) { const datafeedId = `datafeed-${jobId}`; - const [jobStats, isRunning] = await Promise.all([ - callAsInternalUser('ml.jobStats', { jobId: [jobId] }) as Promise, + const [{ body }, isRunning] = await Promise.all([ + asInternalUser.ml.getJobStats({ job_id: jobId }), isDatafeedRunning(datafeedId), ]); - if (jobStats.jobs.length) { - const statsForJob = jobStats.jobs[0]; + if (body.jobs.length) { + const statsForJob = body.jobs[0]; const time = statsForJob.data_counts.latest_record_timestamp; const progress = (time - start) / (end - start); const isJobClosed = statsForJob.state === JOB_STATE.CLOSED; @@ -480,11 +467,11 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { } async function isDatafeedRunning(datafeedId: string) { - const stats = (await callAsInternalUser('ml.datafeedStats', { - datafeedId: [datafeedId], - })) as MlDatafeedsStatsResponse; - if (stats.datafeeds.length) { - const state = stats.datafeeds[0].state; + const { body } = await asInternalUser.ml.getDatafeedStats({ + datafeed_id: datafeedId, + }); + if (body.datafeeds.length) { + const state = body.datafeeds[0].state; return ( state === DATAFEED_STATE.STARTED || state === DATAFEED_STATE.STARTING || diff --git a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts index 576d6f8cbb1600..34206a68ffeb9e 100644 --- a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts +++ b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { ModelSnapshot } from '../../../common/types/anomaly_detection_jobs'; import { datafeedsProvider } from './datafeeds'; import { FormCalendar, CalendarManager } from '../calendar'; @@ -19,9 +19,9 @@ export interface RevertModelSnapshotResponse { model: ModelSnapshot; } -export function modelSnapshotProvider(mlClusterClient: ILegacyScopedClusterClient) { - const { callAsInternalUser } = mlClusterClient; - const { forceStartDatafeeds, getDatafeedIdsByJobId } = datafeedsProvider(mlClusterClient); +export function modelSnapshotProvider(client: IScopedClusterClient) { + const { asInternalUser } = client; + const { forceStartDatafeeds, getDatafeedIdsByJobId } = datafeedsProvider(client); async function revertModelSnapshot( jobId: string, @@ -33,13 +33,13 @@ export function modelSnapshotProvider(mlClusterClient: ILegacyScopedClusterClien ) { let datafeedId = `datafeed-${jobId}`; // ensure job exists - await callAsInternalUser('ml.jobs', { jobId: [jobId] }); + await asInternalUser.ml.getJobs({ job_id: jobId }); try { // ensure the datafeed exists // the datafeed is probably called datafeed- - await callAsInternalUser('ml.datafeeds', { - datafeedId: [datafeedId], + await asInternalUser.ml.getDatafeeds({ + datafeed_id: datafeedId, }); } catch (e) { // if the datafeed isn't called datafeed- @@ -52,19 +52,21 @@ export function modelSnapshotProvider(mlClusterClient: ILegacyScopedClusterClien } // ensure the snapshot exists - const snapshot = (await callAsInternalUser('ml.modelSnapshots', { - jobId, - snapshotId, - })) as ModelSnapshotsResponse; + const { body: snapshot } = await asInternalUser.ml.getModelSnapshots({ + job_id: jobId, + snapshot_id: snapshotId, + }); // apply the snapshot revert - const { model } = (await callAsInternalUser('ml.revertModelSnapshot', { - jobId, - snapshotId, + const { + body: { model }, + } = await asInternalUser.ml.revertModelSnapshot({ + job_id: jobId, + snapshot_id: snapshotId, body: { delete_intervening_results: deleteInterveningResults, }, - })) as RevertModelSnapshotResponse; + }); // create calendar (if specified) and replay datafeed if (replay && model.snapshot_id === snapshotId && snapshot.model_snapshots.length) { @@ -85,7 +87,7 @@ export function modelSnapshotProvider(mlClusterClient: ILegacyScopedClusterClien end_time: s.end, })), }; - const cm = new CalendarManager(mlClusterClient); + const cm = new CalendarManager(client); await cm.newCalendar(calendar); } diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts index ca3e0cef21049a..6b9f30b2ae00bb 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { chunk } from 'lodash'; import { SearchResponse } from 'elasticsearch'; import { CATEGORY_EXAMPLES_SAMPLE_SIZE } from '../../../../../common/constants/categorization_job'; @@ -18,9 +18,9 @@ import { ValidationResults } from './validation_results'; const CHUNK_SIZE = 100; export function categorizationExamplesProvider({ - callAsCurrentUser, - callAsInternalUser, -}: ILegacyScopedClusterClient) { + asCurrentUser, + asInternalUser, +}: IScopedClusterClient) { const validationResults = new ValidationResults(); async function categorizationExamples( @@ -57,7 +57,7 @@ export function categorizationExamplesProvider({ } } - const results: SearchResponse<{ [id: string]: string }> = await callAsCurrentUser('search', { + const { body } = await asCurrentUser.search>({ index: indexPatternTitle, size, body: { @@ -67,7 +67,7 @@ export function categorizationExamplesProvider({ }, }); - const tempExamples = results.hits.hits.map(({ _source }) => _source[categorizationFieldName]); + const tempExamples = body.hits.hits.map(({ _source }) => _source[categorizationFieldName]); validationResults.createNullValueResult(tempExamples); @@ -112,7 +112,9 @@ export function categorizationExamplesProvider({ } async function loadTokens(examples: string[], analyzer: CategorizationAnalyzer) { - const { tokens }: { tokens: Token[] } = await callAsInternalUser('indices.analyze', { + const { + body: { tokens }, + } = await asInternalUser.indices.analyze<{ tokens: Token[] }>({ body: { ...getAnalyzer(analyzer), text: examples, diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts index 5ade86806f3839..347afec8ef73cc 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts @@ -5,13 +5,13 @@ */ import { SearchResponse } from 'elasticsearch'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; import { CategoryId, Category } from '../../../../../common/types/categories'; -export function topCategoriesProvider({ callAsInternalUser }: ILegacyScopedClusterClient) { - async function getTotalCategories(jobId: string): Promise<{ total: number }> { - const totalResp = await callAsInternalUser('search', { +export function topCategoriesProvider({ asInternalUser }: IScopedClusterClient) { + async function getTotalCategories(jobId: string): Promise { + const { body } = await asInternalUser.search>({ index: ML_RESULTS_INDEX_PATTERN, size: 0, body: { @@ -33,11 +33,12 @@ export function topCategoriesProvider({ callAsInternalUser }: ILegacyScopedClust }, }, }); - return totalResp?.hits?.total?.value ?? 0; + // @ts-ignore total is an object here + return body?.hits?.total?.value ?? 0; } async function getTopCategoryCounts(jobId: string, numberOfCategories: number) { - const top: SearchResponse = await callAsInternalUser('search', { + const { body } = await asInternalUser.search>({ index: ML_RESULTS_INDEX_PATTERN, size: 0, body: { @@ -76,7 +77,7 @@ export function topCategoriesProvider({ callAsInternalUser }: ILegacyScopedClust const catCounts: Array<{ id: CategoryId; count: number; - }> = top.aggregations?.cat_count?.buckets.map((c: any) => ({ + }> = body.aggregations?.cat_count?.buckets.map((c: any) => ({ id: c.key, count: c.doc_count, })); @@ -99,7 +100,7 @@ export function topCategoriesProvider({ callAsInternalUser }: ILegacyScopedClust field: 'category_id', }, }; - const result: SearchResponse = await callAsInternalUser('search', { + const { body } = await asInternalUser.search>({ index: ML_RESULTS_INDEX_PATTERN, size, body: { @@ -118,7 +119,7 @@ export function topCategoriesProvider({ callAsInternalUser }: ILegacyScopedClust }, }); - return result.hits.hits?.map((c: { _source: Category }) => c._source) || []; + return body.hits.hits?.map((c: { _source: Category }) => c._source) || []; } async function topCategories(jobId: string, numberOfCategories: number) { diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts index 4b90283a3a966f..60595ccedff45e 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts @@ -144,7 +144,7 @@ export class ValidationResults { this.createPrivilegesErrorResult(error); return; } - const message: string = error.message; + const message: string = error.body.error?.reason; if (message) { const rxp = /exceeded the allowed maximum of \[(\d+?)\]/; const match = rxp.exec(message); @@ -170,7 +170,7 @@ export class ValidationResults { } public createPrivilegesErrorResult(error: any) { - const message: string = error.message; + const message: string = error.body.error?.reason; if (message) { this._results.push({ id: VALIDATION_RESULT.INSUFFICIENT_PRIVILEGES, diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/charts.ts b/x-pack/plugins/ml/server/models/job_service/new_job/charts.ts index 63ae2c624ac381..da7d8d0577e4e6 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/charts.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/charts.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { newJobLineChartProvider } from './line_chart'; import { newJobPopulationChartProvider } from './population_chart'; -export function newJobChartsProvider(mlClusterClient: ILegacyScopedClusterClient) { - const { newJobLineChart } = newJobLineChartProvider(mlClusterClient); - const { newJobPopulationChart } = newJobPopulationChartProvider(mlClusterClient); +export function newJobChartsProvider(client: IScopedClusterClient) { + const { newJobLineChart } = newJobLineChartProvider(client); + const { newJobPopulationChart } = newJobPopulationChartProvider(client); return { newJobLineChart, diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts b/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts index 3080b37867de52..9eea1ea2a28ae9 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { AggFieldNamePair, EVENT_RATE_FIELD_ID } from '../../../../common/types/fields'; import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; @@ -23,7 +23,7 @@ interface ProcessedResults { totalResults: number; } -export function newJobLineChartProvider({ callAsCurrentUser }: ILegacyScopedClusterClient) { +export function newJobLineChartProvider({ asCurrentUser }: IScopedClusterClient) { async function newJobLineChart( indexPatternTitle: string, timeField: string, @@ -47,9 +47,9 @@ export function newJobLineChartProvider({ callAsCurrentUser }: ILegacyScopedClus splitFieldValue ); - const results = await callAsCurrentUser('search', json); + const { body } = await asCurrentUser.search(json); return processSearchResults( - results, + body, aggFieldNamePairs.map((af) => af.field) ); } diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts b/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts index ab75787a0069f0..567afec8094054 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { AggFieldNamePair, EVENT_RATE_FIELD_ID } from '../../../../common/types/fields'; import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; @@ -29,7 +29,7 @@ interface ProcessedResults { totalResults: number; } -export function newJobPopulationChartProvider({ callAsCurrentUser }: ILegacyScopedClusterClient) { +export function newJobPopulationChartProvider({ asCurrentUser }: IScopedClusterClient) { async function newJobPopulationChart( indexPatternTitle: string, timeField: string, @@ -51,15 +51,11 @@ export function newJobPopulationChartProvider({ callAsCurrentUser }: ILegacyScop splitFieldName ); - try { - const results = await callAsCurrentUser('search', json); - return processSearchResults( - results, - aggFieldNamePairs.map((af) => af.field) - ); - } catch (error) { - return { error }; - } + const { body } = await asCurrentUser.search(json); + return processSearchResults( + body, + aggFieldNamePairs.map((af) => af.field) + ); } return { diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts index fd20610450cc17..c3b1de64c3eb5e 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { cloneDeep } from 'lodash'; import { SavedObjectsClientContract } from 'kibana/server'; import { @@ -40,35 +40,36 @@ const supportedTypes: string[] = [ export function fieldServiceProvider( indexPattern: string, isRollup: boolean, - mlClusterClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, savedObjectsClient: SavedObjectsClientContract ) { - return new FieldsService(indexPattern, isRollup, mlClusterClient, savedObjectsClient); + return new FieldsService(indexPattern, isRollup, client, savedObjectsClient); } class FieldsService { private _indexPattern: string; private _isRollup: boolean; - private _mlClusterClient: ILegacyScopedClusterClient; + private _mlClusterClient: IScopedClusterClient; private _savedObjectsClient: SavedObjectsClientContract; constructor( indexPattern: string, isRollup: boolean, - mlClusterClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, savedObjectsClient: SavedObjectsClientContract ) { this._indexPattern = indexPattern; this._isRollup = isRollup; - this._mlClusterClient = mlClusterClient; + this._mlClusterClient = client; this._savedObjectsClient = savedObjectsClient; } private async loadFieldCaps(): Promise { - return this._mlClusterClient.callAsCurrentUser('fieldCaps', { + const { body } = await this._mlClusterClient.asCurrentUser.fieldCaps({ index: this._indexPattern, fields: '*', }); + return body; } // create field object from the results from _field_caps diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts index 38d6481e02a742..891cb2e0d1e64b 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts @@ -21,28 +21,23 @@ describe('job_service - job_caps', () => { let savedObjectsClientMock: any; beforeEach(() => { - const callAsNonRollupMock = jest.fn((action: string) => { - switch (action) { - case 'fieldCaps': - return farequoteFieldCaps; - } - }); + const asNonRollupMock = { + fieldCaps: jest.fn(() => ({ body: farequoteFieldCaps })), + }; + mlClusterClientNonRollupMock = { - callAsCurrentUser: callAsNonRollupMock, - callAsInternalUser: callAsNonRollupMock, + asCurrentUser: asNonRollupMock, + asInternalUser: asNonRollupMock, + }; + + const callAsRollupMock = { + fieldCaps: jest.fn(() => ({ body: cloudwatchFieldCaps })), + rollup: { getRollupIndexCaps: jest.fn(() => Promise.resolve({ body: rollupCaps })) }, }; - const callAsRollupMock = jest.fn((action: string) => { - switch (action) { - case 'fieldCaps': - return cloudwatchFieldCaps; - case 'ml.rollupIndexCapabilities': - return Promise.resolve(rollupCaps); - } - }); mlClusterClientRollupMock = { - callAsCurrentUser: callAsRollupMock, - callAsInternalUser: callAsRollupMock, + asCurrentUser: callAsRollupMock, + asInternalUser: callAsRollupMock, }; savedObjectsClientMock = { diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts index 5616dade53a787..7559111d012d0d 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; +import { IScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; import { Aggregation, Field, NewJobCaps } from '../../../../common/types/fields'; import { fieldServiceProvider } from './field_service'; @@ -12,18 +12,13 @@ interface NewJobCapsResponse { [indexPattern: string]: NewJobCaps; } -export function newJobCapsProvider(mlClusterClient: ILegacyScopedClusterClient) { +export function newJobCapsProvider(client: IScopedClusterClient) { async function newJobCaps( indexPattern: string, isRollup: boolean = false, savedObjectsClient: SavedObjectsClientContract ): Promise { - const fieldService = fieldServiceProvider( - indexPattern, - isRollup, - mlClusterClient, - savedObjectsClient - ); + const fieldService = fieldServiceProvider(indexPattern, isRollup, client, savedObjectsClient); const { aggs, fields } = await fieldService.getData(); convertForStringify(aggs, fields); diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts index f3a9bd49c27d6e..b7f4c8af62283d 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { SavedObject } from 'kibana/server'; import { IndexPatternAttributes } from 'src/plugins/data/server'; import { SavedObjectsClientContract } from 'kibana/server'; @@ -22,7 +22,7 @@ export interface RollupJob { export async function rollupServiceProvider( indexPattern: string, - { callAsCurrentUser }: ILegacyScopedClusterClient, + { asCurrentUser }: IScopedClusterClient, savedObjectsClient: SavedObjectsClientContract ) { const rollupIndexPatternObject = await loadRollupIndexPattern(indexPattern, savedObjectsClient); @@ -32,8 +32,8 @@ export async function rollupServiceProvider( if (rollupIndexPatternObject !== null) { const parsedTypeMetaData = JSON.parse(rollupIndexPatternObject.attributes.typeMeta); const rollUpIndex: string = parsedTypeMetaData.params.rollup_index; - const rollupCaps = await callAsCurrentUser('ml.rollupIndexCapabilities', { - indexPattern: rollUpIndex, + const { body: rollupCaps } = await asCurrentUser.rollup.getRollupIndexCaps({ + index: rollUpIndex, }); const indexRollupCaps = rollupCaps[rollUpIndex]; diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts index 1c74953e4dda94..810d0ae9dcd871 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts @@ -4,48 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { validateJob, ValidateJobPayload } from './job_validation'; import { JobValidationMessage } from '../../../common/constants/messages'; -const mlClusterClient = ({ - // mock callAsCurrentUser - callAsCurrentUser: (method: string) => { - return new Promise((resolve) => { - if (method === 'fieldCaps') { - resolve({ fields: [] }); - return; - } else if (method === 'ml.info') { - resolve({ +const callAs = { + fieldCaps: () => Promise.resolve({ body: { fields: [] } }), + ml: { + info: () => + Promise.resolve({ + body: { limits: { effective_max_model_memory_limit: '100MB', max_model_memory_limit: '1GB', }, - }); - } - resolve({}); - }) as Promise; + }, + }), }, + search: () => Promise.resolve({ body: {} }), +}; - // mock callAsInternalUser - callAsInternalUser: (method: string) => { - return new Promise((resolve) => { - if (method === 'fieldCaps') { - resolve({ fields: [] }); - return; - } else if (method === 'ml.info') { - resolve({ - limits: { - effective_max_model_memory_limit: '100MB', - max_model_memory_limit: '1GB', - }, - }); - } - resolve({}); - }) as Promise; - }, -} as unknown) as ILegacyScopedClusterClient; +const mlClusterClient = ({ + asCurrentUser: callAs, + asInternalUser: callAs, +} as unknown) as IScopedClusterClient; // Note: The tests cast `payload` as any // so we can simulate possible runtime payloads diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts index 6692ecb22bd9e6..9e272f1f770fcd 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import Boom from 'boom'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; import { fieldsServiceProvider } from '../fields_service'; import { renderTemplate } from '../../../common/util/string_utils'; @@ -34,7 +34,7 @@ export type ValidateJobPayload = TypeOf; * @kbn/config-schema has checked the payload {@link validateJobSchema}. */ export async function validateJob( - mlClusterClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, payload: ValidateJobPayload, kbnVersion = 'current', isSecurityDisabled?: boolean @@ -63,8 +63,8 @@ export async function validateJob( // if no duration was part of the request, fall back to finding out // the time range of the time field of the index, but also check first // if the time field is a valid field of type 'date' using isValidTimeField() - if (typeof duration === 'undefined' && (await isValidTimeField(mlClusterClient, job))) { - const fs = fieldsServiceProvider(mlClusterClient); + if (typeof duration === 'undefined' && (await isValidTimeField(client, job))) { + const fs = fieldsServiceProvider(client); const index = job.datafeed_config.indices.join(','); const timeField = job.data_description.time_field; const timeRange = await fs.getTimeFieldRange(index, timeField, job.datafeed_config.query); @@ -79,24 +79,22 @@ export async function validateJob( // next run only the cardinality tests to find out if they trigger an error // so we can decide later whether certain additional tests should be run - const cardinalityMessages = await validateCardinality(mlClusterClient, job); + const cardinalityMessages = await validateCardinality(client, job); validationMessages.push(...cardinalityMessages); const cardinalityError = cardinalityMessages.some((m) => { return messages[m.id as MessageId].status === VALIDATION_STATUS.ERROR; }); validationMessages.push( - ...(await validateBucketSpan(mlClusterClient, job, duration, isSecurityDisabled)) + ...(await validateBucketSpan(client, job, duration, isSecurityDisabled)) ); - validationMessages.push(...(await validateTimeRange(mlClusterClient, job, duration))); + validationMessages.push(...(await validateTimeRange(client, job, duration))); // only run the influencer and model memory limit checks // if cardinality checks didn't return a message with an error level if (cardinalityError === false) { validationMessages.push(...(await validateInfluencers(job))); - validationMessages.push( - ...(await validateModelMemoryLimit(mlClusterClient, job, duration)) - ); + validationMessages.push(...(await validateModelMemoryLimit(client, job, duration))); } } else { validationMessages = basicValidation.messages; diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js index 11f8d8967c4e0b..315ad09176571e 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js @@ -45,7 +45,7 @@ const pickBucketSpan = (bucketSpans) => { return bucketSpans[i]; }; -export async function validateBucketSpan(mlClusterClient, job, duration) { +export async function validateBucketSpan(client, job, duration) { validateJobObject(job); // if there is no duration, do not run the estimate test @@ -117,7 +117,7 @@ export async function validateBucketSpan(mlClusterClient, job, duration) { try { const estimations = estimatorConfigs.map((data) => { return new Promise((resolve) => { - estimateBucketSpanFactory(mlClusterClient)(data) + estimateBucketSpanFactory(client)(data) .then(resolve) // this catch gets triggered when the estimation code runs without error // but isn't able to come up with a bucket span estimation. diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts index f9145ab576d71e..80418d590af764 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts @@ -24,12 +24,12 @@ import mockItSearchResponse from './__mocks__/mock_it_search_response.json'; const mlClusterClientFactory = (mockSearchResponse: any) => { const callAs = () => { return new Promise((resolve) => { - resolve(mockSearchResponse); + resolve({ body: mockSearchResponse }); }); }; return { - callAsCurrentUser: callAs, - callAsInternalUser: callAs, + asCurrentUser: callAs, + asInternalUser: callAs, }; }; diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts index 16ee70ad9efde6..1be0751e15f227 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts @@ -6,7 +6,7 @@ import cloneDeep from 'lodash/cloneDeep'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; @@ -24,21 +24,21 @@ const mockResponses = { const mlClusterClientFactory = ( responses: Record, fail = false -): ILegacyScopedClusterClient => { - const callAs = (requestName: string) => { - return new Promise((resolve, reject) => { - const response = responses[requestName]; - if (fail) { - reject(response); - } else { - resolve(response); - } - }) as Promise; +): IScopedClusterClient => { + const callAs = { + search: () => Promise.resolve({ body: responses.search }), + fieldCaps: () => Promise.resolve({ body: responses.fieldCaps }), }; - return { - callAsCurrentUser: callAs, - callAsInternalUser: callAs, + + const callAsFail = { + search: () => Promise.reject({ body: {} }), + fieldCaps: () => Promise.reject({ body: {} }), }; + + return ({ + asCurrentUser: fail === false ? callAs : callAsFail, + asInternalUser: fail === false ? callAs : callAsFail, + } as unknown) as IScopedClusterClient; }; describe('ML - validateCardinality', () => { diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts index 1545c4c0062ec8..c5822b863c83da 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { DataVisualizer } from '../data_visualizer'; import { validateJobObject } from './validate_job_object'; @@ -43,12 +43,9 @@ type Validator = (obj: { messages: Messages; }>; -const validateFactory = ( - mlClusterClient: ILegacyScopedClusterClient, - job: CombinedJob -): Validator => { - const { callAsCurrentUser } = mlClusterClient; - const dv = new DataVisualizer(mlClusterClient); +const validateFactory = (client: IScopedClusterClient, job: CombinedJob): Validator => { + const { asCurrentUser } = client; + const dv = new DataVisualizer(client); const modelPlotConfigTerms = job?.model_plot_config?.terms ?? ''; const modelPlotConfigFieldCount = @@ -77,7 +74,7 @@ const validateFactory = ( ] as string[]; // use fieldCaps endpoint to get data about whether fields are aggregatable - const fieldCaps = await callAsCurrentUser('fieldCaps', { + const { body: fieldCaps } = await asCurrentUser.fieldCaps({ index: job.datafeed_config.indices.join(','), fields: uniqueFieldNames, }); @@ -154,7 +151,7 @@ const validateFactory = ( }; export async function validateCardinality( - mlClusterClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, job?: CombinedJob ): Promise | never { const messages: Messages = []; @@ -174,7 +171,7 @@ export async function validateCardinality( } // validate({ type, isInvalid }) asynchronously returns an array of validation messages - const validate = validateFactory(mlClusterClient, job); + const validate = validateFactory(client, job); const modelPlotEnabled = job.model_plot_config?.enabled ?? false; diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts index 6ffb0e320982bc..35792c66e66ece 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { CombinedJob, Detector } from '../../../common/types/anomaly_detection_jobs'; -import { ModelMemoryEstimate } from '../calculate_model_memory_limit/calculate_model_memory_limit'; +import { ModelMemoryEstimateResponse } from '../calculate_model_memory_limit/calculate_model_memory_limit'; import { validateModelMemoryLimit } from './validate_model_memory_limit'; describe('ML - validateModelMemoryLimit', () => { @@ -65,44 +65,36 @@ describe('ML - validateModelMemoryLimit', () => { }; // mock estimate model memory - const modelMemoryEstimateResponse: ModelMemoryEstimate = { + const modelMemoryEstimateResponse: ModelMemoryEstimateResponse = { model_memory_estimate: '40mb', }; interface MockAPICallResponse { - 'ml.estimateModelMemory'?: ModelMemoryEstimate; + 'ml.estimateModelMemory'?: ModelMemoryEstimateResponse; } - // mock callAsCurrentUser + // mock asCurrentUser // used in three places: // - to retrieve the info endpoint // - to search for cardinality of split field // - to retrieve field capabilities used in search for split field cardinality const getMockMlClusterClient = ({ 'ml.estimateModelMemory': estimateModelMemory, - }: MockAPICallResponse = {}): ILegacyScopedClusterClient => { - const callAs = (call: string) => { - if (typeof call === undefined) { - return Promise.reject(); - } - - let response = {}; - if (call === 'ml.info') { - response = mlInfoResponse; - } else if (call === 'search') { - response = cardinalitySearchResponse; - } else if (call === 'fieldCaps') { - response = fieldCapsResponse; - } else if (call === 'ml.estimateModelMemory') { - response = estimateModelMemory || modelMemoryEstimateResponse; - } - return Promise.resolve(response); + }: MockAPICallResponse = {}): IScopedClusterClient => { + const callAs = { + ml: { + info: () => Promise.resolve({ body: mlInfoResponse }), + estimateModelMemory: () => + Promise.resolve({ body: estimateModelMemory || modelMemoryEstimateResponse }), + }, + search: () => Promise.resolve({ body: cardinalitySearchResponse }), + fieldCaps: () => Promise.resolve({ body: fieldCapsResponse }), }; - return { - callAsCurrentUser: callAs, - callAsInternalUser: callAs, - }; + return ({ + asCurrentUser: callAs, + asInternalUser: callAs, + } as unknown) as IScopedClusterClient; }; function getJobConfig(influencers: string[] = [], detectors: Detector[] = []) { diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts index 728342294c4247..9733e17e0f379b 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts @@ -5,7 +5,7 @@ */ import numeral from '@elastic/numeral'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import { validateJobObject } from './validate_job_object'; import { calculateModelMemoryLimitProvider } from '../calculate_model_memory_limit'; @@ -16,11 +16,11 @@ import { MlInfoResponse } from '../../../common/types/ml_server_info'; const MODEL_MEMORY_LIMIT_MINIMUM_BYTES = 1048576; export async function validateModelMemoryLimit( - mlClusterClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, job: CombinedJob, duration?: { start?: number; end?: number } ) { - const { callAsInternalUser } = mlClusterClient; + const { asInternalUser } = client; validateJobObject(job); // retrieve the model memory limit specified by the user in the job config. @@ -52,12 +52,12 @@ export async function validateModelMemoryLimit( // retrieve the max_model_memory_limit value from the server // this will be unset unless the user has set this on their cluster - const info = (await callAsInternalUser('ml.info')) as MlInfoResponse; - const maxModelMemoryLimit = info.limits.max_model_memory_limit?.toUpperCase(); - const effectiveMaxModelMemoryLimit = info.limits.effective_max_model_memory_limit?.toUpperCase(); + const { body } = await asInternalUser.ml.info(); + const maxModelMemoryLimit = body.limits.max_model_memory_limit?.toUpperCase(); + const effectiveMaxModelMemoryLimit = body.limits.effective_max_model_memory_limit?.toUpperCase(); if (runCalcModelMemoryTest) { - const { modelMemoryLimit } = await calculateModelMemoryLimitProvider(mlClusterClient)( + const { modelMemoryLimit } = await calculateModelMemoryLimitProvider(client)( job.analysis_config, job.datafeed_config.indices.join(','), job.datafeed_config.query, diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts index a45be189ba3d8f..12458af0521a9a 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts @@ -6,7 +6,7 @@ import cloneDeep from 'lodash/cloneDeep'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; @@ -21,16 +21,15 @@ const mockSearchResponse = { search: mockTimeRange, }; -const mlClusterClientFactory = (resp: any): ILegacyScopedClusterClient => { - const callAs = (path: string) => { - return new Promise((resolve) => { - resolve(resp[path]); - }) as Promise; - }; - return { - callAsCurrentUser: callAs, - callAsInternalUser: callAs, +const mlClusterClientFactory = (response: any): IScopedClusterClient => { + const callAs = { + fieldCaps: () => Promise.resolve({ body: response.fieldCaps }), + search: () => Promise.resolve({ body: response.search }), }; + return ({ + asCurrentUser: callAs, + asInternalUser: callAs, + } as unknown) as IScopedClusterClient; }; function getMinimalValidJob() { diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts index a94ceffa902732..83d9621898f96e 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/server'; import { parseInterval } from '../../../common/util/parse_interval'; import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; @@ -26,15 +26,12 @@ const BUCKET_SPAN_COMPARE_FACTOR = 25; const MIN_TIME_SPAN_MS = 7200000; const MIN_TIME_SPAN_READABLE = '2 hours'; -export async function isValidTimeField( - { callAsCurrentUser }: ILegacyScopedClusterClient, - job: CombinedJob -) { +export async function isValidTimeField({ asCurrentUser }: IScopedClusterClient, job: CombinedJob) { const index = job.datafeed_config.indices.join(','); const timeField = job.data_description.time_field; // check if time_field is of type 'date' or 'date_nanos' - const fieldCaps = await callAsCurrentUser('fieldCaps', { + const { body: fieldCaps } = await asCurrentUser.fieldCaps({ index, fields: [timeField], }); @@ -47,7 +44,7 @@ export async function isValidTimeField( } export async function validateTimeRange( - mlClientCluster: ILegacyScopedClusterClient, + mlClientCluster: IScopedClusterClient, job: CombinedJob, timeRange?: Partial ) { diff --git a/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts b/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts index 9c0efe259844c1..76dc68d2b59e3d 100644 --- a/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts +++ b/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { PARTITION_FIELDS } from '../../../common/constants/anomalies'; import { PartitionFieldsType } from '../../../common/types/anomalies'; import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; @@ -74,9 +74,7 @@ function getFieldObject(fieldType: PartitionFieldsType, aggs: any) { : {}; } -export const getPartitionFieldsValuesFactory = ({ - callAsInternalUser, -}: ILegacyScopedClusterClient) => +export const getPartitionFieldsValuesFactory = ({ asInternalUser }: IScopedClusterClient) => /** * Gets the record of partition fields with possible values that fit the provided queries. * @param jobId - Job ID @@ -92,7 +90,7 @@ export const getPartitionFieldsValuesFactory = ({ earliestMs: number, latestMs: number ) { - const jobsResponse = await callAsInternalUser('ml.jobs', { jobId: [jobId] }); + const { body: jobsResponse } = await asInternalUser.ml.getJobs({ job_id: jobId }); if (jobsResponse.count === 0 || jobsResponse.jobs === undefined) { throw Boom.notFound(`Job with the id "${jobId}" not found`); } @@ -101,7 +99,7 @@ export const getPartitionFieldsValuesFactory = ({ const isModelPlotEnabled = job?.model_plot_config?.enabled; - const resp = await callAsInternalUser('search', { + const { body } = await asInternalUser.search({ index: ML_RESULTS_INDEX_PATTERN, size: 0, body: { @@ -151,7 +149,7 @@ export const getPartitionFieldsValuesFactory = ({ return PARTITION_FIELDS.reduce((acc, key) => { return { ...acc, - ...getFieldObject(key, resp.aggregations), + ...getFieldObject(key, body.aggregations), }; }, {}); }; diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts index 7be8bac61e69d1..190b5d99309d7d 100644 --- a/x-pack/plugins/ml/server/models/results_service/results_service.ts +++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts @@ -9,7 +9,7 @@ import slice from 'lodash/slice'; import get from 'lodash/get'; import moment from 'moment'; import { SearchResponse } from 'elasticsearch'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import Boom from 'boom'; import { buildAnomalyTableItems } from './build_anomaly_table_items'; import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; @@ -40,8 +40,8 @@ interface Influencer { fieldValue: any; } -export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClient) { - const { callAsInternalUser } = mlClusterClient; +export function resultsServiceProvider(client: IScopedClusterClient) { + const { asInternalUser } = client; // Obtains data for the anomalies table, aggregating anomalies by day or hour as requested. // Return an Object with properties 'anomalies' and 'interval' (interval used to aggregate anomalies, // one of day, hour or second. Note 'auto' can be provided as the aggregationInterval in the request, @@ -144,7 +144,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }); } - const resp: SearchResponse = await callAsInternalUser('search', { + const { body } = await asInternalUser.search>({ index: ML_RESULTS_INDEX_PATTERN, rest_total_hits_as_int: true, size: maxRecords, @@ -178,9 +178,9 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie anomalies: [], interval: 'second', }; - if (resp.hits.total !== 0) { + if (body.hits.total !== 0) { let records: AnomalyRecordDoc[] = []; - resp.hits.hits.forEach((hit) => { + body.hits.hits.forEach((hit) => { records.push(hit._source); }); @@ -298,8 +298,8 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }, }; - const resp = await callAsInternalUser('search', query); - const maxScore = get(resp, ['aggregations', 'max_score', 'value'], null); + const { body } = await asInternalUser.search(query); + const maxScore = get(body, ['aggregations', 'max_score', 'value'], null); return { maxScore }; } @@ -336,7 +336,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie // Size of job terms agg, consistent with maximum number of jobs supported by Java endpoints. const maxJobs = 10000; - const resp = await callAsInternalUser('search', { + const { body } = await asInternalUser.search({ index: ML_RESULTS_INDEX_PATTERN, size: 0, body: { @@ -364,7 +364,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }); const bucketsByJobId: Array<{ key: string; maxTimestamp: { value?: number } }> = get( - resp, + body, ['aggregations', 'byJobId', 'buckets'], [] ); @@ -380,7 +380,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie // from the given index and job ID. // Returned response consists of a list of examples against category ID. async function getCategoryExamples(jobId: string, categoryIds: any, maxExamples: number) { - const resp = await callAsInternalUser('search', { + const { body } = await asInternalUser.search({ index: ML_RESULTS_INDEX_PATTERN, rest_total_hits_as_int: true, size: ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, // Matches size of records in anomaly summary table. @@ -394,8 +394,8 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }); const examplesByCategoryId: { [key: string]: any } = {}; - if (resp.hits.total !== 0) { - resp.hits.hits.forEach((hit: any) => { + if (body.hits.total !== 0) { + body.hits.hits.forEach((hit: any) => { if (maxExamples) { examplesByCategoryId[hit._source.category_id] = slice( hit._source.examples, @@ -415,7 +415,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie // Returned response contains four properties - categoryId, regex, examples // and terms (space delimited String of the common tokens matched in values of the category). async function getCategoryDefinition(jobId: string, categoryId: string) { - const resp = await callAsInternalUser('search', { + const { body } = await asInternalUser.search({ index: ML_RESULTS_INDEX_PATTERN, rest_total_hits_as_int: true, size: 1, @@ -429,8 +429,8 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }); const definition = { categoryId, terms: null, regex: null, examples: [] }; - if (resp.hits.total !== 0) { - const source = resp.hits.hits[0]._source; + if (body.hits.total !== 0) { + const source = body.hits.hits[0]._source; definition.categoryId = source.category_id; definition.regex = source.regex; definition.terms = source.terms; @@ -456,7 +456,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }, }); } - const results: SearchResponse = await callAsInternalUser('search', { + const { body } = await asInternalUser.search>({ index: ML_RESULTS_INDEX_PATTERN, body: { query: { @@ -473,7 +473,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }, }, }); - return results ? results.hits.hits.map((r) => r._source) : []; + return body ? body.hits.hits.map((r) => r._source) : []; } async function getCategoryStoppedPartitions( @@ -485,15 +485,15 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }; // first determine from job config if stop_on_warn is true // if false return [] - const jobConfigResponse: MlJobsResponse = await callAsInternalUser('ml.jobs', { - jobId: jobIds, + const { body } = await asInternalUser.ml.getJobs({ + job_id: jobIds.join(), }); - if (!jobConfigResponse || jobConfigResponse.jobs.length < 1) { + if (!body || body.jobs.length < 1) { throw Boom.notFound(`Unable to find anomaly detector jobs ${jobIds.join(', ')}`); } - const jobIdsWithStopOnWarnSet = jobConfigResponse.jobs + const jobIdsWithStopOnWarnSet = body.jobs .filter( (jobConfig) => jobConfig.analysis_config?.per_partition_categorization?.stop_on_warn === true @@ -543,7 +543,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }, }, ]; - const results: SearchResponse = await callAsInternalUser('search', { + const { body: results } = await asInternalUser.search>({ index: ML_RESULTS_INDEX_PATTERN, size: 0, body: { @@ -594,7 +594,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie getCategoryExamples, getLatestBucketTimestampByJob, getMaxAnomalyScore, - getPartitionFieldsValues: getPartitionFieldsValuesFactory(mlClusterClient), + getPartitionFieldsValues: getPartitionFieldsValuesFactory(client), getCategorizerStats, getCategoryStoppedPartitions, }; diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 76128341e6ddc8..39672f5b188bcf 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -9,18 +9,16 @@ import { CoreSetup, CoreStart, Plugin, - ILegacyScopedClusterClient, KibanaRequest, Logger, PluginInitializerContext, - ILegacyCustomClusterClient, CapabilitiesStart, + IClusterClient, } from 'kibana/server'; import { PluginsSetup, RouteInitialization } from './types'; import { PLUGIN_ID, PLUGIN_ICON } from '../common/constants/app'; import { MlCapabilities } from '../common/types/capabilities'; -import { elasticsearchJsPlugin } from './client/elasticsearch_ml'; import { initMlTelemetry } from './lib/telemetry'; import { initMlServerLog } from './client/log'; import { initSampleDataSets } from './lib/sample_data_sets'; @@ -50,17 +48,7 @@ import { setupCapabilitiesSwitcher } from './lib/capabilities'; import { registerKibanaSettings } from './lib/register_settings'; import { inferenceRoutes } from './routes/inference'; -declare module 'kibana/server' { - interface RequestHandlerContext { - [PLUGIN_ID]?: { - mlClient: ILegacyScopedClusterClient; - }; - } -} - -export interface MlPluginSetup extends SharedServices { - mlClient: ILegacyCustomClusterClient; -} +export type MlPluginSetup = SharedServices; export type MlPluginStart = void; export class MlServerPlugin implements Plugin { @@ -68,6 +56,7 @@ export class MlServerPlugin implements Plugin { - return { - mlClient: mlClient.asScoped(request), - }; - }); - const routeInit: RouteInitialization = { router: coreSetup.http.createRouter(), mlLicense: this.mlLicense, @@ -176,13 +154,19 @@ export class MlServerPlugin implements Plugin this.clusterClient + ), }; } public start(coreStart: CoreStart): MlPluginStart { this.capabilities = coreStart.capabilities; + this.clusterClient = coreStart.elasticsearch.client; } public stop() { diff --git a/x-pack/plugins/ml/server/routes/annotations.ts b/x-pack/plugins/ml/server/routes/annotations.ts index a6de80bb7e5e26..5c4b36164fbb0c 100644 --- a/x-pack/plugins/ml/server/routes/annotations.ts +++ b/x-pack/plugins/ml/server/routes/annotations.ts @@ -58,9 +58,9 @@ export function annotationRoutes( tags: ['access:ml:canGetAnnotations'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { getAnnotations } = annotationServiceProvider(legacyClient); + const { getAnnotations } = annotationServiceProvider(client); const resp = await getAnnotations(request.body); return response.ok({ @@ -91,14 +91,14 @@ export function annotationRoutes( tags: ['access:ml:canCreateAnnotation'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable(legacyClient); + const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable(client); if (annotationsFeatureAvailable === false) { throw getAnnotationsFeatureUnavailableErrorMessage(); } - const { indexAnnotation } = annotationServiceProvider(legacyClient); + const { indexAnnotation } = annotationServiceProvider(client); const currentUser = securityPlugin !== undefined ? securityPlugin.authc.getCurrentUser(request) : {}; @@ -134,15 +134,15 @@ export function annotationRoutes( tags: ['access:ml:canDeleteAnnotation'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable(legacyClient); + const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable(client); if (annotationsFeatureAvailable === false) { throw getAnnotationsFeatureUnavailableErrorMessage(); } const annotationId = request.params.annotationId; - const { deleteAnnotation } = annotationServiceProvider(legacyClient); + const { deleteAnnotation } = annotationServiceProvider(client); const resp = await deleteAnnotation(annotationId); return response.ok({ diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index 0027bec910134d..251e465eafccc8 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -5,6 +5,7 @@ */ import { schema } from '@kbn/config-schema'; +import { RequestParams } from '@elastic/elasticsearch'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { @@ -20,6 +21,7 @@ import { getModelSnapshotsSchema, updateModelSnapshotSchema, } from './schemas/anomaly_detectors_schema'; + /** * Routes for the anomaly detectors */ @@ -42,11 +44,11 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ response, client }) => { try { - const results = await legacyClient.callAsInternalUser('ml.jobs'); + const { body } = await client.asInternalUser.ml.getJobs(); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -73,12 +75,12 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { jobId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.jobs', { jobId }); + const { body } = await client.asInternalUser.ml.getJobs({ job_id: jobId }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -104,11 +106,11 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.jobStats'); + const { body } = await client.asInternalUser.ml.getJobStats(); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -135,12 +137,12 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { jobId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.jobStats', { jobId }); + const { body } = await client.asInternalUser.ml.getJobStats({ job_id: jobId }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -171,15 +173,15 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { jobId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.addJob', { - jobId, + const { body } = await client.asInternalUser.ml.putJob({ + job_id: jobId, body: request.body, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -208,15 +210,15 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canUpdateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { jobId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.updateJob', { - jobId, + const { body } = await client.asInternalUser.ml.updateJob({ + job_id: jobId, body: request.body, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -243,14 +245,12 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canOpenJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { jobId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.openJob', { - jobId, - }); + const { body } = await client.asInternalUser.ml.openJob({ job_id: jobId }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -277,18 +277,18 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCloseJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const options: { jobId: string; force?: boolean } = { - jobId: request.params.jobId, + const options: RequestParams.MlCloseJob = { + job_id: request.params.jobId, }; const force = request.query.force; if (force !== undefined) { options.force = force; } - const results = await legacyClient.callAsInternalUser('ml.closeJob', options); + const { body } = await client.asInternalUser.ml.closeJob(options); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -315,18 +315,19 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canDeleteJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const options: { jobId: string; force?: boolean } = { - jobId: request.params.jobId, + const options: RequestParams.MlDeleteJob = { + job_id: request.params.jobId, + wait_for_completion: false, }; const force = request.query.force; if (force !== undefined) { options.force = force; } - const results = await legacyClient.callAsInternalUser('ml.deleteJob', options); + const { body } = await client.asInternalUser.ml.deleteJob(options); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -351,13 +352,11 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.validateDetector', { - body: request.body, - }); + const { body } = await client.asInternalUser.ml.validateDetector({ body: request.body }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -386,16 +385,16 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canForecastJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const jobId = request.params.jobId; const duration = request.body.duration; - const results = await legacyClient.callAsInternalUser('ml.forecast', { - jobId, + const { body } = await client.asInternalUser.ml.forecast({ + job_id: jobId, duration, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -427,14 +426,14 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.records', { - jobId: request.params.jobId, + const { body } = await client.asInternalUser.ml.getRecords({ + job_id: request.params.jobId, body: request.body, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -466,15 +465,15 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.buckets', { - jobId: request.params.jobId, + const { body } = await client.asInternalUser.ml.getBuckets({ + job_id: request.params.jobId, timestamp: request.params.timestamp, body: request.body, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -506,17 +505,17 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.overallBuckets', { - jobId: request.params.jobId, + const { body } = await client.asInternalUser.ml.getOverallBuckets({ + job_id: request.params.jobId, top_n: request.body.topN, bucket_span: request.body.bucketSpan, start: request.body.start, end: request.body.end, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -543,14 +542,14 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.categories', { - jobId: request.params.jobId, - categoryId: request.params.categoryId, + const { body } = await client.asInternalUser.ml.getCategories({ + job_id: request.params.jobId, + category_id: request.params.categoryId, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -577,13 +576,13 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.modelSnapshots', { - jobId: request.params.jobId, + const { body } = await client.asInternalUser.ml.getModelSnapshots({ + job_id: request.params.jobId, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -610,14 +609,14 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.modelSnapshots', { - jobId: request.params.jobId, - snapshotId: request.params.snapshotId, + const { body } = await client.asInternalUser.ml.getModelSnapshots({ + job_id: request.params.jobId, + snapshot_id: request.params.snapshotId, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -646,15 +645,15 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.updateModelSnapshot', { - jobId: request.params.jobId, - snapshotId: request.params.snapshotId, + const { body } = await client.asInternalUser.ml.updateModelSnapshot({ + job_id: request.params.jobId, + snapshot_id: request.params.snapshotId, body: request.body, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -681,14 +680,14 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.deleteModelSnapshot', { - jobId: request.params.jobId, - snapshotId: request.params.snapshotId, + const { body } = await client.asInternalUser.ml.deleteModelSnapshot({ + job_id: request.params.jobId, + snapshot_id: request.params.snapshotId, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); diff --git a/x-pack/plugins/ml/server/routes/calendars.ts b/x-pack/plugins/ml/server/routes/calendars.ts index 3beb6e437b2eed..2c95ce6fb59ece 100644 --- a/x-pack/plugins/ml/server/routes/calendars.ts +++ b/x-pack/plugins/ml/server/routes/calendars.ts @@ -4,43 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { calendarSchema, calendarIdSchema, calendarIdsSchema } from './schemas/calendars_schema'; import { CalendarManager, Calendar, FormCalendar } from '../models/calendar'; -function getAllCalendars(legacyClient: ILegacyScopedClusterClient) { - const cal = new CalendarManager(legacyClient); +function getAllCalendars(client: IScopedClusterClient) { + const cal = new CalendarManager(client); return cal.getAllCalendars(); } -function getCalendar(legacyClient: ILegacyScopedClusterClient, calendarId: string) { - const cal = new CalendarManager(legacyClient); +function getCalendar(client: IScopedClusterClient, calendarId: string) { + const cal = new CalendarManager(client); return cal.getCalendar(calendarId); } -function newCalendar(legacyClient: ILegacyScopedClusterClient, calendar: FormCalendar) { - const cal = new CalendarManager(legacyClient); +function newCalendar(client: IScopedClusterClient, calendar: FormCalendar) { + const cal = new CalendarManager(client); return cal.newCalendar(calendar); } -function updateCalendar( - legacyClient: ILegacyScopedClusterClient, - calendarId: string, - calendar: Calendar -) { - const cal = new CalendarManager(legacyClient); +function updateCalendar(client: IScopedClusterClient, calendarId: string, calendar: Calendar) { + const cal = new CalendarManager(client); return cal.updateCalendar(calendarId, calendar); } -function deleteCalendar(legacyClient: ILegacyScopedClusterClient, calendarId: string) { - const cal = new CalendarManager(legacyClient); +function deleteCalendar(client: IScopedClusterClient, calendarId: string) { + const cal = new CalendarManager(client); return cal.deleteCalendar(calendarId); } -function getCalendarsByIds(legacyClient: ILegacyScopedClusterClient, calendarIds: string) { - const cal = new CalendarManager(legacyClient); +function getCalendarsByIds(client: IScopedClusterClient, calendarIds: string) { + const cal = new CalendarManager(client); return cal.getCalendarsByIds(calendarIds); } @@ -60,9 +56,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetCalendars'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const resp = await getAllCalendars(legacyClient); + const resp = await getAllCalendars(client); return response.ok({ body: resp, @@ -92,15 +88,15 @@ export function calendars({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetCalendars'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { let returnValue; try { const calendarIds = request.params.calendarIds.split(','); if (calendarIds.length === 1) { - returnValue = await getCalendar(legacyClient, calendarIds[0]); + returnValue = await getCalendar(client, calendarIds[0]); } else { - returnValue = await getCalendarsByIds(legacyClient, calendarIds); + returnValue = await getCalendarsByIds(client, calendarIds); } return response.ok({ @@ -131,10 +127,10 @@ export function calendars({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateCalendar'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const body = request.body; - const resp = await newCalendar(legacyClient, body); + const resp = await newCalendar(client, body); return response.ok({ body: resp, @@ -166,11 +162,11 @@ export function calendars({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateCalendar'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { calendarId } = request.params; const body = request.body; - const resp = await updateCalendar(legacyClient, calendarId, body); + const resp = await updateCalendar(client, calendarId, body); return response.ok({ body: resp, @@ -200,10 +196,10 @@ export function calendars({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canDeleteCalendar'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { calendarId } = request.params; - const resp = await deleteCalendar(legacyClient, calendarId); + const resp = await deleteCalendar(client, calendarId); return response.ok({ body: resp, diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 75d48056cf4584..dea4803e8275e9 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandlerContext, ILegacyScopedClusterClient } from 'kibana/server'; +import { RequestHandlerContext, IScopedClusterClient } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; import { analyticsAuditMessagesProvider } from '../models/data_frame_analytics/analytics_audit_messages'; import { RouteInitialization } from '../types'; @@ -36,13 +36,14 @@ function deleteDestIndexPatternById(context: RequestHandlerContext, indexPattern */ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitialization) { async function userCanDeleteIndex( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, destinationIndex: string ): Promise { if (!mlLicense.isSecurityEnabled()) { return true; } - const privilege = await legacyClient.callAsCurrentUser('ml.privilegeCheck', { + + const { body } = await client.asCurrentUser.security.hasPrivileges({ body: { index: [ { @@ -52,10 +53,8 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat ], }, }); - if (!privilege) { - return false; - } - return privilege.has_all_requested === true; + + return body?.has_all_requested === true; } /** @@ -76,11 +75,11 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canGetDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.getDataFrameAnalytics'); + const { body } = await client.asInternalUser.ml.getDataFrameAnalytics({ size: 1000 }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -107,14 +106,14 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canGetDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { analyticsId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.getDataFrameAnalytics', { - analyticsId, + const { body } = await client.asInternalUser.ml.getDataFrameAnalytics({ + id: analyticsId, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -137,11 +136,11 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canGetDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.getDataFrameAnalyticsStats'); + const { body } = await client.asInternalUser.ml.getDataFrameAnalyticsStats({ size: 1000 }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -168,14 +167,14 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canGetDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { analyticsId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.getDataFrameAnalyticsStats', { - analyticsId, + const { body } = await client.asInternalUser.ml.getDataFrameAnalyticsStats({ + id: analyticsId, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -205,16 +204,18 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canCreateDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { analyticsId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.createDataFrameAnalytics', { - body: request.body, - analyticsId, - ...getAuthorizationHeader(request), - }); + const { body } = await client.asInternalUser.ml.putDataFrameAnalytics( + { + id: analyticsId, + body: request.body, + }, + getAuthorizationHeader(request) + ); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -241,14 +242,16 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canGetDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.evaluateDataFrameAnalytics', { - body: request.body, - ...getAuthorizationHeader(request), - }); + const { body } = await client.asInternalUser.ml.evaluateDataFrame( + { + body: request.body, + }, + getAuthorizationHeader(request) + ); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -276,13 +279,13 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canCreateDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.explainDataFrameAnalytics', { + const { body } = await client.asInternalUser.ml.explainDataFrameAnalytics({ body: request.body, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -310,7 +313,7 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canDeleteDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response, context }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response, context }) => { try { const { analyticsId } = request.params; const { deleteDestIndex, deleteDestIndexPattern } = request.query; @@ -324,11 +327,11 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat // Check if analyticsId is valid and get destination index if (deleteDestIndex || deleteDestIndexPattern) { try { - const dfa = await legacyClient.callAsInternalUser('ml.getDataFrameAnalytics', { - analyticsId, + const { body } = await client.asInternalUser.ml.getDataFrameAnalytics({ + id: analyticsId, }); - if (Array.isArray(dfa.data_frame_analytics) && dfa.data_frame_analytics.length > 0) { - destinationIndex = dfa.data_frame_analytics[0].dest.index; + if (Array.isArray(body.data_frame_analytics) && body.data_frame_analytics.length > 0) { + destinationIndex = body.data_frame_analytics[0].dest.index; } } catch (e) { return response.customError(wrapError(e)); @@ -337,11 +340,11 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat // If user checks box to delete the destinationIndex associated with the job if (destinationIndex && deleteDestIndex) { // Verify if user has privilege to delete the destination index - const userCanDeleteDestIndex = await userCanDeleteIndex(legacyClient, destinationIndex); + const userCanDeleteDestIndex = await userCanDeleteIndex(client, destinationIndex); // If user does have privilege to delete the index, then delete the index if (userCanDeleteDestIndex) { try { - await legacyClient.callAsCurrentUser('indices.delete', { + await client.asCurrentUser.indices.delete({ index: destinationIndex, }); destIndexDeleted.success = true; @@ -370,8 +373,8 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat // Delete the data frame analytics try { - await legacyClient.callAsInternalUser('ml.deleteDataFrameAnalytics', { - analyticsId, + await client.asInternalUser.ml.deleteDataFrameAnalytics({ + id: analyticsId, }); analyticsJobDeleted.success = true; } catch (deleteDFAError) { @@ -413,14 +416,14 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canStartStopDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { analyticsId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.startDataFrameAnalytics', { - analyticsId, + const { body } = await client.asInternalUser.ml.startDataFrameAnalytics({ + id: analyticsId, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -449,10 +452,10 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canStartStopDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const options: { analyticsId: string; force?: boolean | undefined } = { - analyticsId: request.params.analyticsId, + const options: { id: string; force?: boolean | undefined } = { + id: request.params.analyticsId, }; // @ts-expect-error TODO: update types if (request.url?.query?.force !== undefined) { @@ -460,9 +463,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat options.force = request.url.query.force; } - const results = await legacyClient.callAsInternalUser('ml.stopDataFrameAnalytics', options); + const { body } = await client.asInternalUser.ml.stopDataFrameAnalytics(options); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -490,16 +493,18 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canCreateDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { analyticsId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.updateDataFrameAnalytics', { - body: request.body, - analyticsId, - ...getAuthorizationHeader(request), - }); + const { body } = await client.asInternalUser.ml.updateDataFrameAnalytics( + { + id: analyticsId, + body: request.body, + }, + getAuthorizationHeader(request) + ); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -526,10 +531,10 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canGetDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { analyticsId } = request.params; - const { getAnalyticsAuditMessages } = analyticsAuditMessagesProvider(legacyClient); + const { getAnalyticsAuditMessages } = analyticsAuditMessagesProvider(client); const results = await getAnalyticsAuditMessages(analyticsId); return response.ok({ diff --git a/x-pack/plugins/ml/server/routes/data_visualizer.ts b/x-pack/plugins/ml/server/routes/data_visualizer.ts index 6355285127f06c..a697fe017f1920 100644 --- a/x-pack/plugins/ml/server/routes/data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/data_visualizer.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; import { DataVisualizer } from '../models/data_visualizer'; import { Field, HistogramField } from '../models/data_visualizer/data_visualizer'; @@ -17,7 +17,7 @@ import { import { RouteInitialization } from '../types'; function getOverallStats( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, indexPatternTitle: string, query: object, aggregatableFields: string[], @@ -27,7 +27,7 @@ function getOverallStats( earliestMs: number, latestMs: number ) { - const dv = new DataVisualizer(legacyClient); + const dv = new DataVisualizer(client); return dv.getOverallStats( indexPatternTitle, query, @@ -41,7 +41,7 @@ function getOverallStats( } function getStatsForFields( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, indexPatternTitle: string, query: any, fields: Field[], @@ -52,7 +52,7 @@ function getStatsForFields( interval: number, maxExamples: number ) { - const dv = new DataVisualizer(legacyClient); + const dv = new DataVisualizer(client); return dv.getStatsForFields( indexPatternTitle, query, @@ -67,13 +67,13 @@ function getStatsForFields( } function getHistogramsForFields( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, indexPatternTitle: string, query: any, fields: HistogramField[], samplerShardSize: number ) { - const dv = new DataVisualizer(legacyClient); + const dv = new DataVisualizer(client); return dv.getHistogramsForFields(indexPatternTitle, query, fields, samplerShardSize); } @@ -104,7 +104,7 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canAccessML'], }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { const { params: { indexPatternTitle }, @@ -112,7 +112,7 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) } = request; const results = await getHistogramsForFields( - legacyClient, + client, indexPatternTitle, query, fields, @@ -151,7 +151,7 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canAccessML'], }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { const { params: { indexPatternTitle }, @@ -168,7 +168,7 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) } = request; const results = await getStatsForFields( - legacyClient, + client, indexPatternTitle, query, fields, @@ -216,7 +216,7 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canAccessML'], }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { const { params: { indexPatternTitle }, @@ -232,7 +232,7 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) } = request; const results = await getOverallStats( - legacyClient, + client, indexPatternTitle, query, aggregatableFields, diff --git a/x-pack/plugins/ml/server/routes/datafeeds.ts b/x-pack/plugins/ml/server/routes/datafeeds.ts index 47a9afc2244d96..df2aa9e79d71b2 100644 --- a/x-pack/plugins/ml/server/routes/datafeeds.ts +++ b/x-pack/plugins/ml/server/routes/datafeeds.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RequestParams } from '@elastic/elasticsearch'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { @@ -33,12 +34,12 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetDatafeeds'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const resp = await legacyClient.callAsInternalUser('ml.datafeeds'); + const { body } = await client.asInternalUser.ml.getDatafeeds(); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -65,13 +66,13 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetDatafeeds'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const datafeedId = request.params.datafeedId; - const resp = await legacyClient.callAsInternalUser('ml.datafeeds', { datafeedId }); + const { body } = await client.asInternalUser.ml.getDatafeeds({ datafeed_id: datafeedId }); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -94,12 +95,12 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetDatafeeds'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await legacyClient.callAsInternalUser('ml.datafeedStats'); + const { body } = await client.asInternalUser.ml.getDatafeedStats(); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -126,15 +127,15 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetDatafeeds'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const datafeedId = request.params.datafeedId; - const resp = await legacyClient.callAsInternalUser('ml.datafeedStats', { - datafeedId, + const { body } = await client.asInternalUser.ml.getDatafeedStats({ + datafeed_id: datafeedId, }); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -163,17 +164,19 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const datafeedId = request.params.datafeedId; - const resp = await legacyClient.callAsInternalUser('ml.addDatafeed', { - datafeedId, - body: request.body, - ...getAuthorizationHeader(request), - }); + const { body } = await client.asInternalUser.ml.putDatafeed( + { + datafeed_id: datafeedId, + body: request.body, + }, + getAuthorizationHeader(request) + ); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -202,17 +205,19 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canUpdateDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const datafeedId = request.params.datafeedId; - const resp = await legacyClient.callAsInternalUser('ml.updateDatafeed', { - datafeedId, - body: request.body, - ...getAuthorizationHeader(request), - }); + const { body } = await client.asInternalUser.ml.updateDatafeed( + { + datafeed_id: datafeedId, + body: request.body, + }, + getAuthorizationHeader(request) + ); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -241,20 +246,20 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canDeleteDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const options: { datafeedId: string; force?: boolean } = { - datafeedId: request.params.jobId, + const options: RequestParams.MlDeleteDatafeed = { + datafeed_id: request.params.jobId, }; const force = request.query.force; if (force !== undefined) { options.force = force; } - const resp = await legacyClient.callAsInternalUser('ml.deleteDatafeed', options); + const { body } = await client.asInternalUser.ml.deleteDatafeed(options); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -283,19 +288,19 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canStartStopDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const datafeedId = request.params.datafeedId; const { start, end } = request.body; - const resp = await legacyClient.callAsInternalUser('ml.startDatafeed', { - datafeedId, + const { body } = await client.asInternalUser.ml.startDatafeed({ + datafeed_id: datafeedId, start, end, }); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -322,16 +327,16 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canStartStopDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const datafeedId = request.params.datafeedId; - const resp = await legacyClient.callAsInternalUser('ml.stopDatafeed', { - datafeedId, + const { body } = await client.asInternalUser.ml.stopDatafeed({ + datafeed_id: datafeedId, }); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -358,16 +363,18 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canPreviewDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const datafeedId = request.params.datafeedId; - const resp = await legacyClient.callAsInternalUser('ml.datafeedPreview', { - datafeedId, - ...getAuthorizationHeader(request), - }); + const { body } = await client.asInternalUser.ml.previewDatafeed( + { + datafeed_id: datafeedId, + }, + getAuthorizationHeader(request) + ); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); diff --git a/x-pack/plugins/ml/server/routes/fields_service.ts b/x-pack/plugins/ml/server/routes/fields_service.ts index 0595b31d5bbbce..e1bd8a5736d828 100644 --- a/x-pack/plugins/ml/server/routes/fields_service.ts +++ b/x-pack/plugins/ml/server/routes/fields_service.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { @@ -13,14 +13,14 @@ import { } from './schemas/fields_service_schema'; import { fieldsServiceProvider } from '../models/fields_service'; -function getCardinalityOfFields(legacyClient: ILegacyScopedClusterClient, payload: any) { - const fs = fieldsServiceProvider(legacyClient); +function getCardinalityOfFields(client: IScopedClusterClient, payload: any) { + const fs = fieldsServiceProvider(client); const { index, fieldNames, query, timeFieldName, earliestMs, latestMs } = payload; return fs.getCardinalityOfFields(index, fieldNames, query, timeFieldName, earliestMs, latestMs); } -function getTimeFieldRange(legacyClient: ILegacyScopedClusterClient, payload: any) { - const fs = fieldsServiceProvider(legacyClient); +function getTimeFieldRange(client: IScopedClusterClient, payload: any) { + const fs = fieldsServiceProvider(client); const { index, timeFieldName, query } = payload; return fs.getTimeFieldRange(index, timeFieldName, query); } @@ -50,9 +50,9 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canAccessML'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getCardinalityOfFields(legacyClient, request.body); + const resp = await getCardinalityOfFields(client, request.body); return response.ok({ body: resp, @@ -85,9 +85,9 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canAccessML'], }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getTimeFieldRange(legacyClient, request.body); + const resp = await getTimeFieldRange(client, request.body); return response.ok({ body: resp, diff --git a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts index 88949fecbc7df5..4c1ee87e96fc56 100644 --- a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts @@ -5,7 +5,7 @@ */ import { schema } from '@kbn/config-schema'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { MAX_FILE_SIZE_BYTES } from '../../common/constants/file_datavisualizer'; import { InputOverrides, @@ -28,17 +28,13 @@ import { importFileQuerySchema, } from './schemas/file_data_visualizer_schema'; -function analyzeFiles( - legacyClient: ILegacyScopedClusterClient, - data: InputData, - overrides: InputOverrides -) { - const { analyzeFile } = fileDataVisualizerProvider(legacyClient); +function analyzeFiles(client: IScopedClusterClient, data: InputData, overrides: InputOverrides) { + const { analyzeFile } = fileDataVisualizerProvider(client); return analyzeFile(data, overrides); } function importData( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, id: string, index: string, settings: Settings, @@ -46,7 +42,7 @@ function importData( ingestPipeline: IngestPipelineWrapper, data: InputData ) { - const { importData: importDataFunc } = importDataProvider(legacyClient); + const { importData: importDataFunc } = importDataProvider(client); return importDataFunc(id, index, settings, mappings, ingestPipeline, data); } @@ -78,9 +74,9 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canFindFileStructure'], }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { - const result = await analyzeFiles(legacyClient, request.body, request.query); + const result = await analyzeFiles(client, request.body, request.query); return response.ok({ body: result }); } catch (e) { return response.customError(wrapError(e)); @@ -113,7 +109,7 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canFindFileStructure'], }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { const { id } = request.query; const { index, data, settings, mappings, ingestPipeline } = request.body; @@ -126,7 +122,7 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat } const result = await importData( - legacyClient, + client, id, index, settings, diff --git a/x-pack/plugins/ml/server/routes/filters.ts b/x-pack/plugins/ml/server/routes/filters.ts index bb4f8a2bebaa98..efb4acfa432f99 100644 --- a/x-pack/plugins/ml/server/routes/filters.ts +++ b/x-pack/plugins/ml/server/routes/filters.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { createFilterSchema, filterIdSchema, updateFilterSchema } from './schemas/filters_schema'; @@ -12,37 +12,33 @@ import { FilterManager, FormFilter } from '../models/filter'; // TODO - add function for returning a list of just the filter IDs. // TODO - add function for returning a list of filter IDs plus item count. -function getAllFilters(legacyClient: ILegacyScopedClusterClient) { - const mgr = new FilterManager(legacyClient); +function getAllFilters(client: IScopedClusterClient) { + const mgr = new FilterManager(client); return mgr.getAllFilters(); } -function getAllFilterStats(legacyClient: ILegacyScopedClusterClient) { - const mgr = new FilterManager(legacyClient); +function getAllFilterStats(client: IScopedClusterClient) { + const mgr = new FilterManager(client); return mgr.getAllFilterStats(); } -function getFilter(legacyClient: ILegacyScopedClusterClient, filterId: string) { - const mgr = new FilterManager(legacyClient); +function getFilter(client: IScopedClusterClient, filterId: string) { + const mgr = new FilterManager(client); return mgr.getFilter(filterId); } -function newFilter(legacyClient: ILegacyScopedClusterClient, filter: FormFilter) { - const mgr = new FilterManager(legacyClient); +function newFilter(client: IScopedClusterClient, filter: FormFilter) { + const mgr = new FilterManager(client); return mgr.newFilter(filter); } -function updateFilter( - legacyClient: ILegacyScopedClusterClient, - filterId: string, - filter: FormFilter -) { - const mgr = new FilterManager(legacyClient); +function updateFilter(client: IScopedClusterClient, filterId: string, filter: FormFilter) { + const mgr = new FilterManager(client); return mgr.updateFilter(filterId, filter); } -function deleteFilter(legacyClient: ILegacyScopedClusterClient, filterId: string) { - const mgr = new FilterManager(legacyClient); +function deleteFilter(client: IScopedClusterClient, filterId: string) { + const mgr = new FilterManager(client); return mgr.deleteFilter(filterId); } @@ -65,9 +61,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetFilters'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const resp = await getAllFilters(legacyClient); + const resp = await getAllFilters(client); return response.ok({ body: resp, @@ -100,9 +96,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetFilters'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getFilter(legacyClient, request.params.filterId); + const resp = await getFilter(client, request.params.filterId); return response.ok({ body: resp, }); @@ -134,10 +130,10 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateFilter'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const body = request.body; - const resp = await newFilter(legacyClient, body); + const resp = await newFilter(client, body); return response.ok({ body: resp, @@ -172,11 +168,11 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateFilter'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { filterId } = request.params; const body = request.body; - const resp = await updateFilter(legacyClient, filterId, body); + const resp = await updateFilter(client, filterId, body); return response.ok({ body: resp, @@ -206,10 +202,10 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canDeleteFilter'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { filterId } = request.params; - const resp = await deleteFilter(legacyClient, filterId); + const resp = await deleteFilter(client, filterId); return response.ok({ body: resp, @@ -239,9 +235,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetFilters'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const resp = await getAllFilterStats(legacyClient); + const resp = await getAllFilterStats(client); return response.ok({ body: resp, diff --git a/x-pack/plugins/ml/server/routes/indices.ts b/x-pack/plugins/ml/server/routes/indices.ts index 6a759cb97f3086..ee817c492dbd4e 100644 --- a/x-pack/plugins/ml/server/routes/indices.ts +++ b/x-pack/plugins/ml/server/routes/indices.ts @@ -31,7 +31,7 @@ export function indicesRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canAccessML'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { body: { index, fields: requestFields }, @@ -40,8 +40,8 @@ export function indicesRoutes({ router, mlLicense }: RouteInitialization) { requestFields !== undefined && Array.isArray(requestFields) ? requestFields.join(',') : '*'; - const result = await legacyClient.callAsCurrentUser('fieldCaps', { index, fields }); - return response.ok({ body: result }); + const { body } = await client.asInternalUser.fieldCaps({ index, fields }); + return response.ok({ body }); } catch (e) { return response.customError(wrapError(e)); } diff --git a/x-pack/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts index 2313decfabd5b9..0c90081f8e755f 100644 --- a/x-pack/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts @@ -37,9 +37,9 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { getJobAuditMessages } = jobAuditMessagesProvider(legacyClient); + const { getJobAuditMessages } = jobAuditMessagesProvider(client); const { jobId } = request.params; const { from } = request.query; const resp = await getJobAuditMessages(jobId, from); @@ -72,9 +72,9 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { getJobAuditMessages } = jobAuditMessagesProvider(legacyClient); + const { getJobAuditMessages } = jobAuditMessagesProvider(client); const { from } = request.query; const resp = await getJobAuditMessages(undefined, from); diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index 3d560fc857e954..3c7f35b871b101 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -48,9 +48,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canStartStopDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { forceStartDatafeeds } = jobServiceProvider(legacyClient); + const { forceStartDatafeeds } = jobServiceProvider(client); const { datafeedIds, start, end } = request.body; const resp = await forceStartDatafeeds(datafeedIds, start, end); @@ -82,9 +82,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canStartStopDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { stopDatafeeds } = jobServiceProvider(legacyClient); + const { stopDatafeeds } = jobServiceProvider(client); const { datafeedIds } = request.body; const resp = await stopDatafeeds(datafeedIds); @@ -116,9 +116,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canDeleteJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { deleteJobs } = jobServiceProvider(legacyClient); + const { deleteJobs } = jobServiceProvider(client); const { jobIds } = request.body; const resp = await deleteJobs(jobIds); @@ -150,9 +150,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCloseJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { closeJobs } = jobServiceProvider(legacyClient); + const { closeJobs } = jobServiceProvider(client); const { jobIds } = request.body; const resp = await closeJobs(jobIds); @@ -184,9 +184,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCloseJob', 'access:ml:canStartStopDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { forceStopAndCloseJob } = jobServiceProvider(legacyClient); + const { forceStopAndCloseJob } = jobServiceProvider(client); const { jobId } = request.body; const resp = await forceStopAndCloseJob(jobId); @@ -223,9 +223,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { jobsSummary } = jobServiceProvider(legacyClient); + const { jobsSummary } = jobServiceProvider(client); const { jobIds } = request.body; const resp = await jobsSummary(jobIds); @@ -257,9 +257,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const { jobsWithTimerange } = jobServiceProvider(legacyClient); + const { jobsWithTimerange } = jobServiceProvider(client); const resp = await jobsWithTimerange(); return response.ok({ @@ -290,9 +290,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { createFullJobsList } = jobServiceProvider(legacyClient); + const { createFullJobsList } = jobServiceProvider(client); const { jobIds } = request.body; const resp = await createFullJobsList(jobIds); @@ -320,9 +320,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const { getAllGroups } = jobServiceProvider(legacyClient); + const { getAllGroups } = jobServiceProvider(client); const resp = await getAllGroups(); return response.ok({ @@ -353,9 +353,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canUpdateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { updateGroups } = jobServiceProvider(legacyClient); + const { updateGroups } = jobServiceProvider(client); const { jobs } = request.body; const resp = await updateGroups(jobs); @@ -383,9 +383,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const { deletingJobTasks } = jobServiceProvider(legacyClient); + const { deletingJobTasks } = jobServiceProvider(client); const resp = await deletingJobTasks(); return response.ok({ @@ -416,9 +416,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { jobsExist } = jobServiceProvider(legacyClient); + const { jobsExist } = jobServiceProvider(client); const { jobIds } = request.body; const resp = await jobsExist(jobIds); @@ -449,12 +449,12 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response, context }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response, context }) => { try { const { indexPattern } = request.params; const isRollup = request.query.rollup === 'true'; const savedObjectsClient = context.core.savedObjects.client; - const { newJobCaps } = jobServiceProvider(legacyClient); + const { newJobCaps } = jobServiceProvider(client); const resp = await newJobCaps(indexPattern, isRollup, savedObjectsClient); return response.ok({ @@ -485,7 +485,7 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { indexPatternTitle, @@ -499,7 +499,7 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { splitFieldValue, } = request.body; - const { newJobLineChart } = jobServiceProvider(legacyClient); + const { newJobLineChart } = jobServiceProvider(client); const resp = await newJobLineChart( indexPatternTitle, timeField, @@ -540,7 +540,7 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { indexPatternTitle, @@ -553,7 +553,7 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { splitFieldName, } = request.body; - const { newJobPopulationChart } = jobServiceProvider(legacyClient); + const { newJobPopulationChart } = jobServiceProvider(client); const resp = await newJobPopulationChart( indexPatternTitle, timeField, @@ -589,9 +589,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const { getAllJobAndGroupIds } = jobServiceProvider(legacyClient); + const { getAllJobAndGroupIds } = jobServiceProvider(client); const resp = await getAllJobAndGroupIds(); return response.ok({ @@ -622,9 +622,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { getLookBackProgress } = jobServiceProvider(legacyClient); + const { getLookBackProgress } = jobServiceProvider(client); const { jobId, start, end } = request.body; const resp = await getLookBackProgress(jobId, start, end); @@ -656,9 +656,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { validateCategoryExamples } = categorizationExamplesProvider(legacyClient); + const { validateCategoryExamples } = categorizationExamplesProvider(client); const { indexPatternTitle, timeField, @@ -709,9 +709,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { topCategories } = jobServiceProvider(legacyClient); + const { topCategories } = jobServiceProvider(client); const { jobId, count } = request.body; const resp = await topCategories(jobId, count); @@ -743,9 +743,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateJob', 'access:ml:canStartStopDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { revertModelSnapshot } = jobServiceProvider(legacyClient); + const { revertModelSnapshot } = jobServiceProvider(client); const { jobId, snapshotId, diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts index 6da052663a002b..b52043595327be 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; import { AnalysisConfig } from '../../common/types/anomaly_detection_jobs'; import { wrapError } from '../client/error_wrapper'; @@ -27,12 +27,12 @@ type CalculateModelMemoryLimitPayload = TypeOf; */ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, version: string) { function calculateModelMemoryLimit( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, payload: CalculateModelMemoryLimitPayload ) { const { analysisConfig, indexPattern, query, timeFieldName, earliestMs, latestMs } = payload; - return calculateModelMemoryLimitProvider(legacyClient)( + return calculateModelMemoryLimitProvider(client)( analysisConfig as AnalysisConfig, indexPattern, query, @@ -61,10 +61,10 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { let errorResp; - const resp = await estimateBucketSpanFactory(legacyClient)(request.body) + const resp = await estimateBucketSpanFactory(client)(request.body) // this catch gets triggered when the estimation code runs without error // but isn't able to come up with a bucket span estimation. // this doesn't return a HTTP error but an object with an error message @@ -109,9 +109,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await calculateModelMemoryLimit(legacyClient, request.body); + const resp = await calculateModelMemoryLimit(client, request.body); return response.ok({ body: resp, @@ -141,9 +141,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await validateCardinality(legacyClient, request.body); + const resp = await validateCardinality(client, request.body); return response.ok({ body: resp, @@ -173,11 +173,11 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { // version corresponds to the version used in documentation links. const resp = await validateJob( - legacyClient, + client, request.body, version, mlLicense.isSecurityEnabled() === false diff --git a/x-pack/plugins/ml/server/routes/modules.ts b/x-pack/plugins/ml/server/routes/modules.ts index 23e37d22130292..72a4c5e428c2b8 100644 --- a/x-pack/plugins/ml/server/routes/modules.ts +++ b/x-pack/plugins/ml/server/routes/modules.ts @@ -6,11 +6,7 @@ import { TypeOf } from '@kbn/config-schema'; -import { - ILegacyScopedClusterClient, - KibanaRequest, - SavedObjectsClientContract, -} from 'kibana/server'; +import { IScopedClusterClient, KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; import { DatafeedOverride, JobOverride } from '../../common/types/modules'; import { wrapError } from '../client/error_wrapper'; import { DataRecognizer } from '../models/data_recognizer'; @@ -23,22 +19,22 @@ import { import { RouteInitialization } from '../types'; function recognize( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, savedObjectsClient: SavedObjectsClientContract, request: KibanaRequest, indexPatternTitle: string ) { - const dr = new DataRecognizer(legacyClient, savedObjectsClient, request); + const dr = new DataRecognizer(client, savedObjectsClient, request); return dr.findMatches(indexPatternTitle); } function getModule( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, savedObjectsClient: SavedObjectsClientContract, request: KibanaRequest, moduleId: string ) { - const dr = new DataRecognizer(legacyClient, savedObjectsClient, request); + const dr = new DataRecognizer(client, savedObjectsClient, request); if (moduleId === undefined) { return dr.listModules(); } else { @@ -47,7 +43,7 @@ function getModule( } function setup( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, savedObjectsClient: SavedObjectsClientContract, request: KibanaRequest, moduleId: string, @@ -63,7 +59,7 @@ function setup( datafeedOverrides?: DatafeedOverride | DatafeedOverride[], estimateModelMemory?: boolean ) { - const dr = new DataRecognizer(legacyClient, savedObjectsClient, request); + const dr = new DataRecognizer(client, savedObjectsClient, request); return dr.setup( moduleId, prefix, @@ -81,12 +77,12 @@ function setup( } function dataRecognizerJobsExist( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, savedObjectsClient: SavedObjectsClientContract, request: KibanaRequest, moduleId: string ) { - const dr = new DataRecognizer(legacyClient, savedObjectsClient, request); + const dr = new DataRecognizer(client, savedObjectsClient, request); return dr.dataRecognizerJobsExist(moduleId); } @@ -131,11 +127,11 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response, context }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response, context }) => { try { const { indexPatternTitle } = request.params; const results = await recognize( - legacyClient, + client, context.core.savedObjects.client, request, indexPatternTitle @@ -266,7 +262,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response, context }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response, context }) => { try { let { moduleId } = request.params; if (moduleId === '') { @@ -275,7 +271,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { moduleId = undefined; } const results = await getModule( - legacyClient, + client, context.core.savedObjects.client, request, moduleId @@ -439,7 +435,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response, context }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response, context }) => { try { const { moduleId } = request.params; @@ -458,7 +454,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { } = request.body as TypeOf; const result = await setup( - legacyClient, + client, context.core.savedObjects.client, request, moduleId, @@ -544,11 +540,11 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response, context }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response, context }) => { try { const { moduleId } = request.params; const result = await dataRecognizerJobsExist( - legacyClient, + client, context.core.savedObjects.client, request, moduleId diff --git a/x-pack/plugins/ml/server/routes/notification_settings.ts b/x-pack/plugins/ml/server/routes/notification_settings.ts index 09c145d6257a85..5bb51bf9e596cc 100644 --- a/x-pack/plugins/ml/server/routes/notification_settings.ts +++ b/x-pack/plugins/ml/server/routes/notification_settings.ts @@ -26,16 +26,15 @@ export function notificationRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canAccessML'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const params = { - includeDefaults: true, - filterPath: '**.xpack.notification', - }; - const resp = await legacyClient.callAsCurrentUser('cluster.getSettings', params); + const { body } = await client.asCurrentUser.cluster.getSettings({ + include_defaults: true, + filter_path: '**.xpack.notification', + }); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); diff --git a/x-pack/plugins/ml/server/routes/results_service.ts b/x-pack/plugins/ml/server/routes/results_service.ts index 2af37c17f714a1..4e34320d513339 100644 --- a/x-pack/plugins/ml/server/routes/results_service.ts +++ b/x-pack/plugins/ml/server/routes/results_service.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; @@ -23,8 +23,8 @@ import { getCategorizerStoppedPartitionsSchema, } from './schemas/results_service_schema'; -function getAnomaliesTableData(legacyClient: ILegacyScopedClusterClient, payload: any) { - const rs = resultsServiceProvider(legacyClient); +function getAnomaliesTableData(client: IScopedClusterClient, payload: any) { + const rs = resultsServiceProvider(client); const { jobIds, criteriaFields, @@ -53,39 +53,39 @@ function getAnomaliesTableData(legacyClient: ILegacyScopedClusterClient, payload ); } -function getCategoryDefinition(legacyClient: ILegacyScopedClusterClient, payload: any) { - const rs = resultsServiceProvider(legacyClient); +function getCategoryDefinition(client: IScopedClusterClient, payload: any) { + const rs = resultsServiceProvider(client); return rs.getCategoryDefinition(payload.jobId, payload.categoryId); } -function getCategoryExamples(legacyClient: ILegacyScopedClusterClient, payload: any) { - const rs = resultsServiceProvider(legacyClient); +function getCategoryExamples(client: IScopedClusterClient, payload: any) { + const rs = resultsServiceProvider(client); const { jobId, categoryIds, maxExamples } = payload; return rs.getCategoryExamples(jobId, categoryIds, maxExamples); } -function getMaxAnomalyScore(legacyClient: ILegacyScopedClusterClient, payload: any) { - const rs = resultsServiceProvider(legacyClient); +function getMaxAnomalyScore(client: IScopedClusterClient, payload: any) { + const rs = resultsServiceProvider(client); const { jobIds, earliestMs, latestMs } = payload; return rs.getMaxAnomalyScore(jobIds, earliestMs, latestMs); } -function getPartitionFieldsValues(legacyClient: ILegacyScopedClusterClient, payload: any) { - const rs = resultsServiceProvider(legacyClient); +function getPartitionFieldsValues(client: IScopedClusterClient, payload: any) { + const rs = resultsServiceProvider(client); const { jobId, searchTerm, criteriaFields, earliestMs, latestMs } = payload; return rs.getPartitionFieldsValues(jobId, searchTerm, criteriaFields, earliestMs, latestMs); } -function getCategorizerStats(legacyClient: ILegacyScopedClusterClient, params: any, query: any) { +function getCategorizerStats(client: IScopedClusterClient, params: any, query: any) { const { jobId } = params; const { partitionByValue } = query; - const rs = resultsServiceProvider(legacyClient); + const rs = resultsServiceProvider(client); return rs.getCategorizerStats(jobId, partitionByValue); } -function getCategoryStoppedPartitions(legacyClient: ILegacyScopedClusterClient, payload: any) { +function getCategoryStoppedPartitions(client: IScopedClusterClient, payload: any) { const { jobIds, fieldToBucket } = payload; - const rs = resultsServiceProvider(legacyClient); + const rs = resultsServiceProvider(client); return rs.getCategoryStoppedPartitions(jobIds, fieldToBucket); } @@ -112,9 +112,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getAnomaliesTableData(legacyClient, request.body); + const resp = await getAnomaliesTableData(client, request.body); return response.ok({ body: resp, @@ -144,9 +144,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getCategoryDefinition(legacyClient, request.body); + const resp = await getCategoryDefinition(client, request.body); return response.ok({ body: resp, @@ -176,9 +176,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getMaxAnomalyScore(legacyClient, request.body); + const resp = await getMaxAnomalyScore(client, request.body); return response.ok({ body: resp, @@ -208,9 +208,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getCategoryExamples(legacyClient, request.body); + const resp = await getCategoryExamples(client, request.body); return response.ok({ body: resp, @@ -240,9 +240,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getPartitionFieldsValues(legacyClient, request.body); + const resp = await getPartitionFieldsValues(client, request.body); return response.ok({ body: resp, @@ -269,14 +269,14 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { - const body = { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { + const { body } = await client.asInternalUser.search({ ...request.body, index: ML_RESULTS_INDEX_PATTERN, - }; + }); try { return response.ok({ - body: await legacyClient.callAsInternalUser('search', body), + body, }); } catch (error) { return response.customError(wrapError(error)); @@ -304,9 +304,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getCategorizerStats(legacyClient, request.params, request.query); + const resp = await getCategorizerStats(client, request.params, request.query); return response.ok({ body: resp, }); @@ -334,9 +334,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getCategoryStoppedPartitions(legacyClient, request.body); + const resp = await getCategoryStoppedPartitions(client, request.body); return response.ok({ body: resp, }); diff --git a/x-pack/plugins/ml/server/routes/system.ts b/x-pack/plugins/ml/server/routes/system.ts index 273b86163245fc..3a66f60943bb39 100644 --- a/x-pack/plugins/ml/server/routes/system.ts +++ b/x-pack/plugins/ml/server/routes/system.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { Request } from 'hapi'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; import { mlLog } from '../client/log'; import { capabilitiesProvider } from '../lib/capabilities'; @@ -21,17 +21,16 @@ export function systemRoutes( { router, mlLicense }: RouteInitialization, { spaces, cloud, resolveMlCapabilities }: SystemRouteDeps ) { - async function getNodeCount(legacyClient: ILegacyScopedClusterClient) { - const filterPath = 'nodes.*.attributes'; - const resp = await legacyClient.callAsInternalUser('nodes.info', { - filterPath, + async function getNodeCount(client: IScopedClusterClient) { + const { body } = await client.asInternalUser.nodes.info({ + filter_path: 'nodes.*.attributes', }); let count = 0; - if (typeof resp.nodes === 'object') { - Object.keys(resp.nodes).forEach((k) => { - if (resp.nodes[k].attributes !== undefined) { - const maxOpenJobs = resp.nodes[k].attributes['ml.max_open_jobs']; + if (typeof body.nodes === 'object') { + Object.keys(body.nodes).forEach((k) => { + if (body.nodes[k].attributes !== undefined) { + const maxOpenJobs = body.nodes[k].attributes['ml.max_open_jobs']; if (maxOpenJobs !== null && maxOpenJobs > 0) { count++; } @@ -58,15 +57,15 @@ export function systemRoutes( tags: ['access:ml:canAccessML'], }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { - const { callAsCurrentUser, callAsInternalUser } = legacyClient; + const { asCurrentUser, asInternalUser } = client; let upgradeInProgress = false; try { - const info = await callAsInternalUser('ml.info'); + const { body } = await asInternalUser.ml.info(); // if ml indices are currently being migrated, upgrade_mode will be set to true // pass this back with the privileges to allow for the disabling of UI controls. - upgradeInProgress = info.upgrade_mode === true; + upgradeInProgress = body.upgrade_mode === true; } catch (error) { // if the ml.info check fails, it could be due to the user having insufficient privileges // most likely they do not have the ml_user role and therefore will be blocked from using @@ -90,11 +89,12 @@ export function systemRoutes( }, }); } else { - const body = request.body; - const resp = await callAsCurrentUser('ml.privilegeCheck', { body }); - resp.upgradeInProgress = upgradeInProgress; + const { body } = await asCurrentUser.security.hasPrivileges({ body: request.body }); return response.ok({ - body: resp, + body: { + ...body, + upgradeInProgress, + }, }); } } catch (error) { @@ -115,7 +115,7 @@ export function systemRoutes( path: '/api/ml/ml_capabilities', validate: false, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { // if spaces is disabled force isMlEnabledInSpace to be true const { isMlEnabledInSpace } = @@ -129,7 +129,7 @@ export function systemRoutes( } const { getCapabilities } = capabilitiesProvider( - legacyClient, + client, mlCapabilities, mlLicense, isMlEnabledInSpace @@ -159,10 +159,10 @@ export function systemRoutes( }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { return response.ok({ - body: await getNodeCount(legacyClient), + body: await getNodeCount(client), }); } catch (e) { return response.customError(wrapError(e)); @@ -185,12 +185,12 @@ export function systemRoutes( tags: ['access:ml:canAccessML'], }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { - const info = await legacyClient.callAsInternalUser('ml.info'); + const { body } = await client.asInternalUser.ml.info(); const cloudId = cloud && cloud.cloudId; return response.ok({ - body: { ...info, cloudId }, + body: { ...body, cloudId }, }); } catch (error) { return response.customError(wrapError(error)); @@ -216,10 +216,11 @@ export function systemRoutes( tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { + const { body } = await client.asCurrentUser.search(request.body); return response.ok({ - body: await legacyClient.callAsCurrentUser('search', request.body), + body, }); } catch (error) { return response.customError(wrapError(error)); @@ -243,22 +244,21 @@ export function systemRoutes( tags: ['access:ml:canAccessML'], }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { const { index } = request.body; const options = { index: [index], fields: ['*'], - ignoreUnavailable: true, - allowNoIndices: true, - ignore: 404, + ignore_unavailable: true, + allow_no_indices: true, }; - const fieldsResult = await legacyClient.callAsCurrentUser('fieldCaps', options); + const { body } = await client.asCurrentUser.fieldCaps(options); const result = { exists: false }; - if (Array.isArray(fieldsResult.indices) && fieldsResult.indices.length !== 0) { + if (Array.isArray(body.indices) && body.indices.length !== 0) { result.exists = true; } diff --git a/x-pack/plugins/ml/server/shared_services/errors.ts b/x-pack/plugins/ml/server/shared_services/errors.ts new file mode 100644 index 00000000000000..f15a85a490a460 --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/errors.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export class MLClusterClientUninitialized extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/x-pack/plugins/ml/server/shared_services/license_checks/errors.ts b/x-pack/plugins/ml/server/shared_services/license_checks/errors.ts new file mode 100644 index 00000000000000..18e7dab43fda7e --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/license_checks/errors.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable max-classes-per-file */ + +export class InsufficientFullLicenseError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export class InsufficientBasicLicenseError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/x-pack/plugins/ml/server/shared_services/license_checks/index.ts b/x-pack/plugins/ml/server/shared_services/license_checks/index.ts new file mode 100644 index 00000000000000..6b837dadf5c0de --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/license_checks/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { LicenseCheck, licenseChecks } from './license_checks'; +export { InsufficientBasicLicenseError, InsufficientFullLicenseError } from './errors'; diff --git a/x-pack/plugins/ml/server/shared_services/license_checks.ts b/x-pack/plugins/ml/server/shared_services/license_checks/license_checks.ts similarity index 66% rename from x-pack/plugins/ml/server/shared_services/license_checks.ts rename to x-pack/plugins/ml/server/shared_services/license_checks/license_checks.ts index 191124ffa5f3af..3d9de1ef70f2d6 100644 --- a/x-pack/plugins/ml/server/shared_services/license_checks.ts +++ b/x-pack/plugins/ml/server/shared_services/license_checks/license_checks.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MlServerLicense } from '../lib/license'; +import { MlServerLicense } from '../../lib/license'; +import { InsufficientFullLicenseError, InsufficientBasicLicenseError } from './errors'; export type LicenseCheck = () => void; @@ -14,12 +15,12 @@ export function licenseChecks( return { isFullLicense() { if (mlLicense.isFullLicense() === false) { - throw Error('Platinum, Enterprise or trial license needed'); + throw new InsufficientFullLicenseError('Platinum, Enterprise or trial license needed'); } }, isMinimumLicense() { if (mlLicense.isMinimumLicense() === false) { - throw Error('Basic license needed'); + throw new InsufficientBasicLicenseError('Basic license needed'); } }, }; diff --git a/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts b/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts index 603b4fba17adbf..53898cb64d07f6 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts @@ -4,40 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient, KibanaRequest } from 'kibana/server'; +import { KibanaRequest } from 'kibana/server'; import { Job } from '../../../common/types/anomaly_detection_jobs'; -import { SharedServicesChecks } from '../shared_services'; +import { GetGuards } from '../shared_services'; export interface AnomalyDetectorsProvider { anomalyDetectorsProvider( - mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest ): { jobs(jobId?: string): Promise<{ count: number; jobs: Job[] }>; }; } -export function getAnomalyDetectorsProvider({ - isFullLicense, - getHasMlCapabilities, -}: SharedServicesChecks): AnomalyDetectorsProvider { +export function getAnomalyDetectorsProvider(getGuards: GetGuards): AnomalyDetectorsProvider { return { - anomalyDetectorsProvider(mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest) { - // APM is using this service in anomaly alert, kibana alerting doesn't provide request object - // So we are adding a dummy request for now - // TODO: Remove this once kibana alerting provides request object - const hasMlCapabilities = - request.params !== 'DummyKibanaRequest' - ? getHasMlCapabilities(request) - : (_caps: string[]) => Promise.resolve(); + anomalyDetectorsProvider(request: KibanaRequest) { return { async jobs(jobId?: string) { - isFullLicense(); - await hasMlCapabilities(['canGetJobs']); - return mlClusterClient.callAsInternalUser( - 'ml.jobs', - jobId !== undefined ? { jobId } : {} - ); + return await getGuards(request) + .isFullLicense() + .hasMlCapabilities(['canGetJobs']) + .ok(async ({ scopedClient }) => { + const { body } = await scopedClient.asInternalUser.ml.getJobs<{ + count: number; + jobs: Job[]; + }>(jobId !== undefined ? { job_id: jobId } : undefined); + return body; + }); }, }; }, diff --git a/x-pack/plugins/ml/server/shared_services/providers/job_service.ts b/x-pack/plugins/ml/server/shared_services/providers/job_service.ts index c734dcc1583a1b..2897bf08717f85 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/job_service.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/job_service.ts @@ -4,38 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient, KibanaRequest } from 'kibana/server'; +import { KibanaRequest } from 'kibana/server'; import { jobServiceProvider } from '../../models/job_service'; -import { SharedServicesChecks } from '../shared_services'; +import { GetGuards } from '../shared_services'; type OrigJobServiceProvider = ReturnType; export interface JobServiceProvider { jobServiceProvider( - mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest ): { jobsSummary: OrigJobServiceProvider['jobsSummary']; }; } -export function getJobServiceProvider({ - isFullLicense, - getHasMlCapabilities, -}: SharedServicesChecks): JobServiceProvider { +export function getJobServiceProvider(getGuards: GetGuards): JobServiceProvider { return { - jobServiceProvider(mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest) { - // const hasMlCapabilities = getHasMlCapabilities(request); - const { jobsSummary } = jobServiceProvider(mlClusterClient); + jobServiceProvider(request: KibanaRequest) { return { - async jobsSummary(...args) { - isFullLicense(); - // Removed while https://github.com/elastic/kibana/issues/64588 exists. - // SIEM are calling this endpoint with a dummy request object from their alerting - // integration and currently alerting does not supply a request object. - // await hasMlCapabilities(['canGetJobs']); - - return jobsSummary(...args); + jobsSummary: async (...args) => { + return await getGuards(request) + .isFullLicense() + .hasMlCapabilities(['canGetJobs']) + .ok(async ({ scopedClient }) => { + const { jobsSummary } = jobServiceProvider(scopedClient); + return jobsSummary(...args); + }); }, }; }, diff --git a/x-pack/plugins/ml/server/shared_services/providers/modules.ts b/x-pack/plugins/ml/server/shared_services/providers/modules.ts index fb7d59f9c8218d..a727d96433f1db 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/modules.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/modules.ts @@ -4,23 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ILegacyScopedClusterClient, - KibanaRequest, - SavedObjectsClientContract, -} from 'kibana/server'; +import { IScopedClusterClient, KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; import { DataRecognizer } from '../../models/data_recognizer'; -import { SharedServicesChecks } from '../shared_services'; +import { GetGuards } from '../shared_services'; import { moduleIdParamSchema, setupModuleBodySchema } from '../../routes/schemas/modules'; -import { HasMlCapabilities } from '../../lib/capabilities'; export type ModuleSetupPayload = TypeOf & TypeOf; export interface ModulesProvider { modulesProvider( - mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest, savedObjectsClient: SavedObjectsClientContract ): { @@ -31,61 +25,58 @@ export interface ModulesProvider { }; } -export function getModulesProvider({ - isFullLicense, - getHasMlCapabilities, -}: SharedServicesChecks): ModulesProvider { +export function getModulesProvider(getGuards: GetGuards): ModulesProvider { return { - modulesProvider( - mlClusterClient: ILegacyScopedClusterClient, - request: KibanaRequest, - savedObjectsClient: SavedObjectsClientContract - ) { - let hasMlCapabilities: HasMlCapabilities; - if (request.params === 'DummyKibanaRequest') { - hasMlCapabilities = () => Promise.resolve(); - } else { - hasMlCapabilities = getHasMlCapabilities(request); - } - const dr = dataRecognizerFactory(mlClusterClient, savedObjectsClient, request); - + modulesProvider(request: KibanaRequest, savedObjectsClient: SavedObjectsClientContract) { return { async recognize(...args) { - isFullLicense(); - await hasMlCapabilities(['canCreateJob']); - - return dr.findMatches(...args); + return await getGuards(request) + .isFullLicense() + .hasMlCapabilities(['canGetJobs']) + .ok(async ({ scopedClient }) => { + const dr = dataRecognizerFactory(scopedClient, savedObjectsClient, request); + return dr.findMatches(...args); + }); }, async getModule(moduleId: string) { - isFullLicense(); - await hasMlCapabilities(['canGetJobs']); - - return dr.getModule(moduleId); + return await getGuards(request) + .isFullLicense() + .hasMlCapabilities(['canGetJobs']) + .ok(async ({ scopedClient }) => { + const dr = dataRecognizerFactory(scopedClient, savedObjectsClient, request); + return dr.getModule(moduleId); + }); }, async listModules() { - isFullLicense(); - await hasMlCapabilities(['canGetJobs']); - - return dr.listModules(); + return await getGuards(request) + .isFullLicense() + .hasMlCapabilities(['canGetJobs']) + .ok(async ({ scopedClient }) => { + const dr = dataRecognizerFactory(scopedClient, savedObjectsClient, request); + return dr.listModules(); + }); }, async setup(payload: ModuleSetupPayload) { - isFullLicense(); - await hasMlCapabilities(['canCreateJob']); - - return dr.setup( - payload.moduleId, - payload.prefix, - payload.groups, - payload.indexPatternName, - payload.query, - payload.useDedicatedIndex, - payload.startDatafeed, - payload.start, - payload.end, - payload.jobOverrides, - payload.datafeedOverrides, - payload.estimateModelMemory - ); + return await getGuards(request) + .isFullLicense() + .hasMlCapabilities(['canCreateJob']) + .ok(async ({ scopedClient }) => { + const dr = dataRecognizerFactory(scopedClient, savedObjectsClient, request); + return dr.setup( + payload.moduleId, + payload.prefix, + payload.groups, + payload.indexPatternName, + payload.query, + payload.useDedicatedIndex, + payload.startDatafeed, + payload.start, + payload.end, + payload.jobOverrides, + payload.datafeedOverrides, + payload.estimateModelMemory + ); + }); }, }; }, @@ -93,9 +84,9 @@ export function getModulesProvider({ } function dataRecognizerFactory( - mlClusterClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, savedObjectsClient: SavedObjectsClientContract, request: KibanaRequest ) { - return new DataRecognizer(mlClusterClient, savedObjectsClient, request); + return new DataRecognizer(client, savedObjectsClient, request); } diff --git a/x-pack/plugins/ml/server/shared_services/providers/results_service.ts b/x-pack/plugins/ml/server/shared_services/providers/results_service.ts index 6af4eb008567ac..5536765cc376a2 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/results_service.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/results_service.ts @@ -4,41 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient, KibanaRequest } from 'kibana/server'; +import { KibanaRequest } from 'kibana/server'; import { resultsServiceProvider } from '../../models/results_service'; -import { SharedServicesChecks } from '../shared_services'; +import { GetGuards } from '../shared_services'; type OrigResultsServiceProvider = ReturnType; export interface ResultsServiceProvider { resultsServiceProvider( - mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest ): { getAnomaliesTableData: OrigResultsServiceProvider['getAnomaliesTableData']; }; } -export function getResultsServiceProvider({ - isFullLicense, - getHasMlCapabilities, -}: SharedServicesChecks): ResultsServiceProvider { +export function getResultsServiceProvider(getGuards: GetGuards): ResultsServiceProvider { return { - resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest) { - // Uptime is using this service in anomaly alert, kibana alerting doesn't provide request object - // So we are adding a dummy request for now - // TODO: Remove this once kibana alerting provides request object - const hasMlCapabilities = - request.params !== 'DummyKibanaRequest' - ? getHasMlCapabilities(request) - : (_caps: string[]) => Promise.resolve(); - - const { getAnomaliesTableData } = resultsServiceProvider(mlClusterClient); + resultsServiceProvider(request: KibanaRequest) { return { async getAnomaliesTableData(...args) { - isFullLicense(); - await hasMlCapabilities(['canGetJobs']); - return getAnomaliesTableData(...args); + return await getGuards(request) + .isFullLicense() + .hasMlCapabilities(['canGetJobs']) + .ok(async ({ scopedClient }) => { + const { getAnomaliesTableData } = resultsServiceProvider(scopedClient); + return getAnomaliesTableData(...args); + }); }, }; }, diff --git a/x-pack/plugins/ml/server/shared_services/providers/system.ts b/x-pack/plugins/ml/server/shared_services/providers/system.ts index d292abc438a2f3..3217ff13787b00 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/system.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/system.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient, KibanaRequest } from 'kibana/server'; -import { SearchResponse, SearchParams } from 'elasticsearch'; +import { KibanaRequest } from 'kibana/server'; +import { SearchResponse } from 'elasticsearch'; +import { RequestParams } from '@elastic/elasticsearch'; import { MlServerLicense } from '../../lib/license'; import { CloudSetup } from '../../../../cloud/server'; import { spacesUtilsProvider } from '../../lib/spaces_utils'; @@ -14,73 +15,79 @@ import { capabilitiesProvider } from '../../lib/capabilities'; import { MlInfoResponse } from '../../../common/types/ml_server_info'; import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; import { MlCapabilitiesResponse, ResolveMlCapabilities } from '../../../common/types/capabilities'; -import { SharedServicesChecks } from '../shared_services'; +import { GetGuards } from '../shared_services'; export interface MlSystemProvider { mlSystemProvider( - mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest ): { mlCapabilities(): Promise; mlInfo(): Promise; - mlAnomalySearch(searchParams: SearchParams): Promise>; + mlAnomalySearch(searchParams: RequestParams.Search): Promise>; }; } export function getMlSystemProvider( - { isMinimumLicense, isFullLicense, getHasMlCapabilities }: SharedServicesChecks, + getGuards: GetGuards, mlLicense: MlServerLicense, spaces: SpacesPluginSetup | undefined, cloud: CloudSetup | undefined, resolveMlCapabilities: ResolveMlCapabilities ): MlSystemProvider { return { - mlSystemProvider(mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest) { - // const hasMlCapabilities = getHasMlCapabilities(request); - const { callAsInternalUser } = mlClusterClient; + mlSystemProvider(request: KibanaRequest) { return { async mlCapabilities() { - isMinimumLicense(); + return await getGuards(request) + .isMinimumLicense() + .ok(async ({ scopedClient }) => { + const { isMlEnabledInSpace } = + spaces !== undefined + ? spacesUtilsProvider(spaces, request) + : { isMlEnabledInSpace: async () => true }; - const { isMlEnabledInSpace } = - spaces !== undefined - ? spacesUtilsProvider(spaces, request) - : { isMlEnabledInSpace: async () => true }; + const mlCapabilities = await resolveMlCapabilities(request); + if (mlCapabilities === null) { + throw new Error('mlCapabilities is not defined'); + } - const mlCapabilities = await resolveMlCapabilities(request); - if (mlCapabilities === null) { - throw new Error('mlCapabilities is not defined'); - } - - const { getCapabilities } = capabilitiesProvider( - mlClusterClient, - mlCapabilities, - mlLicense, - isMlEnabledInSpace - ); - return getCapabilities(); + const { getCapabilities } = capabilitiesProvider( + scopedClient, + mlCapabilities, + mlLicense, + isMlEnabledInSpace + ); + return getCapabilities(); + }); }, async mlInfo(): Promise { - isMinimumLicense(); + return await getGuards(request) + .isMinimumLicense() + .ok(async ({ scopedClient }) => { + const { asInternalUser } = scopedClient; - const info = await callAsInternalUser('ml.info'); - const cloudId = cloud && cloud.cloudId; - return { - ...info, - cloudId, - }; + const { body: info } = await asInternalUser.ml.info(); + const cloudId = cloud && cloud.cloudId; + return { + ...info, + cloudId, + }; + }); }, - async mlAnomalySearch(searchParams: SearchParams): Promise> { - isFullLicense(); - // Removed while https://github.com/elastic/kibana/issues/64588 exists. - // SIEM are calling this endpoint with a dummy request object from their alerting - // integration and currently alerting does not supply a request object. - // await hasMlCapabilities(['canAccessML']); - - return callAsInternalUser('search', { - ...searchParams, - index: ML_RESULTS_INDEX_PATTERN, - }); + async mlAnomalySearch( + searchParams: RequestParams.Search + ): Promise> { + return await getGuards(request) + .isFullLicense() + .hasMlCapabilities(['canAccessML']) + .ok(async ({ scopedClient }) => { + const { asInternalUser } = scopedClient; + const { body } = await asInternalUser.search>({ + ...searchParams, + index: ML_RESULTS_INDEX_PATTERN, + }); + return body; + }); }, }; }, diff --git a/x-pack/plugins/ml/server/shared_services/shared_services.ts b/x-pack/plugins/ml/server/shared_services/shared_services.ts index 3345111fad4ae2..4c568e4515a278 100644 --- a/x-pack/plugins/ml/server/shared_services/shared_services.ts +++ b/x-pack/plugins/ml/server/shared_services/shared_services.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest } from 'kibana/server'; +import { IClusterClient, IScopedClusterClient } from 'kibana/server'; +// including KibanaRequest from 'kibana/server' causes an error +// when being used with instanceof +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { KibanaRequest } from '../../.././../../src/core/server/http'; import { MlServerLicense } from '../lib/license'; import { SpacesPluginSetup } from '../../../spaces/server'; @@ -18,8 +22,9 @@ import { AnomalyDetectorsProvider, getAnomalyDetectorsProvider, } from './providers/anomaly_detectors'; -import { ResolveMlCapabilities } from '../../common/types/capabilities'; +import { ResolveMlCapabilities, MlCapabilitiesKey } from '../../common/types/capabilities'; import { hasMlCapabilitiesProvider, HasMlCapabilities } from '../lib/capabilities'; +import { MLClusterClientUninitialized } from './errors'; export type SharedServices = JobServiceProvider & AnomalyDetectorsProvider & @@ -27,31 +32,97 @@ export type SharedServices = JobServiceProvider & ModulesProvider & ResultsServiceProvider; +interface Guards { + isMinimumLicense(): Guards; + isFullLicense(): Guards; + hasMlCapabilities: (caps: MlCapabilitiesKey[]) => Guards; + ok(callback: OkCallback): any; +} + +export type GetGuards = (request: KibanaRequest) => Guards; + export interface SharedServicesChecks { - isFullLicense(): void; - isMinimumLicense(): void; - getHasMlCapabilities(request: KibanaRequest): HasMlCapabilities; + getGuards(request: KibanaRequest): Guards; } +interface OkParams { + scopedClient: IScopedClusterClient; +} + +type OkCallback = (okParams: OkParams) => any; + export function createSharedServices( mlLicense: MlServerLicense, spaces: SpacesPluginSetup | undefined, cloud: CloudSetup, - resolveMlCapabilities: ResolveMlCapabilities + resolveMlCapabilities: ResolveMlCapabilities, + getClusterClient: () => IClusterClient | null ): SharedServices { + const getRequestItems = getRequestItemsProvider(resolveMlCapabilities, getClusterClient); const { isFullLicense, isMinimumLicense } = licenseChecks(mlLicense); - const getHasMlCapabilities = hasMlCapabilitiesProvider(resolveMlCapabilities); - const checks: SharedServicesChecks = { - isFullLicense, - isMinimumLicense, - getHasMlCapabilities, - }; + + function getGuards(request: KibanaRequest): Guards { + const { hasMlCapabilities, scopedClient } = getRequestItems(request); + const asyncGuards: Array> = []; + + const guards: Guards = { + isMinimumLicense: () => { + isMinimumLicense(); + return guards; + }, + isFullLicense: () => { + isFullLicense(); + return guards; + }, + hasMlCapabilities: (caps: MlCapabilitiesKey[]) => { + asyncGuards.push(hasMlCapabilities(caps)); + return guards; + }, + async ok(callback: OkCallback) { + await Promise.all(asyncGuards); + return callback({ scopedClient }); + }, + }; + return guards; + } return { - ...getJobServiceProvider(checks), - ...getAnomalyDetectorsProvider(checks), - ...getModulesProvider(checks), - ...getResultsServiceProvider(checks), - ...getMlSystemProvider(checks, mlLicense, spaces, cloud, resolveMlCapabilities), + ...getJobServiceProvider(getGuards), + ...getAnomalyDetectorsProvider(getGuards), + ...getModulesProvider(getGuards), + ...getResultsServiceProvider(getGuards), + ...getMlSystemProvider(getGuards, mlLicense, spaces, cloud, resolveMlCapabilities), + }; +} + +function getRequestItemsProvider( + resolveMlCapabilities: ResolveMlCapabilities, + getClusterClient: () => IClusterClient | null +) { + return (request: KibanaRequest) => { + const getHasMlCapabilities = hasMlCapabilitiesProvider(resolveMlCapabilities); + let hasMlCapabilities: HasMlCapabilities; + let scopedClient: IScopedClusterClient; + // While https://github.com/elastic/kibana/issues/64588 exists we + // will not receive a real request object when being called from an alert. + // instead a dummy request object will be supplied + const clusterClient = getClusterClient(); + + if (clusterClient === null) { + throw new MLClusterClientUninitialized(`ML's cluster client has not been initialized`); + } + + if (request instanceof KibanaRequest) { + hasMlCapabilities = getHasMlCapabilities(request); + scopedClient = clusterClient.asScoped(request); + } else { + hasMlCapabilities = () => Promise.resolve(); + const { asInternalUser } = clusterClient; + scopedClient = { + asInternalUser, + asCurrentUser: asInternalUser, + }; + } + return { hasMlCapabilities, scopedClient }; }; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts index 18a64a12431b8c..bd9bf50688b58a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts @@ -6,13 +6,12 @@ import dateMath from '@elastic/datemath'; -import { ILegacyScopedClusterClient, KibanaRequest } from '../../../../../../../src/core/server'; +import { KibanaRequest } from '../../../../../../../src/core/server'; import { MlPluginSetup } from '../../../../../ml/server'; import { getAnomalies } from '../../machine_learning'; export const findMlSignals = async ({ ml, - clusterClient, request, jobId, anomalyThreshold, @@ -20,14 +19,13 @@ export const findMlSignals = async ({ to, }: { ml: MlPluginSetup; - clusterClient: ILegacyScopedClusterClient; request: KibanaRequest; jobId: string; anomalyThreshold: number; from: string; to: string; }) => { - const { mlAnomalySearch } = ml.mlSystemProvider(clusterClient, request); + const { mlAnomalySearch } = ml.mlSystemProvider(request); const params = { jobIds: [jobId], threshold: anomalyThreshold, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index c5124edcaf187a..9d688736a98460 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -185,12 +185,12 @@ export const signalRulesAlertType = ({ ); } - const scopedClusterClient = services.getLegacyScopedClusterClient(ml.mlClient); // Using fake KibanaRequest as it is needed to satisfy the ML Services API, but can be empty as it is // currently unused by the jobsSummary function. - const summaryJobs = await ( - await ml.jobServiceProvider(scopedClusterClient, ({} as unknown) as KibanaRequest) - ).jobsSummary([machineLearningJobId]); + const fakeRequest = {} as KibanaRequest; + const summaryJobs = await ml + .jobServiceProvider(fakeRequest) + .jobsSummary([machineLearningJobId]); const jobSummary = summaryJobs.find((job) => job.id === machineLearningJobId); if (jobSummary == null || !isJobStarted(jobSummary.jobState, jobSummary.datafeedState)) { @@ -207,7 +207,6 @@ export const signalRulesAlertType = ({ const anomalyResults = await findMlSignals({ ml, - clusterClient: scopedClusterClient, // Using fake KibanaRequest as it is needed to satisfy the ML Services API, but can be empty as it is // currently unused by the mlAnomalySearch function. request: ({} as unknown) as KibanaRequest, diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/authz.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/authz.ts index 98de9536b1baa3..386eda5281f0c2 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/authz.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/authz.ts @@ -114,7 +114,6 @@ export const isMlAdmin = async ({ request: KibanaRequest; ml: MlPluginSetup; }): Promise => { - const scopedMlClient = ml.mlClient.asScoped(request); - const mlCapabilities = await ml.mlSystemProvider(scopedMlClient, request).mlCapabilities(); + const mlCapabilities = await ml.mlSystemProvider(request).mlCapabilities(); return hasMlAdminPermissions(mlCapabilities); }; diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts index ad2f1e5a8285c4..c17d9ab1bb46ee 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchResponse, SearchParams } from 'elasticsearch'; +import { SearchResponse } from 'elasticsearch'; +import { RequestParams } from '@elastic/elasticsearch'; import { AnomalyRecordDoc as Anomaly } from '../../../../ml/server'; export { Anomaly }; export type AnomalyResults = SearchResponse; -type MlAnomalySearch = (searchParams: SearchParams) => Promise>; +type MlAnomalySearch = (searchParams: RequestParams.Search) => Promise>; export interface AnomaliesSearchParams { jobIds: string[]; diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts index a6d4dc7a38e14a..5cf17af2fa9c0b 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts @@ -172,18 +172,12 @@ export const getMlJobsUsage = async (ml: MlPluginSetup | undefined): Promise module.jobs); - const jobs = await ml.jobServiceProvider(internalMlClient, fakeRequest).jobsSummary(); + const jobs = await ml.jobServiceProvider(fakeRequest).jobsSummary(); jobsUsage = jobs.filter(isSecurityJob).reduce((usage, job) => { const isElastic = moduleJobs.some((moduleJob) => moduleJob.id === job.id); diff --git a/x-pack/plugins/transform/server/routes/api/field_histograms.ts b/x-pack/plugins/transform/server/routes/api/field_histograms.ts index f2fd81368ec17c..2642040c4cd0db 100644 --- a/x-pack/plugins/transform/server/routes/api/field_histograms.ts +++ b/x-pack/plugins/transform/server/routes/api/field_histograms.ts @@ -34,7 +34,7 @@ export function registerFieldHistogramsRoutes({ router, license }: RouteDependen try { const resp = await getHistogramsForFields( - ctx.transform!.dataClient, + ctx.core.elasticsearch.client, indexPatternTitle, query, fields, diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts index 9ed453d2862854..568b0873b8dbb8 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { KibanaRequest } from 'kibana/server'; import moment from 'moment'; import { schema } from '@kbn/config-schema'; -import { ILegacyScopedClusterClient } from 'kibana/server'; import { updateState } from './common'; import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; import { commonStateTranslations, durationAnomalyTranslations } from './translations'; @@ -36,13 +36,11 @@ export const getAnomalySummary = (anomaly: AnomaliesTableRecord, monitorInfo: Pi const getAnomalies = async ( plugins: UptimeCorePlugins, - mlClusterClient: ILegacyScopedClusterClient, params: Record, lastCheckedAt: string ) => { - const { getAnomaliesTableData } = plugins.ml.resultsServiceProvider(mlClusterClient, { - params: 'DummyKibanaRequest', - } as any); + const fakeRequest = {} as KibanaRequest; + const { getAnomaliesTableData } = plugins.ml.resultsServiceProvider(fakeRequest); return await getAnomaliesTableData( [getMLJobId(params.monitorId)], @@ -82,23 +80,12 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory = (_server, _li producer: 'uptime', async executor(options) { const { - services: { - alertInstanceFactory, - callCluster, - savedObjectsClient, - getLegacyScopedClusterClient, - }, + services: { alertInstanceFactory, callCluster, savedObjectsClient }, state, params, } = options; - const { anomalies } = - (await getAnomalies( - plugins, - getLegacyScopedClusterClient(plugins.ml.mlClient), - params, - state.lastCheckedAt - )) ?? {}; + const { anomalies } = (await getAnomalies(plugins, params, state.lastCheckedAt)) ?? {}; const foundAnomalies = anomalies?.length > 0; diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/update.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/update.ts index f4964308cd8c95..7aa5c180c5a02c 100644 --- a/x-pack/test/api_integration/apis/ml/data_frame_analytics/update.ts +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/update.ts @@ -258,7 +258,7 @@ export default ({ getService }: FtrProviderContext) => { description: 'Not found', }; const id = `${jobId}_invalid`; - const message = `[resource_not_found_exception] No known data frame analytics with id [${id}]`; + const message = 'resource_not_found_exception'; const { body } = await supertest .post(`/api/ml/data_frame/analytics/${id}/_update`) diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_histograms.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_histograms.ts index 299f5f93fd2812..a5969bdffbf8a7 100644 --- a/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_histograms.ts +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_histograms.ts @@ -60,8 +60,7 @@ export default ({ getService }: FtrProviderContext) => { responseBody: { statusCode: 404, error: 'Not Found', - message: - '[index_not_found_exception] no such index [ft_farequote_not_exists], with { resource.type="index_or_alias" & resource.id="ft_farequote_not_exists" & index_uuid="_na_" & index="ft_farequote_not_exists" }', + message: 'index_not_found_exception', }, }, }; diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts index 5795eac9637b1e..4ec8ae0429a6bb 100644 --- a/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts @@ -152,8 +152,7 @@ export default ({ getService }: FtrProviderContext) => { responseBody: { statusCode: 404, error: 'Not Found', - message: - '[index_not_found_exception] no such index [ft_farequote_not_exists], with { resource.type="index_or_alias" & resource.id="ft_farequote_not_exists" & index_uuid="_na_" & index="ft_farequote_not_exists" }', + message: 'index_not_found_exception', }, }, }; diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts index fa83807be161a6..af2c53c5772539 100644 --- a/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts @@ -116,8 +116,7 @@ export default ({ getService }: FtrProviderContext) => { responseBody: { statusCode: 404, error: 'Not Found', - message: - '[index_not_found_exception] no such index [ft_farequote_not_exist], with { resource.type="index_or_alias" & resource.id="ft_farequote_not_exist" & index_uuid="_na_" & index="ft_farequote_not_exist" }', + message: 'index_not_found_exception', }, }, }, diff --git a/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts b/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts index 627d9454beeb6f..7dbb4a9d03e4f8 100644 --- a/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts +++ b/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts @@ -78,8 +78,7 @@ export default ({ getService }: FtrProviderContext) => { responseBody: { statusCode: 404, error: 'Not Found', - message: - '[index_not_found_exception] no such index [ft_ecommerce_not_exist], with { resource.type="index_or_alias" & resource.id="ft_ecommerce_not_exist" & index_uuid="_na_" & index="ft_ecommerce_not_exist" }', + message: 'index_not_found_exception', }, }, }, diff --git a/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts b/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts index b1c086ddbb456e..6588d4df570b75 100644 --- a/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts +++ b/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts @@ -81,8 +81,7 @@ export default ({ getService }: FtrProviderContext) => { responseBody: { statusCode: 404, error: 'Not Found', - message: - '[index_not_found_exception] no such index [ft_ecommerce_not_exist], with { resource.type="index_or_alias" & resource.id="ft_ecommerce_not_exist" & index_uuid="_na_" & index="ft_ecommerce_not_exist" }', + message: 'index_not_found_exception', }, }, }, diff --git a/x-pack/test/api_integration/apis/ml/filters/create_filters.ts b/x-pack/test/api_integration/apis/ml/filters/create_filters.ts index dfec7798ffc0ce..598fab2cb7fa6d 100644 --- a/x-pack/test/api_integration/apis/ml/filters/create_filters.ts +++ b/x-pack/test/api_integration/apis/ml/filters/create_filters.ts @@ -73,7 +73,7 @@ export default ({ getService }: FtrProviderContext) => { responseCode: 400, responseBody: { error: 'Bad Request', - message: 'Invalid filter_id', + message: 'status_exception', }, }, }, diff --git a/x-pack/test/api_integration/apis/ml/filters/get_filters.ts b/x-pack/test/api_integration/apis/ml/filters/get_filters.ts index 5d7900ea5e9d93..4a3589634ded93 100644 --- a/x-pack/test/api_integration/apis/ml/filters/get_filters.ts +++ b/x-pack/test/api_integration/apis/ml/filters/get_filters.ts @@ -91,7 +91,7 @@ export default ({ getService }: FtrProviderContext) => { .set(COMMON_REQUEST_HEADERS) .expect(400); expect(body.error).to.eql('Bad Request'); - expect(body.message).to.contain('Unable to find filter'); + expect(body.message).to.contain('resource_not_found_exception'); }); }); }; diff --git a/x-pack/test/api_integration/apis/ml/filters/update_filters.ts b/x-pack/test/api_integration/apis/ml/filters/update_filters.ts index fbbb94d54c035f..6f421ad120b510 100644 --- a/x-pack/test/api_integration/apis/ml/filters/update_filters.ts +++ b/x-pack/test/api_integration/apis/ml/filters/update_filters.ts @@ -111,7 +111,7 @@ export default ({ getService }: FtrProviderContext) => { .send(updateFilterRequestBody) .expect(400); - expect(body.message).to.contain('No filter with id'); + expect(body.message).to.contain('resource_not_found_exception'); }); }); }; diff --git a/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts b/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts index 5b9c5393e81d91..0b7f9cf927d26b 100644 --- a/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts @@ -47,8 +47,8 @@ export default ({ getService }: FtrProviderContext) => { responseCode: 200, responseBody: { - [SINGLE_METRIC_JOB_CONFIG.job_id]: { closed: false, error: { statusCode: 409 } }, - [MULTI_METRIC_JOB_CONFIG.job_id]: { closed: false, error: { statusCode: 409 } }, + [SINGLE_METRIC_JOB_CONFIG.job_id]: { closed: false, error: { status: 409 } }, + [MULTI_METRIC_JOB_CONFIG.job_id]: { closed: false, error: { status: 409 } }, }, }, }, @@ -162,9 +162,7 @@ export default ({ getService }: FtrProviderContext) => { expectedRspJobIds.forEach((id) => { expect(body[id].closed).to.eql(testData.expected.responseBody[id].closed); - expect(body[id].error.statusCode).to.eql( - testData.expected.responseBody[id].error.statusCode - ); + expect(body[id].error.status).to.eql(testData.expected.responseBody[id].error.status); }); // ensure jobs are still open From 70cea4871846e5487cd977a13ef6b488e57ecdfa Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 2 Sep 2020 09:11:05 -0400 Subject: [PATCH 175/216] [ML] DF Analytics jobs list: persist pagination through refresh interval (#75996) * wip: switch analyticsList inMemoryTable to basic and implement search bar * move basicTable settings to custom hook and update types * update types * add types for empty prompt * ensure sorting works * add refresh to analytics management list * ensure table still updates editing job --- .../analytics_list/analytics_list.tsx | 245 ++++++++---------- .../components/analytics_list/common.ts | 1 + .../analytics_list/empty_prompt.tsx | 51 ++++ .../components/analytics_list/use_columns.tsx | 2 - .../analytics_list/use_table_settings.ts | 119 +++++++++ .../analytics_search_bar.tsx | 157 +++++++++++ .../components/analytics_search_bar/index.ts | 7 + .../components/jobs_list/jobs_list.js | 2 +- 8 files changed, 446 insertions(+), 138 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/empty_prompt.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/analytics_search_bar.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/index.ts 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 c4c7a8a4ca11a8..88287b963a0287 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 @@ -5,17 +5,13 @@ */ import React, { FC, useCallback, useState, useEffect } from 'react'; - import { i18n } from '@kbn/i18n'; - import { - Direction, - EuiButton, EuiCallOut, - EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, - EuiInMemoryTable, + EuiBasicTable, + EuiSearchBar, EuiSearchBarProps, EuiSpacer, } from '@elastic/eui'; @@ -43,6 +39,39 @@ import { getGroupQueryText, } from '../../../../../jobs/jobs_list/components/utils'; import { SourceSelection } from '../source_selection'; +import { filterAnalytics, AnalyticsSearchBar } from '../analytics_search_bar'; +import { AnalyticsEmptyPrompt } from './empty_prompt'; +import { useTableSettings } from './use_table_settings'; +import { RefreshAnalyticsListButton } from '../refresh_analytics_list_button'; + +const filters: EuiSearchBarProps['filters'] = [ + { + type: 'field_value_selection', + field: 'job_type', + name: i18n.translate('xpack.ml.dataframe.analyticsList.typeFilter', { + defaultMessage: 'Type', + }), + multiSelect: 'or', + options: Object.values(ANALYSIS_CONFIG_TYPE).map((val) => ({ + value: val, + name: val, + view: getJobTypeBadge(val), + })), + }, + { + type: 'field_value_selection', + field: 'state', + name: i18n.translate('xpack.ml.dataframe.analyticsList.statusFilter', { + defaultMessage: 'Status', + }), + multiSelect: 'or', + options: Object.values(DATA_FRAME_TASK_STATE).map((val) => ({ + value: val, + name: val, + view: getTaskStateBadge(val), + })), + }, +]; function getItemIdToExpandedRowMap( itemIds: DataFrameAnalyticsId[], @@ -70,23 +99,23 @@ export const DataFrameAnalyticsList: FC = ({ const [isInitialized, setIsInitialized] = useState(false); const [isSourceIndexModalVisible, setIsSourceIndexModalVisible] = useState(false); const [isLoading, setIsLoading] = useState(false); - + const [filteredAnalytics, setFilteredAnalytics] = useState<{ + active: boolean; + items: DataFrameAnalyticsListRow[]; + }>({ + active: false, + items: [], + }); const [searchQueryText, setSearchQueryText] = useState(''); - const [analytics, setAnalytics] = useState([]); const [analyticsStats, setAnalyticsStats] = useState( undefined ); const [expandedRowItemIds, setExpandedRowItemIds] = useState([]); - const [errorMessage, setErrorMessage] = useState(undefined); - const [searchError, setSearchError] = useState(undefined); - - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(10); - - const [sortField, setSortField] = useState(DataFrameAnalyticsListColumn.id); - const [sortDirection, setSortDirection] = useState('asc'); + // Query text/job_id based on url but only after getAnalytics is done first + // selectedJobIdFromUrlInitialized makes sure the query is only run once since analytics is being refreshed constantly + const [selectedIdFromUrlInitialized, setSelectedIdFromUrlInitialized] = useState(false); const disabled = !checkPermission('canCreateDataFrameAnalytics') || @@ -100,9 +129,29 @@ export const DataFrameAnalyticsList: FC = ({ blockRefresh ); - // Query text/job_id based on url but only after getAnalytics is done first - // selectedJobIdFromUrlInitialized makes sure the query is only run once since analytics is being refreshed constantly - const [selectedIdFromUrlInitialized, setSelectedIdFromUrlInitialized] = useState(false); + const setQueryClauses = (queryClauses: any) => { + if (queryClauses.length) { + const filtered = filterAnalytics(analytics, queryClauses); + setFilteredAnalytics({ active: true, items: filtered }); + } else { + setFilteredAnalytics({ active: false, items: [] }); + } + }; + + const filterList = () => { + if (searchQueryText !== '' && selectedIdFromUrlInitialized === true) { + // trigger table filtering with query for job id to trigger table filter + const query = EuiSearchBar.Query.parse(searchQueryText); + let clauses: any = []; + if (query && query.ast !== undefined && query.ast.clauses !== undefined) { + clauses = query.ast.clauses; + } + setQueryClauses(clauses); + } else { + setQueryClauses([]); + } + }; + useEffect(() => { if (selectedIdFromUrlInitialized === false && analytics.length > 0) { const { jobId, groupIds } = getSelectedIdFromUrl(window.location.href); @@ -116,9 +165,15 @@ export const DataFrameAnalyticsList: FC = ({ setSelectedIdFromUrlInitialized(true); setSearchQueryText(queryText); + } else { + filterList(); } }, [selectedIdFromUrlInitialized, analytics]); + useEffect(() => { + filterList(); + }, [selectedIdFromUrlInitialized, searchQueryText]); + const getAnalyticsCallback = useCallback(() => getAnalytics(true), []); // Subscribe to the refresh observable to trigger reloading the analytics list. @@ -137,6 +192,10 @@ export const DataFrameAnalyticsList: FC = ({ isMlEnabledInSpace ); + const { onTableChange, pageOfItems, pagination, sorting } = useTableSettings( + filteredAnalytics.active ? filteredAnalytics.items : analytics + ); + // Before the analytics have been loaded for the first time, display the loading indicator only. // Otherwise a user would see 'No data frame analytics found' during the initial loading. if (!isInitialized) { @@ -160,34 +219,10 @@ export const DataFrameAnalyticsList: FC = ({ if (analytics.length === 0) { return ( <> - - {i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptTitle', { - defaultMessage: 'Create your first data frame analytics job', - })} - - } - actions={ - !isManagementTable - ? [ - setIsSourceIndexModalVisible(true)} - isDisabled={disabled} - color="primary" - iconType="plusInCircle" - fill - data-test-subj="mlAnalyticsCreateFirstButton" - > - {i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptButtonText', { - defaultMessage: 'Create job', - })} - , - ] - : [] - } - data-test-subj="mlNoDataFrameAnalyticsFound" + setIsSourceIndexModalVisible(true)} /> {isSourceIndexModalVisible === true && ( setIsSourceIndexModalVisible(false)} /> @@ -196,95 +231,32 @@ export const DataFrameAnalyticsList: FC = ({ ); } - const sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - const itemIdToExpandedRowMap = getItemIdToExpandedRowMap(expandedRowItemIds, analytics); - const pagination = { - initialPageIndex: pageIndex, - initialPageSize: pageSize, - totalItemCount: analytics.length, - pageSizeOptions: [10, 20, 50], - hidePerPageOptions: false, - }; - - const handleSearchOnChange: EuiSearchBarProps['onChange'] = (search) => { - if (search.error !== null) { - setSearchError(search.error.message); - return false; - } - - setSearchError(undefined); - setSearchQueryText(search.queryText); - return true; - }; - - const search: EuiSearchBarProps = { - query: searchQueryText, - onChange: handleSearchOnChange, - box: { - incremental: true, - }, - filters: [ - { - type: 'field_value_selection', - field: 'job_type', - name: i18n.translate('xpack.ml.dataframe.analyticsList.typeFilter', { - defaultMessage: 'Type', - }), - multiSelect: 'or', - options: Object.values(ANALYSIS_CONFIG_TYPE).map((val) => ({ - value: val, - name: val, - view: getJobTypeBadge(val), - })), - }, - { - type: 'field_value_selection', - field: 'state', - name: i18n.translate('xpack.ml.dataframe.analyticsList.statusFilter', { - defaultMessage: 'Status', - }), - multiSelect: 'or', - options: Object.values(DATA_FRAME_TASK_STATE).map((val) => ({ - value: val, - name: val, - view: getTaskStateBadge(val), - })), - }, - ], - }; - - const onTableChange: EuiInMemoryTable['onTableChange'] = ({ - page = { index: 0, size: 10 }, - sort = { field: DataFrameAnalyticsListColumn.id, direction: 'asc' }, - }) => { - const { index, size } = page; - setPageIndex(index); - setPageSize(size); + const stats = analyticsStats && ( + + + + ); - const { field, direction } = sort; - setSortField(field); - setSortDirection(direction); - }; + const managementStats = ( + + + {stats} + + + + + + ); return ( <> {modals} - + {!isManagementTable && } - - {analyticsStats && ( - - - - )} - + {!isManagementTable && stats} + {isManagementTable && managementStats} {!isManagementTable && ( @@ -300,22 +272,25 @@ export const DataFrameAnalyticsList: FC = ({
- + + className="mlAnalyticsTable" columns={columns} - error={searchError} hasActions={false} isExpandable={true} isSelectable={false} - items={analytics} + items={pageOfItems} itemId={DataFrameAnalyticsListColumn.id} itemIdToExpandedRowMap={itemIdToExpandedRowMap} loading={isLoading} - onTableChange={onTableChange} - pagination={pagination} + onChange={onTableChange} + pagination={pagination!} sorting={sorting} - search={search} data-test-subj={isLoading ? 'mlAnalyticsTable loading' : 'mlAnalyticsTable loaded'} rowProps={(item) => ({ 'data-test-subj': `mlAnalyticsTableRow row-${item.id}`, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts index 774864ae964a8e..994357412510df 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts @@ -26,6 +26,7 @@ export type Clause = Parameters[0]; type ExtractClauseType = T extends (x: any) => x is infer Type ? Type : never; export type TermClause = ExtractClauseType; export type FieldClause = ExtractClauseType; +export type Value = Parameters[0]; interface ProgressSection { phase: string; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/empty_prompt.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/empty_prompt.tsx new file mode 100644 index 00000000000000..fb173697b45726 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/empty_prompt.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, { FC } from 'react'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface Props { + disabled: boolean; + isManagementTable: boolean; + onCreateFirstJobClick: () => void; +} + +export const AnalyticsEmptyPrompt: FC = ({ + disabled, + isManagementTable, + onCreateFirstJobClick, +}) => ( + + {i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptTitle', { + defaultMessage: 'Create your first data frame analytics job', + })} + + } + actions={ + !isManagementTable + ? [ + + {i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptButtonText', { + defaultMessage: 'Create job', + })} + , + ] + : [] + } + data-test-subj="mlNoDataFrameAnalyticsFound" + /> +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx index 7001681b6917aa..ef1d373a55a124 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx @@ -23,7 +23,6 @@ import { getJobIdUrl, TAB_IDS } from '../../../../../util/get_selected_ids_url'; import { getAnalysisType, DataFrameAnalyticsId } from '../../../../common'; import { - getDataFrameAnalyticsProgress, getDataFrameAnalyticsProgressPhase, isDataFrameAnalyticsFailed, isDataFrameAnalyticsRunning, @@ -76,7 +75,6 @@ export const progressColumn = { name: i18n.translate('xpack.ml.dataframe.analyticsList.progress', { defaultMessage: 'Progress', }), - sortable: (item: DataFrameAnalyticsListRow) => getDataFrameAnalyticsProgress(item.stats), truncateText: true, render(item: DataFrameAnalyticsListRow) { const { currentPhase, progress, totalPhases } = getDataFrameAnalyticsProgressPhase(item.stats); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts new file mode 100644 index 00000000000000..57eb9f6857053f --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts @@ -0,0 +1,119 @@ +/* + * 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 } from 'react'; +import { Direction, EuiBasicTableProps, EuiTableSortingType } from '@elastic/eui'; +import sortBy from 'lodash/sortBy'; +import get from 'lodash/get'; +import { DataFrameAnalyticsListColumn, DataFrameAnalyticsListRow } from './common'; + +const PAGE_SIZE = 10; +const PAGE_SIZE_OPTIONS = [10, 25, 50]; + +const jobPropertyMap = { + ID: 'id', + Status: 'state', + Type: 'job_type', +}; + +interface AnalyticsBasicTableSettings { + pageIndex: number; + pageSize: number; + totalItemCount: number; + hidePerPageOptions: boolean; + sortField: string; + sortDirection: Direction; +} + +interface UseTableSettingsReturnValue { + onTableChange: EuiBasicTableProps['onChange']; + pageOfItems: DataFrameAnalyticsListRow[]; + pagination: EuiBasicTableProps['pagination']; + sorting: EuiTableSortingType; +} + +export function useTableSettings(items: DataFrameAnalyticsListRow[]): UseTableSettingsReturnValue { + const [tableSettings, setTableSettings] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + totalItemCount: 0, + hidePerPageOptions: false, + sortField: DataFrameAnalyticsListColumn.id, + sortDirection: 'asc', + }); + + const getPageOfItems = ( + list: any[], + index: number, + size: number, + sortField: string, + sortDirection: Direction + ) => { + list = sortBy(list, (item) => + get(item, jobPropertyMap[sortField as keyof typeof jobPropertyMap] || sortField) + ); + list = sortDirection === 'asc' ? list : list.reverse(); + const listLength = list.length; + + let pageStart = index * size; + if (pageStart >= listLength && listLength !== 0) { + // if the page start is larger than the number of items due to + // filters being applied or items being deleted, calculate a new page start + pageStart = Math.floor((listLength - 1) / size) * size; + + setTableSettings({ ...tableSettings, pageIndex: pageStart / size }); + } + return { + pageOfItems: list.slice(pageStart, pageStart + size), + totalItemCount: listLength, + }; + }; + + const onTableChange = ({ + page = { index: 0, size: PAGE_SIZE }, + sort = { field: DataFrameAnalyticsListColumn.id, direction: 'asc' }, + }: { + page?: { index: number; size: number }; + sort?: { field: string; direction: Direction }; + }) => { + const { index, size } = page; + const { field, direction } = sort; + + setTableSettings({ + ...tableSettings, + pageIndex: index, + pageSize: size, + sortField: field, + sortDirection: direction, + }); + }; + + const { pageIndex, pageSize, sortField, sortDirection } = tableSettings; + + const { pageOfItems, totalItemCount } = getPageOfItems( + items, + pageIndex, + pageSize, + sortField, + sortDirection + ); + + const pagination = { + pageIndex, + pageSize, + totalItemCount, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }; + + const sorting = { + sort: { + field: sortField, + direction: sortDirection, + }, + }; + + return { onTableChange, pageOfItems, pagination, sorting }; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/analytics_search_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/analytics_search_bar.tsx new file mode 100644 index 00000000000000..44a6572a3766c8 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/analytics_search_bar.tsx @@ -0,0 +1,157 @@ +/* + * 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, { Dispatch, SetStateAction, FC, Fragment, useState } from 'react'; +import { + EuiSearchBar, + EuiSearchBarProps, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { stringMatch } from '../../../../../util/string_utils'; +import { + TermClause, + FieldClause, + Value, + DataFrameAnalyticsListRow, +} from '../analytics_list/common'; + +export function filterAnalytics( + items: DataFrameAnalyticsListRow[], + clauses: Array +) { + if (clauses.length === 0) { + return items; + } + + // keep count of the number of matches we make as we're looping over the clauses + // we only want to return items which match all clauses, i.e. each search term is ANDed + const matches: Record = items.reduce((p: Record, c) => { + p[c.id] = { + job: c, + count: 0, + }; + return p; + }, {}); + + clauses.forEach((c) => { + // the search term could be negated with a minus, e.g. -bananas + const bool = c.match === 'must'; + let js = []; + + if (c.type === 'term') { + // filter term based clauses, e.g. bananas + // match on id, description and memory_status + // if the term has been negated, AND the matches + if (bool === true) { + js = items.filter( + (item) => + stringMatch(item.id, c.value) === bool || + stringMatch(item.config.description, c.value) === bool || + stringMatch(item.stats?.memory_usage?.status, c.value) === bool + ); + } else { + js = items.filter( + (item) => + stringMatch(item.id, c.value) === bool && + stringMatch(item.config.description, c.value) === bool && + stringMatch(item.stats?.memory_usage?.status, c.value) === bool + ); + } + } else { + // filter other clauses, i.e. the filters for type and status + if (Array.isArray(c.value)) { + // job type value and status value are an array of string(s) e.g. c.value => ['failed', 'stopped'] + js = items.filter((item) => + (c.value as Value[]).includes( + item[c.field as keyof Pick] + ) + ); + } else { + js = items.filter( + (item) => item[c.field as keyof Pick] === c.value + ); + } + } + + js.forEach((j) => matches[j.id].count++); + }); + + // loop through the matches and return only those items which have match all the clauses + const filtered = Object.values(matches) + .filter((m) => (m && m.count) >= clauses.length) + .map((m) => m.job); + + return filtered; +} + +function getError(errorMessage: string | null) { + if (errorMessage) { + return i18n.translate('xpack.ml.analyticList.searchBar.invalidSearchErrorMessage', { + defaultMessage: 'Invalid search: {errorMessage}', + values: { errorMessage }, + }); + } + + return ''; +} + +interface Props { + filters: EuiSearchBarProps['filters']; + searchQueryText: string; + setSearchQueryText: Dispatch>; +} + +export const AnalyticsSearchBar: FC = ({ filters, searchQueryText, setSearchQueryText }) => { + const [errorMessage, setErrorMessage] = useState(null); + + const onChange: EuiSearchBarProps['onChange'] = ({ query, error }) => { + if (error) { + setErrorMessage(error.message); + } else if (query !== null && query.text !== undefined) { + setSearchQueryText(query.text); + setErrorMessage(null); + } + }; + + return ( + + + {searchQueryText === undefined && ( + + )} + {searchQueryText !== undefined && ( + + )} + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/index.ts new file mode 100644 index 00000000000000..3b901f5063eb1e --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/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 { AnalyticsSearchBar, filterAnalytics } from './analytics_search_bar'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index f90bbf3cf3fe6a..fa4ea09b89ff91 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -7,7 +7,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { sortBy } from 'lodash'; +import sortBy from 'lodash/sortBy'; import moment from 'moment'; import { toLocaleString } from '../../../../util/string_utils'; From 093f58872054c8d0b8601ad94865ba3bf50a4886 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 2 Sep 2020 15:35:33 +0200 Subject: [PATCH 176/216] [RUM Dashboard] Use median values instead of averages to display page load times (#76407) --- .../elasticsearch_fieldnames.test.ts.snap | 12 ++++++++ .../apm/common/elasticsearch_fieldnames.ts | 5 ++++ .../RumDashboard/Charts/PageLoadDistChart.tsx | 2 +- .../PercentileAnnotations.tsx | 4 +-- .../__snapshots__/queries.test.ts.snap | 18 +++++++++--- .../lib/rum_client/get_client_metrics.ts | 28 +++++++++++++------ .../server/lib/transaction_groups/fetcher.ts | 4 +-- .../apm/typings/elasticsearch/aggregations.ts | 2 +- 8 files changed, 57 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index 7c42fb6f12a54a..aecf4af667603d 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -120,6 +120,8 @@ exports[`Error TRACE_ID 1`] = `"trace id"`; exports[`Error TRANSACTION_BREAKDOWN_COUNT 1`] = `undefined`; +exports[`Error TRANSACTION_DOM_INTERACTIVE 1`] = `undefined`; + exports[`Error TRANSACTION_DURATION 1`] = `undefined`; exports[`Error TRANSACTION_ID 1`] = `"transaction id"`; @@ -132,6 +134,8 @@ exports[`Error TRANSACTION_RESULT 1`] = `undefined`; exports[`Error TRANSACTION_SAMPLED 1`] = `undefined`; +exports[`Error TRANSACTION_TIME_TO_FIRST_BYTE 1`] = `undefined`; + exports[`Error TRANSACTION_TYPE 1`] = `"request"`; exports[`Error TRANSACTION_URL 1`] = `undefined`; @@ -268,6 +272,8 @@ exports[`Span TRACE_ID 1`] = `"trace id"`; exports[`Span TRANSACTION_BREAKDOWN_COUNT 1`] = `undefined`; +exports[`Span TRANSACTION_DOM_INTERACTIVE 1`] = `undefined`; + exports[`Span TRANSACTION_DURATION 1`] = `undefined`; exports[`Span TRANSACTION_ID 1`] = `"transaction id"`; @@ -280,6 +286,8 @@ exports[`Span TRANSACTION_RESULT 1`] = `undefined`; exports[`Span TRANSACTION_SAMPLED 1`] = `undefined`; +exports[`Span TRANSACTION_TIME_TO_FIRST_BYTE 1`] = `undefined`; + exports[`Span TRANSACTION_TYPE 1`] = `undefined`; exports[`Span TRANSACTION_URL 1`] = `undefined`; @@ -416,6 +424,8 @@ exports[`Transaction TRACE_ID 1`] = `"trace id"`; exports[`Transaction TRANSACTION_BREAKDOWN_COUNT 1`] = `undefined`; +exports[`Transaction TRANSACTION_DOM_INTERACTIVE 1`] = `undefined`; + exports[`Transaction TRANSACTION_DURATION 1`] = `1337`; exports[`Transaction TRANSACTION_ID 1`] = `"transaction id"`; @@ -428,6 +438,8 @@ exports[`Transaction TRANSACTION_RESULT 1`] = `"transaction result"`; exports[`Transaction TRANSACTION_SAMPLED 1`] = `true`; +exports[`Transaction TRANSACTION_TIME_TO_FIRST_BYTE 1`] = `undefined`; + exports[`Transaction TRANSACTION_TYPE 1`] = `"transaction type"`; exports[`Transaction TRANSACTION_URL 1`] = `undefined`; diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index 610a32e8e9b99b..4aa68de9b8b32f 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -99,3 +99,8 @@ export const TRANSACTION_URL = 'transaction.page.url'; export const CLIENT_GEO = 'client.geo'; export const USER_AGENT_DEVICE = 'user_agent.device.name'; export const USER_AGENT_OS = 'user_agent.os.name'; + +export const TRANSACTION_TIME_TO_FIRST_BYTE = + 'transaction.marks.agent.timeToFirstByte'; +export const TRANSACTION_DOM_INTERACTIVE = + 'transaction.marks.agent.domInteractive'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx index b2b5e66d06ac69..33573052dbcbbf 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx @@ -35,7 +35,7 @@ import { BreakdownSeries } from '../PageLoadDistribution/BreakdownSeries'; interface PageLoadData { pageLoadDistribution: Array<{ x: number; y: number }>; - percentiles: Record | undefined; + percentiles: Record | undefined; minDuration: number; maxDuration: number; } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx index 407ec42f03ff5a..7e81dc011bdb5b 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx @@ -16,11 +16,11 @@ import styled from 'styled-components'; import { EuiToolTip } from '@elastic/eui'; interface Props { - percentiles?: Record; + percentiles?: Record; } function generateAnnotationData( - values?: Record + values?: Record ): LineAnnotationDatum[] { return Object.entries(values ?? {}).map((value) => ({ dataValue: value[1], diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap index c5264373ea4954..22b8c226e90261 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap @@ -10,15 +10,25 @@ Object { "body": Object { "aggs": Object { "backEnd": Object { - "avg": Object { + "percentiles": Object { "field": "transaction.marks.agent.timeToFirstByte", - "missing": 0, + "hdr": Object { + "number_of_significant_value_digits": 3, + }, + "percents": Array [ + 50, + ], }, }, "domInteractive": Object { - "avg": Object { + "percentiles": Object { "field": "transaction.marks.agent.domInteractive", - "missing": 0, + "hdr": Object { + "number_of_significant_value_digits": 3, + }, + "percents": Array [ + 50, + ], }, }, "pageViews": Object { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts index 194c136e2b3d06..e0dec183f06d6c 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts @@ -11,6 +11,10 @@ import { SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; +import { + TRANSACTION_DOM_INTERACTIVE, + TRANSACTION_TIME_TO_FIRST_BYTE, +} from '../../../common/elasticsearch_fieldnames'; export async function getClientMetrics({ setup, @@ -30,15 +34,21 @@ export async function getClientMetrics({ aggs: { pageViews: { value_count: { field: 'transaction.type' } }, backEnd: { - avg: { - field: 'transaction.marks.agent.timeToFirstByte', - missing: 0, + percentiles: { + field: TRANSACTION_TIME_TO_FIRST_BYTE, + percents: [50], + hdr: { + number_of_significant_value_digits: 3, + }, }, }, domInteractive: { - avg: { - field: 'transaction.marks.agent.domInteractive', - missing: 0, + percentiles: { + field: TRANSACTION_DOM_INTERACTIVE, + percents: [50], + hdr: { + number_of_significant_value_digits: 3, + }, }, }, }, @@ -53,9 +63,11 @@ export async function getClientMetrics({ // Divide by 1000 to convert ms into seconds return { pageViews, - backEnd: { value: (backEnd.value || 0) / 1000 }, + backEnd: { value: (backEnd.values['50.0'] || 0) / 1000 }, frontEnd: { - value: ((domInteractive.value || 0) - (backEnd.value || 0)) / 1000, + value: + ((domInteractive.values['50.0'] || 0) - (backEnd.values['50.0'] || 0)) / + 1000, }, }; } diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index 2a1a581c795742..d0ba31f42c536b 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -65,7 +65,7 @@ function getItemsWithRelativeImpact( key: string | Record; avg?: number | null; count?: number | null; - p95?: number; + p95?: number | null; sample?: Transaction; }> ) { @@ -188,7 +188,7 @@ export interface TransactionGroup { key: Record | string; averageResponseTime: number | null | undefined; transactionsPerMinute: number; - p95: number | undefined; + p95: number | null | undefined; impact: number; sample: Transaction; } diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts index d25ec8709e3bef..6a2a0e81e81cdd 100644 --- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts +++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts @@ -223,7 +223,7 @@ interface AggregationResponsePart< value: number; }; percentiles: { - values: Record; + values: Record; }; extended_stats: { count: number; From 5345af9281406368d8c4520742fbdd3ae08a61fd Mon Sep 17 00:00:00 2001 From: Brent Kimmel Date: Wed, 2 Sep 2020 09:48:33 -0400 Subject: [PATCH 177/216] [Security Solution][Resolver] Add events link to Process Detail Panel (#76195) * [Security_Solution][Resolver]Add events link to Process Detail Panel --- .../resolver/store/data/reducer.test.ts | 19 ++++++++++++++++++ .../public/resolver/store/data/selectors.ts | 20 +++++++++++++++++++ .../public/resolver/store/selectors.ts | 12 +++++++++++ .../public/resolver/view/panels/index.tsx | 12 +++++++---- .../resolver/view/panels/process_details.tsx | 18 +++++++++++++++++ 5 files changed, 77 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts index e087db9f74685a..21c4f92f8e502e 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts @@ -59,6 +59,7 @@ describe('Resolver Data Middleware', () => { let firstChildNodeInTree: TreeNode; let eventStatsForFirstChildNode: { total: number; byCategory: Record }; let categoryToOverCount: string; + let aggregateCategoryTotalForFirstChildNode: number; let tree: ResolverTree; /** @@ -73,6 +74,7 @@ describe('Resolver Data Middleware', () => { firstChildNodeInTree, eventStatsForFirstChildNode, categoryToOverCount, + aggregateCategoryTotalForFirstChildNode, } = mockedTree()); if (tree) { dispatchTree(tree); @@ -138,6 +140,13 @@ describe('Resolver Data Middleware', () => { expect(notDisplayed(typeCounted)).toBe(0); } }); + it('should return an overall correct count for the number of related events', () => { + const aggregateTotalByEntityId = selectors.relatedEventAggregateTotalByEntityId( + store.getState() + ); + const countForId = aggregateTotalByEntityId(firstChildNodeInTree.id); + expect(countForId).toBe(aggregateCategoryTotalForFirstChildNode); + }); }); describe('when data was received and stats show more related events than the API can provide', () => { beforeEach(() => { @@ -262,6 +271,7 @@ function mockedTree() { tree: tree!, firstChildNodeInTree, eventStatsForFirstChildNode: statsResults.eventStats, + aggregateCategoryTotalForFirstChildNode: statsResults.aggregateCategoryTotal, categoryToOverCount: statsResults.firstCategory, }; } @@ -288,6 +298,7 @@ function compileStatsForChild( }; /** The category of the first event. */ firstCategory: string; + aggregateCategoryTotal: number; } { const totalRelatedEvents = node.relatedEvents.length; // For the purposes of testing, we pick one category to fake an extra event for @@ -295,6 +306,12 @@ function compileStatsForChild( let firstCategory: string | undefined; + // This is the "aggregate total" which is displayed to users as the total count + // of related events for the node. It is tallied by incrementing for every discrete + // event.category in an event.category array (or just 1 for a plain string). E.g. two events + // categories 'file' and ['dns','network'] would have an `aggregate total` of 3. + let aggregateCategoryTotal: number = 0; + const compiledStats = node.relatedEvents.reduce( (counts: Record, relatedEvent) => { // `relatedEvent.event.category` is `string | string[]`. @@ -310,6 +327,7 @@ function compileStatsForChild( // Increment the count of events with this category counts[category] = counts[category] ? counts[category] + 1 : 1; + aggregateCategoryTotal++; } return counts; }, @@ -327,5 +345,6 @@ function compileStatsForChild( byCategory: compiledStats, }, firstCategory, + aggregateCategoryTotal, }; } diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 965547f1e309a9..eaa80b46471fad 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -170,6 +170,26 @@ export const relatedEventsStats: ( } ); +/** + * This returns the "aggregate total" for related events, tallied as the sum + * of their individual `event.category`s. E.g. a [DNS, Network] would count as two + * towards the aggregate total. + */ +export const relatedEventAggregateTotalByEntityId: ( + state: DataState +) => (entityId: string) => number = createSelector(relatedEventsStats, (relatedStats) => { + return (entityId) => { + const statsForEntity = relatedStats(entityId); + if (statsForEntity === undefined) { + return 0; + } + return Object.values(statsForEntity?.events?.byCategory || {}).reduce( + (sum, val) => sum + val, + 0 + ); + }; +}); + /** * returns a map of entity_ids to related event data. */ diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 909a907626f304..bdea08df3d7f50 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -114,6 +114,18 @@ export const relatedEventsStats: ( dataSelectors.relatedEventsStats ); +/** + * This returns the "aggregate total" for related events, tallied as the sum + * of their individual `event.category`s. E.g. a [DNS, Network] would count as two + * towards the aggregate total. + */ +export const relatedEventAggregateTotalByEntityId: ( + state: ResolverState +) => (nodeID: string) => number = composeSelectors( + dataStateSelector, + dataSelectors.relatedEventAggregateTotalByEntityId +); + /** * Map of related events... by entity id */ diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx index 98b737de8fa598..133dcd21e7f565 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx @@ -17,6 +17,7 @@ import { EventCountsForProcess } from './event_counts_for_process'; import { ProcessDetails } from './process_details'; import { ProcessListWithCounts } from './process_list_with_counts'; import { RelatedEventDetail } from './related_event_detail'; +import { ResolverState } from '../../types'; /** * The team decided to use this table to determine which breadcrumbs/view to display: @@ -102,6 +103,12 @@ const PanelContent = memo(function PanelContent() { ? relatedEventStats(idFromParams) : undefined; + const parentCount = useSelector((state: ResolverState) => { + if (idFromParams === '') { + return 0; + } + return selectors.relatedEventAggregateTotalByEntityId(state)(idFromParams); + }); /** * Determine which set of breadcrumbs to display based on the query parameters * for the table & breadcrumb nav. @@ -186,9 +193,6 @@ const PanelContent = memo(function PanelContent() { } if (panelToShow === 'relatedEventDetail') { - const parentCount: number = Object.values( - relatedStatsForIdFromParams?.events.byCategory || {} - ).reduce((sum, val) => sum + val, 0); return ( ; - }, [uiSelectedEvent, crumbEvent, crumbId, relatedStatsForIdFromParams, panelToShow]); + }, [uiSelectedEvent, crumbEvent, crumbId, relatedStatsForIdFromParams, panelToShow, parentCount]); return <>{panelInstance}; }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx index 01fa912caa866f..4fcc5577426430 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx @@ -13,6 +13,7 @@ import { EuiText, EuiTextColor, EuiDescriptionList, + EuiLink, } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from 'react-intl'; @@ -58,6 +59,9 @@ export const ProcessDetails = memo(function ProcessDetails({ const isProcessTerminated = useSelector((state: ResolverState) => selectors.isProcessTerminated(state)(entityId) ); + const relatedEventTotal = useSelector((state: ResolverState) => { + return selectors.relatedEventAggregateTotalByEntityId(state)(entityId); + }); const processInfoEntry: EuiDescriptionListProps['listItems'] = useMemo(() => { const eventTime = event.eventTimestamp(processEvent); const dateTime = eventTime === undefined ? null : formatDate(eventTime); @@ -164,6 +168,12 @@ export const ProcessDetails = memo(function ProcessDetails({ return cubeAssetsForNode(isProcessTerminated, false); }, [processEvent, cubeAssetsForNode, isProcessTerminated]); + const handleEventsLinkClick = useMemo(() => { + return () => { + pushToQueryParams({ crumbId: entityId, crumbEvent: 'all' }); + }; + }, [entityId, pushToQueryParams]); + const titleID = useMemo(() => htmlIdGenerator('resolverTable')(), []); return ( <> @@ -185,6 +195,14 @@ export const ProcessDetails = memo(function ProcessDetails({ {descriptionText} + + + + Date: Wed, 2 Sep 2020 16:00:38 +0200 Subject: [PATCH 178/216] [uiActions] notify action usage (#76294) Notify feature usage when dynamic actions with specified license requirements are executed Co-authored-by: Elastic Machine --- .../dashboard_to_url_drilldown/index.tsx | 1 + .../services/feature_usage_service.mock.ts | 4 + .../action_wizard/action_wizard.test.tsx | 8 +- .../action_wizard/action_wizard.tsx | 10 +-- .../components/action_wizard/test_data.tsx | 16 ++-- .../connected_flyout_manage_drilldowns.tsx | 2 +- .../form_drilldown_wizard.tsx | 2 +- .../public/drilldowns/drilldown_definition.ts | 9 +- .../dynamic_actions/action_factory.test.ts | 87 ++++++++++++++++--- .../public/dynamic_actions/action_factory.ts | 45 ++++++++-- .../action_factory_definition.ts | 11 ++- .../dynamic_action_manager.test.ts | 6 +- .../ui_actions_enhanced/public/mocks.ts | 4 +- .../ui_actions_enhanced/public/plugin.ts | 36 +++++--- .../ui_actions_service_enhancements.test.ts | 43 +++++++-- .../ui_actions_service_enhancements.ts | 31 +++++-- 16 files changed, 247 insertions(+), 68 deletions(-) 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 7d915ea23c66f0..58916f26121d4c 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 @@ -44,6 +44,7 @@ export class DashboardToUrlDrilldown implements Drilldown { public readonly order = 8; readonly minimalLicense = 'gold'; // example of minimal license support + readonly licenseFeatureName = 'Sample URL Drilldown'; public readonly getDisplayName = () => 'Go to URL (example)'; diff --git a/x-pack/plugins/licensing/public/services/feature_usage_service.mock.ts b/x-pack/plugins/licensing/public/services/feature_usage_service.mock.ts index fc9d4f9381151a..b2390ea35c1407 100644 --- a/x-pack/plugins/licensing/public/services/feature_usage_service.mock.ts +++ b/x-pack/plugins/licensing/public/services/feature_usage_service.mock.ts @@ -15,6 +15,8 @@ const createSetupMock = (): jest.Mocked => { register: jest.fn(), }; + mock.register.mockImplementation(() => Promise.resolve()); + return mock; }; @@ -23,6 +25,8 @@ const createStartMock = (): jest.Mocked => { notifyUsage: jest.fn(), }; + mock.notifyUsage.mockImplementation(() => Promise.resolve()); + return mock; }; diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx index 78252dccd20d24..9cc64defc17952 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx @@ -15,7 +15,7 @@ import { urlDrilldownActionFactory, } from './test_data'; import { ActionFactory } from '../../dynamic_actions'; -import { licenseMock } from '../../../../licensing/common/licensing.mock'; +import { licensingMock } from '../../../../licensing/public/mocks'; // TODO: afterEach is not available for it globally during setup // https://github.com/elastic/kibana/issues/59469 @@ -68,8 +68,12 @@ test('If not enough license, button is disabled', () => { { ...urlDrilldownActionFactory, minimalLicense: 'gold', + licenseFeatureName: 'Url Drilldown', }, - () => licenseMock.createLicense() + { + getLicense: () => licensingMock.createLicense(), + getFeatureUsageStart: () => licensingMock.createStart().featureUsage, + } ); const screen = render(); diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx index 7e4fe1de8be8dc..a49251811239fa 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx @@ -93,7 +93,7 @@ export const ActionWizard: React.FC = ({ if ( !currentActionFactory && actionFactories.length === 1 && - actionFactories[0].isCompatibleLicence() + actionFactories[0].isCompatibleLicense() ) { onActionFactoryChange(actionFactories[0]); } @@ -314,8 +314,8 @@ const ActionFactorySelector: React.FC = ({ * make sure not compatible factories are in the end */ const ensureOrder = (factories: ActionFactory[]) => { - const compatibleLicense = factories.filter((f) => f.isCompatibleLicence()); - const notCompatibleLicense = factories.filter((f) => !f.isCompatibleLicence()); + const compatibleLicense = factories.filter((f) => f.isCompatibleLicense()); + const notCompatibleLicense = factories.filter((f) => !f.isCompatibleLicense()); return [ ...compatibleLicense.sort((f1, f2) => f2.order - f1.order), ...notCompatibleLicense.sort((f1, f2) => f2.order - f1.order), @@ -328,7 +328,7 @@ const ActionFactorySelector: React.FC = ({ = ({ label={actionFactory.getDisplayName(context)} data-test-subj={`${TEST_SUBJ_ACTION_FACTORY_ITEM}-${actionFactory.id}`} onClick={() => onActionFactorySelected(actionFactory)} - disabled={!actionFactory.isCompatibleLicence()} + disabled={!actionFactory.isCompatibleLicense()} > {actionFactory.getIconType(context) && ( diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx index d48cb13b1a4705..71286e9a59c06e 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx @@ -10,7 +10,7 @@ import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/p import { ActionWizard } from './action_wizard'; import { ActionFactory, ActionFactoryDefinition } from '../../dynamic_actions'; import { CollectConfigProps } from '../../../../../../src/plugins/kibana_utils/public'; -import { licenseMock } from '../../../../licensing/common/licensing.mock'; +import { licensingMock } from '../../../../licensing/public/mocks'; import { APPLY_FILTER_TRIGGER, SELECT_RANGE_TRIGGER, @@ -116,9 +116,10 @@ export const dashboardDrilldownActionFactory: ActionFactoryDefinition< }, }; -export const dashboardFactory = new ActionFactory(dashboardDrilldownActionFactory, () => - licenseMock.createLicense() -); +export const dashboardFactory = new ActionFactory(dashboardDrilldownActionFactory, { + getLicense: () => licensingMock.createLicense(), + getFeatureUsageStart: () => licensingMock.createStart().featureUsage, +}); interface UrlDrilldownConfig { url: string; @@ -176,9 +177,10 @@ export const urlDrilldownActionFactory: ActionFactoryDefinition - licenseMock.createLicense() -); +export const urlFactory = new ActionFactory(urlDrilldownActionFactory, { + getLicense: () => licensingMock.createLicense(), + getFeatureUsageStart: () => licensingMock.createStart().featureUsage, +}); export const mockSupportedTriggers: TriggerId[] = [ VALUE_CLICK_TRIGGER, diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx index 9fca785ec90722..b708bbc57375d2 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx @@ -148,7 +148,7 @@ export function createFlyoutManageDrilldowns({ icon: actionFactory?.getIconType(drilldownFactoryContext), error: !actionFactory ? invalidDrilldownType(drilldown.action.factoryId) // this shouldn't happen for the end user, but useful during development - : !actionFactory.isCompatibleLicence() + : !actionFactory.isCompatibleLicense() ? insufficientLicenseLevel : undefined, triggers: drilldown.triggers.map((trigger) => getTrigger(trigger as TriggerId)), diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx index bb3eb89d8f1996..d7f94a52088b7f 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx @@ -75,7 +75,7 @@ export const FormDrilldownWizard: React.FC = ({ ); const hasNotCompatibleLicenseFactory = () => - actionFactories?.some((f) => !f.isCompatibleLicence()); + actionFactories?.some((f) => !f.isCompatibleLicense()); const renderGetMoreActionsLink = () => ( diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts index ff455c6ae45b66..8faccc088a3279 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts @@ -37,11 +37,18 @@ export interface DrilldownDefinition< id: string; /** - * Minimal licence level + * Minimal license level * Empty means no restrictions */ minimalLicense?: LicenseType; + /** + * Required when `minimalLicense` is used. + * Is a user-facing string. Has to be unique. Doesn't need i18n. + * The feature's name will be displayed to Cloud end-users when they're billed based on their feature usage. + */ + licenseFeatureName?: string; + /** * Determines the display order of the drilldowns in the flyout picker. * Higher numbers are displayed first. diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts index a07fed8486438a..032a4a63fe2e9d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts @@ -7,6 +7,7 @@ import { ActionFactory } from './action_factory'; import { ActionFactoryDefinition } from './action_factory_definition'; import { licensingMock } from '../../../licensing/public/mocks'; +import { PublicLicense } from '../../../licensing/public'; const def: ActionFactoryDefinition = { id: 'ACTION_FACTORY_1', @@ -22,34 +23,94 @@ const def: ActionFactoryDefinition = { supportedTriggers: () => [], }; +const featureUsage = licensingMock.createStart().featureUsage; + +const createActionFactory = ( + defOverride: Partial = {}, + license?: Partial +) => { + return new ActionFactory( + { ...def, ...defOverride }, + { + getLicense: () => licensingMock.createLicense({ license }), + getFeatureUsageStart: () => featureUsage, + } + ); +}; + describe('License & ActionFactory', () => { test('no license requirements', async () => { - const factory = new ActionFactory(def, () => licensingMock.createLicense()); + const factory = createActionFactory(); expect(await factory.isCompatible({ triggers: [] })).toBe(true); - expect(factory.isCompatibleLicence()).toBe(true); + expect(factory.isCompatibleLicense()).toBe(true); }); test('not enough license level', async () => { - const factory = new ActionFactory({ ...def, minimalLicense: 'gold' }, () => - licensingMock.createLicense() - ); + const factory = createActionFactory({ minimalLicense: 'gold', licenseFeatureName: 'Feature' }); expect(await factory.isCompatible({ triggers: [] })).toBe(true); - expect(factory.isCompatibleLicence()).toBe(false); + expect(factory.isCompatibleLicense()).toBe(false); }); - test('licence has expired', async () => { - const factory = new ActionFactory({ ...def, minimalLicense: 'gold' }, () => - licensingMock.createLicense({ license: { type: 'gold', status: 'expired' } }) + test('license has expired', async () => { + const factory = createActionFactory( + { minimalLicense: 'gold', licenseFeatureName: 'Feature' }, + { type: 'gold', status: 'expired' } ); expect(await factory.isCompatible({ triggers: [] })).toBe(true); - expect(factory.isCompatibleLicence()).toBe(false); + expect(factory.isCompatibleLicense()).toBe(false); }); test('enough license level', async () => { - const factory = new ActionFactory({ ...def, minimalLicense: 'gold' }, () => - licensingMock.createLicense({ license: { type: 'gold' } }) + const factory = createActionFactory( + { minimalLicense: 'gold', licenseFeatureName: 'Feature' }, + { type: 'gold' } ); + expect(await factory.isCompatible({ triggers: [] })).toBe(true); - expect(factory.isCompatibleLicence()).toBe(true); + expect(factory.isCompatibleLicense()).toBe(true); + }); + + describe('licenseFeatureName', () => { + test('licenseFeatureName is required, if minimalLicense is provided', () => { + expect(() => { + createActionFactory(); + }).not.toThrow(); + + expect(() => { + createActionFactory({ minimalLicense: 'gold', licenseFeatureName: 'feature' }); + }).not.toThrow(); + + expect(() => { + createActionFactory({ minimalLicense: 'gold' }); + }).toThrow(); + }); + + test('"licenseFeatureName"', () => { + expect( + createActionFactory({ minimalLicense: 'gold', licenseFeatureName: 'feature' }) + .licenseFeatureName + ).toBe('feature'); + expect(createActionFactory().licenseFeatureName).toBeUndefined(); + }); + }); + + describe('notifyFeatureUsage', () => { + const spy = jest.spyOn(featureUsage, 'notifyUsage'); + beforeEach(() => { + spy.mockClear(); + }); + test('is not called if no license requirements', async () => { + const action = createActionFactory().create({ name: 'fake', config: {} }); + await action.execute({}); + expect(spy).not.toBeCalled(); + }); + test('is called if has license requirements', async () => { + const action = createActionFactory({ + minimalLicense: 'gold', + licenseFeatureName: 'feature', + }).create({ name: 'fake', config: {} }); + await action.execute({}); + expect(spy).toBeCalledWith('feature'); + }); }); }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts index 35e06ab036fc97..35a82adf9896dd 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts @@ -13,9 +13,14 @@ import { import { ActionFactoryDefinition } from './action_factory_definition'; import { Configurable } from '../../../../../src/plugins/kibana_utils/public'; import { BaseActionFactoryContext, SerializedAction } from './types'; -import { ILicense } from '../../../licensing/public'; +import { ILicense, LicensingPluginStart } from '../../../licensing/public'; import { UiActionsActionDefinition as ActionDefinition } from '../../../../../src/plugins/ui_actions/public'; +export interface ActionFactoryDeps { + readonly getLicense: () => ILicense; + readonly getFeatureUsageStart: () => LicensingPluginStart['featureUsage']; +} + export class ActionFactory< Config extends object = object, SupportedTriggers extends TriggerId = TriggerId, @@ -31,11 +36,18 @@ export class ActionFactory< FactoryContext, ActionContext >, - protected readonly getLicence: () => ILicense - ) {} + protected readonly deps: ActionFactoryDeps + ) { + if (def.minimalLicense && !def.licenseFeatureName) { + throw new Error( + `ActionFactory [actionFactory.id = ${def.id}] "licenseFeatureName" is required, if "minimalLicense" is provided` + ); + } + } public readonly id = this.def.id; public readonly minimalLicense = this.def.minimalLicense; + public readonly licenseFeatureName = this.def.licenseFeatureName; public readonly order = this.def.order || 0; public readonly MenuItem? = this.def.MenuItem; public readonly ReactMenuItem? = this.MenuItem ? uiToReactComponent(this.MenuItem) : undefined; @@ -65,13 +77,13 @@ export class ActionFactory< } /** - * Does this action factory licence requirements + * Does this action factory license requirements * compatible with current license? */ - public isCompatibleLicence() { + public isCompatibleLicense() { if (!this.minimalLicense) return true; - const licence = this.getLicence(); - return licence.isAvailable && licence.isActive && licence.hasAtLeast(this.minimalLicense); + const license = this.deps.getLicense(); + return license.isAvailable && license.isActive && license.hasAtLeast(this.minimalLicense); } public create( @@ -81,14 +93,31 @@ export class ActionFactory< return { ...action, isCompatible: async (context: ActionContext): Promise => { - if (!this.isCompatibleLicence()) return false; + if (!this.isCompatibleLicense()) return false; if (!action.isCompatible) return true; return action.isCompatible(context); }, + execute: async (context: ActionContext): Promise => { + this.notifyFeatureUsage(); + return action.execute(context); + }, }; } public supportedTriggers(): SupportedTriggers[] { return this.def.supportedTriggers(); } + + private notifyFeatureUsage(): void { + if (!this.minimalLicense || !this.licenseFeatureName) return; + this.deps + .getFeatureUsageStart() + .notifyUsage(this.licenseFeatureName) + .catch(() => { + // eslint-disable-next-line no-console + console.warn( + `ActionFactory [actionFactory.id = ${this.def.id}] fail notify feature usage.` + ); + }); + } } diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts index d79614e47ccd4e..91b8c8ec1e5eff 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts @@ -34,11 +34,18 @@ export interface ActionFactoryDefinition< id: string; /** - * Minimal licence level - * Empty means no licence restrictions + * Minimal license level + * Empty means no license restrictions */ readonly minimalLicense?: LicenseType; + /** + * Required when `minimalLicense` is used. + * Is a user-facing string. Has to be unique. Doesn't need i18n. + * The feature's name will be displayed to Cloud end-users when they're billed based on their feature usage. + */ + licenseFeatureName?: string; + /** * This method should return a definition of a new action, normally used to * register it in `ui_actions` registry. diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts index 0b0cd39e35e25f..39d9dfeca2fd6c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts @@ -87,7 +87,9 @@ const setup = ( actions, }); const uiActionsEnhancements = new UiActionsServiceEnhancements({ - getLicenseInfo, + getLicense: getLicenseInfo, + featureUsageSetup: licensingMock.createSetup().featureUsage, + getFeatureUsageStart: () => licensingMock.createStart().featureUsage, }); const manager = new DynamicActionManager({ isCompatible, @@ -671,11 +673,13 @@ describe('DynamicActionManager', () => { const basicActionFactory: ActionFactoryDefinition = { ...actionFactoryDefinition1, minimalLicense: 'basic', + licenseFeatureName: 'Feature 1', }; const goldActionFactory: ActionFactoryDefinition = { ...actionFactoryDefinition2, minimalLicense: 'gold', + licenseFeatureName: 'Feature 2', }; uiActions.registerActionFactory(basicActionFactory); diff --git a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts b/x-pack/plugins/ui_actions_enhanced/public/mocks.ts index ff07d6e74a9c0b..17a6fc1b955dff 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/mocks.ts @@ -11,6 +11,7 @@ import { embeddablePluginMock } from '../../../../src/plugins/embeddable/public/ import { AdvancedUiActionsSetup, AdvancedUiActionsStart } from '.'; import { plugin as pluginInitializer } from '.'; import { licensingMock } from '../../licensing/public/mocks'; +import { StartDependencies } from './plugin'; export type Setup = jest.Mocked; export type Start = jest.Mocked; @@ -35,7 +36,7 @@ const createStartContract = (): Start => { }; const createPlugin = ( - coreSetup: CoreSetup = coreMock.createSetup(), + coreSetup: CoreSetup = coreMock.createSetup(), coreStart: CoreStart = coreMock.createStart() ) => { const pluginInitializerContext = coreMock.createPluginInitializerContext(); @@ -47,6 +48,7 @@ const createPlugin = ( const setup = plugin.setup(coreSetup, { uiActions: uiActions.setup, embeddable: embeddable.setup, + licensing: licensingMock.createSetup(), }); return { diff --git a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts index 5069b485b198d0..015531aab97437 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts @@ -36,16 +36,17 @@ import { } from './custom_time_range_badge'; import { CommonlyUsedRange } from './types'; import { UiActionsServiceEnhancements } from './services'; -import { ILicense, LicensingPluginStart } from '../../licensing/public'; +import { ILicense, LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; import { createFlyoutManageDrilldowns } from './drilldowns'; -import { Storage } from '../../../../src/plugins/kibana_utils/public'; +import { createStartServicesGetter, Storage } from '../../../../src/plugins/kibana_utils/public'; interface SetupDependencies { embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. uiActions: UiActionsSetup; + licensing: LicensingPluginSetup; } -interface StartDependencies { +export interface StartDependencies { embeddable: EmbeddableStart; uiActions: UiActionsStart; licensing: LicensingPluginStart; @@ -70,23 +71,30 @@ declare module '../../../../src/plugins/ui_actions/public' { export class AdvancedUiActionsPublicPlugin implements Plugin { - readonly licenceInfo = new BehaviorSubject(undefined); + readonly licenseInfo = new BehaviorSubject(undefined); private getLicenseInfo(): ILicense { - if (!this.licenceInfo.getValue()) { + if (!this.licenseInfo.getValue()) { throw new Error( - 'AdvancedUiActionsPublicPlugin: Licence is not ready! Licence becomes available only after setup.' + 'AdvancedUiActionsPublicPlugin: License is not ready! License becomes available only after setup.' ); } - return this.licenceInfo.getValue()!; + return this.licenseInfo.getValue()!; } - private readonly enhancements = new UiActionsServiceEnhancements({ - getLicenseInfo: () => this.getLicenseInfo(), - }); + private enhancements?: UiActionsServiceEnhancements; private subs: Subscription[] = []; constructor(initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup, { uiActions }: SetupDependencies): SetupContract { + public setup( + core: CoreSetup, + { uiActions, licensing }: SetupDependencies + ): SetupContract { + const startServices = createStartServicesGetter(core.getStartServices); + this.enhancements = new UiActionsServiceEnhancements({ + getLicense: () => this.getLicenseInfo(), + featureUsageSetup: licensing.featureUsage, + getFeatureUsageStart: () => startServices().plugins.licensing.featureUsage, + }); return { ...uiActions, ...this.enhancements, @@ -94,7 +102,7 @@ export class AdvancedUiActionsPublicPlugin } public start(core: CoreStart, { uiActions, licensing }: StartDependencies): StartContract { - this.subs.push(licensing.license$.subscribe(this.licenceInfo)); + this.subs.push(licensing.license$.subscribe(this.licenseInfo)); const dateFormat = core.uiSettings.get('dateFormat') as string; const commonlyUsedRanges = core.uiSettings.get( @@ -117,9 +125,9 @@ export class AdvancedUiActionsPublicPlugin return { ...uiActions, - ...this.enhancements, + ...this.enhancements!, FlyoutManageDrilldowns: createFlyoutManageDrilldowns({ - actionFactories: this.enhancements.getActionFactories(), + actionFactories: this.enhancements!.getActionFactories(), getTrigger: (triggerId: TriggerId) => uiActions.getTrigger(triggerId), storage: new Storage(window?.localStorage), toastService: core.notifications.toasts, diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts index 08823833b9af28..3a0b65d2ed8446 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts @@ -4,11 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UiActionsServiceEnhancements } from './ui_actions_service_enhancements'; +import { + UiActionsServiceEnhancements, + UiActionsServiceEnhancementsParams, +} from './ui_actions_service_enhancements'; import { ActionFactoryDefinition, ActionFactory } from '../dynamic_actions'; import { licensingMock } from '../../../licensing/public/mocks'; -const getLicenseInfo = () => licensingMock.createLicense(); +const deps: UiActionsServiceEnhancementsParams = { + getLicense: () => licensingMock.createLicense(), + featureUsageSetup: licensingMock.createSetup().featureUsage, + getFeatureUsageStart: () => licensingMock.createStart().featureUsage, +}; describe('UiActionsService', () => { describe('action factories', () => { @@ -34,7 +41,7 @@ describe('UiActionsService', () => { }; test('.getActionFactories() returns empty array if no action factories registered', () => { - const service = new UiActionsServiceEnhancements({ getLicenseInfo }); + const service = new UiActionsServiceEnhancements(deps); const factories = service.getActionFactories(); @@ -42,7 +49,7 @@ describe('UiActionsService', () => { }); test('can register and retrieve an action factory', () => { - const service = new UiActionsServiceEnhancements({ getLicenseInfo }); + const service = new UiActionsServiceEnhancements(deps); service.registerActionFactory(factoryDefinition1); @@ -53,7 +60,7 @@ describe('UiActionsService', () => { }); test('can retrieve all action factories', () => { - const service = new UiActionsServiceEnhancements({ getLicenseInfo }); + const service = new UiActionsServiceEnhancements(deps); service.registerActionFactory(factoryDefinition1); service.registerActionFactory(factoryDefinition2); @@ -67,7 +74,7 @@ describe('UiActionsService', () => { }); test('throws when retrieving action factory that does not exist', () => { - const service = new UiActionsServiceEnhancements({ getLicenseInfo }); + const service = new UiActionsServiceEnhancements(deps); service.registerActionFactory(factoryDefinition1); @@ -77,7 +84,7 @@ describe('UiActionsService', () => { }); test('isCompatible from definition is used on registered factory', async () => { - const service = new UiActionsServiceEnhancements({ getLicenseInfo }); + const service = new UiActionsServiceEnhancements(deps); service.registerActionFactory({ ...factoryDefinition1, @@ -88,5 +95,27 @@ describe('UiActionsService', () => { service.getActionFactory(factoryDefinition1.id).isCompatible({ triggers: [] }) ).resolves.toBe(false); }); + + describe('registerFeature for licensing', () => { + const spy = jest.spyOn(deps.featureUsageSetup, 'register'); + beforeEach(() => { + spy.mockClear(); + }); + test('registerFeature is not called if no license requirements', () => { + const service = new UiActionsServiceEnhancements(deps); + service.registerActionFactory(factoryDefinition1); + expect(spy).not.toBeCalled(); + }); + + test('registerFeature is called if has license requirements', () => { + const service = new UiActionsServiceEnhancements(deps); + service.registerActionFactory({ + ...factoryDefinition1, + minimalLicense: 'gold', + licenseFeatureName: 'a name', + }); + expect(spy).toBeCalledWith('a name', 'gold'); + }); + }); }); }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts index 95753295148356..b8086c16f5e714 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts @@ -13,19 +13,22 @@ import { import { DrilldownDefinition } from '../drilldowns'; import { ILicense } from '../../../licensing/common/types'; import { TriggerContextMapping, TriggerId } from '../../../../../src/plugins/ui_actions/public'; +import { LicensingPluginSetup, LicensingPluginStart } from '../../../licensing/public'; export interface UiActionsServiceEnhancementsParams { readonly actionFactories?: ActionFactoryRegistry; - readonly getLicenseInfo: () => ILicense; + readonly getLicense: () => ILicense; + readonly featureUsageSetup: LicensingPluginSetup['featureUsage']; + readonly getFeatureUsageStart: () => LicensingPluginStart['featureUsage']; } export class UiActionsServiceEnhancements { protected readonly actionFactories: ActionFactoryRegistry; - protected readonly getLicenseInfo: () => ILicense; + protected readonly deps: Omit; - constructor({ actionFactories = new Map(), getLicenseInfo }: UiActionsServiceEnhancementsParams) { + constructor({ actionFactories = new Map(), ...deps }: UiActionsServiceEnhancementsParams) { this.actionFactories = actionFactories; - this.getLicenseInfo = getLicenseInfo; + this.deps = deps; } /** @@ -51,9 +54,10 @@ export class UiActionsServiceEnhancements { SupportedTriggers, FactoryContext, ActionContext - >(definition, this.getLicenseInfo); + >(definition, this.deps); this.actionFactories.set(actionFactory.id, actionFactory as ActionFactory); + this.registerFeatureUsage(definition); }; public readonly getActionFactory = (actionFactoryId: string): ActionFactory => { @@ -94,6 +98,7 @@ export class UiActionsServiceEnhancements { execute, getHref, minimalLicense, + licenseFeatureName, supportedTriggers, isCompatible, }: DrilldownDefinition): void => { @@ -105,6 +110,7 @@ export class UiActionsServiceEnhancements { > = { id: factoryId, minimalLicense, + licenseFeatureName, order, CollectConfig, createConfig, @@ -128,4 +134,19 @@ export class UiActionsServiceEnhancements { this.registerActionFactory(actionFactory); }; + + private registerFeatureUsage = (definition: ActionFactoryDefinition): void => { + if (!definition.minimalLicense || !definition.licenseFeatureName) return; + + // Intentionally don't wait for response because + // happens in setup phase and has to be sync + this.deps.featureUsageSetup + .register(definition.licenseFeatureName, definition.minimalLicense) + .catch(() => { + // eslint-disable-next-line no-console + console.warn( + `ActionFactory [actionFactory.id = ${definition.id}] fail to register feature for featureUsage.` + ); + }); + }; } From 3e077973e8a5b73c03ab149d74c13ae2b9a700f3 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 2 Sep 2020 16:11:12 +0200 Subject: [PATCH 179/216] [APM] Immediately return terms for unbound queries (#74543) Co-authored-by: Elastic Machine --- .../apm/server/lib/environments/get_all_environments.ts | 5 +++++ .../lib/helpers/create_es_client/call_client_with_debug.ts | 2 +- .../agent_configuration/__snapshots__/queries.test.ts.snap | 2 ++ .../lib/settings/agent_configuration/get_service_names.ts | 2 ++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts index 29aaa98169fa5a..8060bf10da99cc 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts @@ -37,6 +37,9 @@ export async function getAllEnvironments({ ], }, body: { + // use timeout + min_doc_count to return as early as possible + // if filter is not defined to prevent timeouts + ...(!serviceName ? { timeout: '1ms' } : {}), size: 0, query: { bool: { @@ -48,6 +51,7 @@ export async function getAllEnvironments({ terms: { field: SERVICE_ENVIRONMENT, size: 100, + ...(!serviceName ? { min_doc_count: 0 } : {}), missing: includeMissing ? ENVIRONMENT_NOT_DEFINED.value : undefined, }, }, @@ -56,6 +60,7 @@ export async function getAllEnvironments({ }; const resp = await apmEventClient.search(params); + const environments = resp.aggregations?.environments.buckets.map( (bucket) => bucket.key as string diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts index c4756405952279..9f7aaafbefb8ce 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts @@ -34,7 +34,7 @@ export async function callClientWithDebug({ let res: any; let esError = null; try { - res = apiCaller(operationName, params); + res = await apiCaller(operationName, params); } catch (e) { // catch error and throw after outputting debug info esError = e; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap index 2b465a0f874752..c01e5c87eeea2f 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap @@ -127,11 +127,13 @@ Object { "services": Object { "terms": Object { "field": "service.name", + "min_doc_count": 0, "size": 50, }, }, }, "size": 0, + "timeout": "1ms", }, } `; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts index 8b6c1d82beab00..91bdfeef003f13 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts @@ -25,12 +25,14 @@ export async function getServiceNames({ setup }: { setup: Setup }) { ], }, body: { + timeout: '1ms', size: 0, aggs: { services: { terms: { field: SERVICE_NAME, size: 50, + min_doc_count: 0, }, }, }, From 01e442081f666322a99266f443b2651228306cf4 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Wed, 2 Sep 2020 10:34:37 -0400 Subject: [PATCH 180/216] [Visualize] Remove alias icon for Lens and Maps (#76418) * [Visualize] Remove alias icon for Lens and Maps * Fix snapshots * Fix translations --- .../__snapshots__/new_vis_modal.test.tsx.snap | 140 +----------------- .../wizard/type_selection/type_selection.tsx | 42 ++---- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 4 files changed, 16 insertions(+), 170 deletions(-) diff --git a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap index 6aed16e9377131..3c4c983efa9faf 100644 --- a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap +++ b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap @@ -248,7 +248,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
  • diff --git a/src/core/public/chrome/ui/header/nav_drawer.tsx b/src/core/public/chrome/ui/header/nav_drawer.tsx index ee4bff6cc0ac44..fc080fbafc3030 100644 --- a/src/core/public/chrome/ui/header/nav_drawer.tsx +++ b/src/core/public/chrome/ui/header/nav_drawer.tsx @@ -33,7 +33,6 @@ export interface Props { appId$: InternalApplicationStart['currentAppId$']; basePath: HttpStart['basePath']; isLocked?: boolean; - legacyMode: boolean; navLinks$: Observable; recentlyAccessed$: Observable; navigateToApp: CoreStart['application']['navigateToApp']; @@ -41,7 +40,7 @@ export interface Props { } function NavDrawerRenderer( - { isLocked, onIsLockedUpdate, basePath, legacyMode, navigateToApp, ...observables }: Props, + { isLocked, onIsLockedUpdate, basePath, navigateToApp, ...observables }: Props, ref: React.Ref ) { const appId = useObservable(observables.appId$, ''); @@ -67,7 +66,6 @@ function NavDrawerRenderer( listItems={navLinks.map((link) => createEuiListItem({ link, - legacyMode, appId, basePath, navigateToApp, diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx index c70a40f49643e0..04d9c5bf7a10a6 100644 --- a/src/core/public/chrome/ui/header/nav_link.tsx +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -29,7 +29,6 @@ export const isModifiedOrPrevented = (event: React.MouseEvent {}, @@ -52,12 +50,7 @@ export function createEuiListItem({ dataTestSubj, externalLink = false, }: Props) { - const { legacy, active, id, title, disabled, euiIconType, icon, tooltip } = link; - let { href } = link; - - if (legacy) { - href = link.url && !active ? link.url : link.baseUrl; - } + const { href, id, title, disabled, euiIconType, icon, tooltip } = link; return { label: tooltip ?? title, @@ -70,8 +63,6 @@ export function createEuiListItem({ if ( !externalLink && // ignore external links - !legacyMode && // ignore when in legacy mode - !legacy && // ignore links to legacy apps event.button === 0 && // ignore everything but left clicks !isModifiedOrPrevented(event) ) { @@ -79,8 +70,7 @@ export function createEuiListItem({ navigateToApp(id); } }, - // Legacy apps use `active` property, NP apps should match the current app - isActive: active || appId === id, + isActive: appId === id, isDisabled: disabled, 'data-test-subj': dataTestSubj, ...(basePath && { @@ -116,7 +106,7 @@ export function createRecentNavLink( ) { const { link, label } = recentLink; const href = relativeToAbsolute(basePath.prepend(link)); - const navLink = navLinks.find((nl) => href.startsWith(nl.baseUrl ?? nl.subUrlBase)); + const navLink = navLinks.find((nl) => href.startsWith(nl.baseUrl)); let titleAndAriaLabel = label; if (navLink) { diff --git a/src/core/public/core_system.test.mocks.ts b/src/core/public/core_system.test.mocks.ts index b5b99418b44b4f..d0e457386ffca3 100644 --- a/src/core/public/core_system.test.mocks.ts +++ b/src/core/public/core_system.test.mocks.ts @@ -23,7 +23,6 @@ import { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock import { httpServiceMock } from './http/http_service.mock'; import { i18nServiceMock } from './i18n/i18n_service.mock'; import { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock'; -import { legacyPlatformServiceMock } from './legacy/legacy_service.mock'; import { notificationServiceMock } from './notifications/notifications_service.mock'; import { overlayServiceMock } from './overlays/overlay_service.mock'; import { pluginsServiceMock } from './plugins/plugins_service.mock'; @@ -34,14 +33,6 @@ import { contextServiceMock } from './context/context_service.mock'; import { integrationsServiceMock } from './integrations/integrations_service.mock'; import { coreAppMock } from './core_app/core_app.mock'; -export const MockLegacyPlatformService = legacyPlatformServiceMock.create(); -export const LegacyPlatformServiceConstructor = jest - .fn() - .mockImplementation(() => MockLegacyPlatformService); -jest.doMock('./legacy', () => ({ - LegacyPlatformService: LegacyPlatformServiceConstructor, -})); - export const MockInjectedMetadataService = injectedMetadataServiceMock.create(); export const InjectedMetadataServiceConstructor = jest .fn() diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 4c1993c90a2e10..213237309c30bd 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -23,13 +23,11 @@ import { HttpServiceConstructor, I18nServiceConstructor, InjectedMetadataServiceConstructor, - LegacyPlatformServiceConstructor, MockChromeService, MockFatalErrorsService, MockHttpService, MockI18nService, MockInjectedMetadataService, - MockLegacyPlatformService, MockNotificationsService, MockOverlayService, MockPluginsService, @@ -80,7 +78,6 @@ describe('constructor', () => { createCoreSystem(); expect(InjectedMetadataServiceConstructor).toHaveBeenCalledTimes(1); - expect(LegacyPlatformServiceConstructor).toHaveBeenCalledTimes(1); expect(I18nServiceConstructor).toHaveBeenCalledTimes(1); expect(FatalErrorsServiceConstructor).toHaveBeenCalledTimes(1); expect(NotificationServiceConstructor).toHaveBeenCalledTimes(1); @@ -106,25 +103,6 @@ describe('constructor', () => { }); }); - it('passes required params to LegacyPlatformService', () => { - const requireLegacyFiles = { requireLegacyFiles: true }; - const requireLegacyBootstrapModule = { requireLegacyBootstrapModule: true }; - const requireNewPlatformShimModule = { requireNewPlatformShimModule: true }; - - createCoreSystem({ - requireLegacyFiles, - requireLegacyBootstrapModule, - requireNewPlatformShimModule, - }); - - expect(LegacyPlatformServiceConstructor).toHaveBeenCalledTimes(1); - expect(LegacyPlatformServiceConstructor).toHaveBeenCalledWith({ - requireLegacyFiles, - requireLegacyBootstrapModule, - requireNewPlatformShimModule, - }); - }); - it('passes browserSupportsCsp to ChromeService', () => { createCoreSystem(); @@ -190,7 +168,6 @@ describe('#setup()', () => { pluginDependencies: new Map([ [pluginA, []], [pluginB, [pluginA]], - [MockLegacyPlatformService.legacyId, [pluginA, pluginB]], ]), }); }); @@ -301,11 +278,6 @@ describe('#start()', () => { expect(MockPluginsService.start).toHaveBeenCalledTimes(1); }); - it('calls legacyPlatform#start()', async () => { - await startCore(); - expect(MockLegacyPlatformService.start).toHaveBeenCalledTimes(1); - }); - it('calls overlays#start()', async () => { await startCore(); expect(MockOverlayService.start).toHaveBeenCalledTimes(1); @@ -317,7 +289,6 @@ describe('#start()', () => { expect(MockRenderingService.start).toHaveBeenCalledWith({ application: expect.any(Object), chrome: expect.any(Object), - injectedMetadata: expect.any(Object), overlays: expect.any(Object), targetDomElement: expect.any(HTMLElement), }); @@ -335,14 +306,6 @@ describe('#start()', () => { }); describe('#stop()', () => { - it('calls legacyPlatform.stop()', () => { - const coreSystem = createCoreSystem(); - - expect(MockLegacyPlatformService.stop).not.toHaveBeenCalled(); - coreSystem.stop(); - expect(MockLegacyPlatformService.stop).toHaveBeenCalled(); - }); - it('calls notifications.stop()', () => { const coreSystem = createCoreSystem(); @@ -422,7 +385,6 @@ describe('RenderingService targetDomElement', () => { let targetDomElementParentInStart: HTMLElement | null; MockRenderingService.start.mockImplementation(({ targetDomElement }) => { targetDomElementParentInStart = targetDomElement.parentElement; - return { legacyTargetDomElement: document.createElement('div') }; }); // Starting the core system should pass the targetDomElement as a child of the rootDomElement @@ -432,24 +394,6 @@ describe('RenderingService targetDomElement', () => { }); }); -describe('LegacyPlatformService targetDomElement', () => { - it('only mounts the element when start, after setting up the legacyPlatformService', async () => { - const core = createCoreSystem(); - - let targetDomElementInStart: HTMLElement | undefined; - MockLegacyPlatformService.start.mockImplementation(({ targetDomElement }) => { - targetDomElementInStart = targetDomElement; - }); - - await core.setup(); - await core.start(); - // Starting the core system should pass the legacyTargetDomElement to the LegacyPlatformService - const renderingLegacyTargetDomElement = - MockRenderingService.start.mock.results[0].value.legacyTargetDomElement; - expect(targetDomElementInStart!).toBe(renderingLegacyTargetDomElement); - }); -}); - describe('Notifications targetDomElement', () => { it('only mounts the element when started, after setting up the notificationsService', async () => { const rootDomElement = document.createElement('div'); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index e08841b0271d9d..006d0036f7a129 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -30,13 +30,12 @@ import { InjectedMetadataSetup, InjectedMetadataStart, } from './injected_metadata'; -import { LegacyPlatformParams, LegacyPlatformService } from './legacy'; import { NotificationsService } from './notifications'; import { OverlayService } from './overlays'; import { PluginsService } from './plugins'; import { UiSettingsService } from './ui_settings'; import { ApplicationService } from './application'; -import { mapToObject, pick } from '../utils/'; +import { pick } from '../utils/'; import { DocLinksService } from './doc_links'; import { RenderingService } from './rendering'; import { SavedObjectsService } from './saved_objects'; @@ -49,9 +48,6 @@ interface Params { rootDomElement: HTMLElement; browserSupportsCsp: boolean; injectedMetadata: InjectedMetadataParams['injectedMetadata']; - requireLegacyFiles?: LegacyPlatformParams['requireLegacyFiles']; - requireLegacyBootstrapModule?: LegacyPlatformParams['requireLegacyBootstrapModule']; - requireNewPlatformShimModule?: LegacyPlatformParams['requireNewPlatformShimModule']; } /** @internal */ @@ -86,7 +82,6 @@ export interface InternalCoreStart extends Omit { export class CoreSystem { private readonly fatalErrors: FatalErrorsService; private readonly injectedMetadata: InjectedMetadataService; - private readonly legacy: LegacyPlatformService; private readonly notifications: NotificationsService; private readonly http: HttpService; private readonly savedObjects: SavedObjectsService; @@ -107,14 +102,7 @@ export class CoreSystem { private fatalErrorsSetup: FatalErrorsSetup | null = null; constructor(params: Params) { - const { - rootDomElement, - browserSupportsCsp, - injectedMetadata, - requireLegacyFiles, - requireLegacyBootstrapModule, - requireNewPlatformShimModule, - } = params; + const { rootDomElement, browserSupportsCsp, injectedMetadata } = params; this.rootDomElement = rootDomElement; @@ -145,12 +133,6 @@ export class CoreSystem { this.context = new ContextService(this.coreContext); this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins); this.coreApp = new CoreApp(this.coreContext); - - this.legacy = new LegacyPlatformService({ - requireLegacyFiles, - requireLegacyBootstrapModule, - requireNewPlatformShimModule, - }); } public async setup() { @@ -170,16 +152,9 @@ export class CoreSystem { const pluginDependencies = this.plugins.getOpaqueIds(); const context = this.context.setup({ - // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: - // 1) Can access context from any NP plugin - // 2) Can register context providers that will only be available to other legacy plugins and will not leak into - // New Platform plugins. - pluginDependencies: new Map([ - ...pluginDependencies, - [this.legacy.legacyId, [...pluginDependencies.keys()]], - ]), + pluginDependencies: new Map([...pluginDependencies]), }); - const application = this.application.setup({ context, http, injectedMetadata }); + const application = this.application.setup({ context, http }); this.coreApp.setup({ application, http, injectedMetadata, notifications }); const core: InternalCoreSetup = { @@ -193,12 +168,7 @@ export class CoreSystem { }; // Services that do not expose contracts at setup - const plugins = await this.plugins.setup(core); - - await this.legacy.setup({ - core, - plugins: mapToObject(plugins.contracts), - }); + await this.plugins.setup(core); return { fatalErrors: this.fatalErrorsSetup }; } catch (error) { @@ -277,7 +247,7 @@ export class CoreSystem { fatalErrors, }; - const plugins = await this.plugins.start(core); + await this.plugins.start(core); // ensure the rootDomElement is empty this.rootDomElement.textContent = ''; @@ -286,20 +256,13 @@ export class CoreSystem { this.rootDomElement.appendChild(notificationsTargetDomElement); this.rootDomElement.appendChild(overlayTargetDomElement); - const rendering = this.rendering.start({ + this.rendering.start({ application, chrome, - injectedMetadata, overlays, targetDomElement: coreUiTargetDomElement, }); - await this.legacy.start({ - core, - plugins: mapToObject(plugins.contracts), - targetDomElement: rendering.legacyTargetDomElement, - }); - return { application, }; @@ -315,7 +278,6 @@ export class CoreSystem { } public stop() { - this.legacy.stop(); this.plugins.stop(); this.coreApp.stop(); this.notifications.stop(); diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 9176a277b3f434..a9774dafd2340a 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -61,7 +61,6 @@ import { import { FatalErrorsSetup, FatalErrorsStart, FatalErrorInfo } from './fatal_errors'; import { HttpSetup, HttpStart } from './http'; import { I18nStart } from './i18n'; -import { InjectedMetadataSetup, InjectedMetadataStart, LegacyNavLink } from './injected_metadata'; import { NotificationsSetup, NotificationsStart } from './notifications'; import { OverlayStart } from './overlays'; import { Plugin, PluginInitializer, PluginInitializerContext, PluginOpaqueId } from './plugins'; @@ -106,7 +105,6 @@ export { ApplicationStart, App, PublicAppInfo, - AppBase, AppMount, AppMountDeprecated, AppUnmount, @@ -122,8 +120,6 @@ export { AppUpdatableFields, AppUpdater, ScopedHistory, - LegacyApp, - PublicLegacyAppInfo, NavigateToAppOptions, } from './application'; @@ -230,7 +226,7 @@ export interface CoreSetup { - /** @deprecated */ - injectedMetadata: InjectedMetadataSetup; -} - -/** - * Start interface exposed to the legacy platform via the `ui/new_platform` module. - * - * @remarks - * Some methods are not supported in the legacy platform and while present to make this type compatibile with - * {@link CoreStart}, unsupported methods will throw exceptions when called. - * - * @public - * @deprecated - */ -export interface LegacyCoreStart extends CoreStart { - /** @deprecated */ - injectedMetadata: InjectedMetadataStart; -} - export { Capabilities, ChromeBadge, @@ -356,7 +322,6 @@ export { HttpSetup, HttpStart, I18nStart, - LegacyNavLink, NotificationsSetup, NotificationsStart, Plugin, diff --git a/src/core/public/injected_metadata/index.ts b/src/core/public/injected_metadata/index.ts index cebd0f017de698..925eeab1875357 100644 --- a/src/core/public/injected_metadata/index.ts +++ b/src/core/public/injected_metadata/index.ts @@ -23,5 +23,4 @@ export { InjectedMetadataSetup, InjectedMetadataStart, InjectedPluginMetadata, - LegacyNavLink, } from './injected_metadata_service'; diff --git a/src/core/public/injected_metadata/injected_metadata_service.mock.ts b/src/core/public/injected_metadata/injected_metadata_service.mock.ts index e6b1c440519bd2..3bb4358406246b 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.mock.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.mock.ts @@ -25,7 +25,6 @@ const createSetupContractMock = () => { getKibanaVersion: jest.fn(), getKibanaBranch: jest.fn(), getCspConfig: jest.fn(), - getLegacyMode: jest.fn(), getAnonymousStatusPage: jest.fn(), getLegacyMetadata: jest.fn(), getPlugins: jest.fn(), @@ -35,7 +34,6 @@ const createSetupContractMock = () => { }; setupContract.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true }); setupContract.getKibanaVersion.mockReturnValue('kibanaVersion'); - setupContract.getLegacyMode.mockReturnValue(true); setupContract.getAnonymousStatusPage.mockReturnValue(false); setupContract.getLegacyMetadata.mockReturnValue({ app: { diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index db4bfdf415bcc8..23630a5bcf228b 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -28,17 +28,6 @@ import { import { deepFreeze } from '../../utils/'; import { AppCategory } from '../'; -/** @public */ -export interface LegacyNavLink { - id: string; - category?: AppCategory; - title: string; - order: number; - url: string; - icon?: string; - euiIconType?: string; -} - export interface InjectedPluginMetadata { id: PluginName; plugin: DiscoveredPlugin; @@ -67,7 +56,6 @@ export interface InjectedMetadataParams { packageInfo: Readonly; }; uiPlugins: InjectedPluginMetadata[]; - legacyMode: boolean; anonymousStatusPage: boolean; legacyMetadata: { app: { @@ -75,7 +63,6 @@ export interface InjectedMetadataParams { title: string; }; bundleId: string; - nav: LegacyNavLink[]; version: string; branch: string; buildNum: number; @@ -137,10 +124,6 @@ export class InjectedMetadataService { return this.state.uiPlugins; }, - getLegacyMode: () => { - return this.state.legacyMode; - }, - getLegacyMetadata: () => { return this.state.legacyMetadata; }, @@ -182,8 +165,6 @@ export interface InjectedMetadataSetup { * An array of frontend plugins in topological order. */ getPlugins: () => InjectedPluginMetadata[]; - /** Indicates whether or not we are rendering a known legacy app. */ - getLegacyMode: () => boolean; getAnonymousStatusPage: () => boolean; getLegacyMetadata: () => { app: { @@ -191,7 +172,6 @@ export interface InjectedMetadataSetup { title: string; }; bundleId: string; - nav: LegacyNavLink[]; version: string; branch: string; buildNum: number; diff --git a/src/core/public/legacy/index.ts b/src/core/public/legacy/index.ts deleted file mode 100644 index 1ea43d8deebbc4..00000000000000 --- a/src/core/public/legacy/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 { LegacyPlatformService, LegacyPlatformParams } from './legacy_service'; diff --git a/src/core/public/legacy/legacy_service.mock.ts b/src/core/public/legacy/legacy_service.mock.ts deleted file mode 100644 index 0c8d9682185d5c..00000000000000 --- a/src/core/public/legacy/legacy_service.mock.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 { LegacyPlatformService } from './legacy_service'; - -// Use Required to get only public properties -type LegacyPlatformServiceContract = Required; -const createMock = () => { - const mocked: jest.Mocked = { - legacyId: Symbol(), - setup: jest.fn(), - start: jest.fn(), - stop: jest.fn(), - }; - return mocked; -}; - -export const legacyPlatformServiceMock = { - create: createMock, -}; diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts deleted file mode 100644 index cb29abc9b0ccc5..00000000000000 --- a/src/core/public/legacy/legacy_service.test.ts +++ /dev/null @@ -1,297 +0,0 @@ -/* - * 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 angular from 'angular'; - -import { chromeServiceMock } from '../chrome/chrome_service.mock'; -import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; -import { httpServiceMock } from '../http/http_service.mock'; -import { i18nServiceMock } from '../i18n/i18n_service.mock'; -import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; -import { notificationServiceMock } from '../notifications/notifications_service.mock'; -import { overlayServiceMock } from '../overlays/overlay_service.mock'; -import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; -import { LegacyPlatformService } from './legacy_service'; -import { applicationServiceMock } from '../application/application_service.mock'; -import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; -import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; -import { contextServiceMock } from '../context/context_service.mock'; - -const applicationSetup = applicationServiceMock.createInternalSetupContract(); -const contextSetup = contextServiceMock.createSetupContract(); -const docLinksSetup = docLinksServiceMock.createSetupContract(); -const fatalErrorsSetup = fatalErrorsServiceMock.createSetupContract(); -const httpSetup = httpServiceMock.createSetupContract(); -const injectedMetadataSetup = injectedMetadataServiceMock.createSetupContract(); -const notificationsSetup = notificationServiceMock.createSetupContract(); -const uiSettingsSetup = uiSettingsServiceMock.createSetupContract(); - -const mockLoadOrder: string[] = []; -const mockUiNewPlatformSetup = jest.fn(); -const mockUiNewPlatformStart = jest.fn(); -const mockUiChromeBootstrap = jest.fn(); -const defaultParams = { - requireLegacyFiles: jest.fn(() => { - mockLoadOrder.push('legacy files'); - }), - requireLegacyBootstrapModule: jest.fn(() => { - mockLoadOrder.push('ui/chrome'); - return { - bootstrap: mockUiChromeBootstrap, - }; - }), - requireNewPlatformShimModule: jest.fn(() => ({ - __setup__: mockUiNewPlatformSetup, - __start__: mockUiNewPlatformStart, - })), -}; - -const defaultSetupDeps = { - core: { - application: applicationSetup, - context: contextSetup, - docLinks: docLinksSetup, - fatalErrors: fatalErrorsSetup, - injectedMetadata: injectedMetadataSetup, - notifications: notificationsSetup, - http: httpSetup, - uiSettings: uiSettingsSetup, - }, - plugins: {}, -}; - -const applicationStart = applicationServiceMock.createInternalStartContract(); -const docLinksStart = docLinksServiceMock.createStartContract(); -const httpStart = httpServiceMock.createStartContract(); -const chromeStart = chromeServiceMock.createStartContract(); -const i18nStart = i18nServiceMock.createStartContract(); -const injectedMetadataStart = injectedMetadataServiceMock.createStartContract(); -const notificationsStart = notificationServiceMock.createStartContract(); -const overlayStart = overlayServiceMock.createStartContract(); -const uiSettingsStart = uiSettingsServiceMock.createStartContract(); -const savedObjectsStart = savedObjectsServiceMock.createStartContract(); -const fatalErrorsStart = fatalErrorsServiceMock.createStartContract(); -const mockStorage = { getItem: jest.fn() } as any; - -const defaultStartDeps = { - core: { - application: applicationStart, - docLinks: docLinksStart, - http: httpStart, - chrome: chromeStart, - i18n: i18nStart, - injectedMetadata: injectedMetadataStart, - notifications: notificationsStart, - overlays: overlayStart, - uiSettings: uiSettingsStart, - savedObjects: savedObjectsStart, - fatalErrors: fatalErrorsStart, - }, - lastSubUrlStorage: mockStorage, - targetDomElement: document.createElement('div'), - plugins: {}, -}; - -afterEach(() => { - jest.clearAllMocks(); - jest.resetModules(); - mockLoadOrder.length = 0; -}); - -describe('#setup()', () => { - describe('default', () => { - it('initializes new platform shim module with core APIs', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - - expect(mockUiNewPlatformSetup).toHaveBeenCalledTimes(1); - expect(mockUiNewPlatformSetup).toHaveBeenCalledWith(expect.any(Object), {}); - }); - - it('throws error if requireNewPlatformShimModule is undefined', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - requireNewPlatformShimModule: undefined, - }); - - expect(() => { - legacyPlatform.setup(defaultSetupDeps); - }).toThrowErrorMatchingInlineSnapshot( - `"requireNewPlatformShimModule must be specified when rendering a legacy application"` - ); - - expect(mockUiNewPlatformSetup).not.toHaveBeenCalled(); - }); - }); -}); - -describe('#start()', () => { - it('fetches and sets legacy lastSubUrls', () => { - chromeStart.navLinks.getAll.mockReturnValue([ - { id: 'link1', baseUrl: 'http://wowza.com/app1', legacy: true } as any, - ]); - mockStorage.getItem.mockReturnValue('http://wowza.com/app1/subUrl'); - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - legacyPlatform.start({ ...defaultStartDeps, lastSubUrlStorage: mockStorage }); - - expect(chromeStart.navLinks.update).toHaveBeenCalledWith('link1', { - url: 'http://wowza.com/app1/subUrl', - }); - }); - - it('initializes ui/new_platform with core APIs', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - legacyPlatform.start(defaultStartDeps); - - expect(mockUiNewPlatformStart).toHaveBeenCalledTimes(1); - expect(mockUiNewPlatformStart).toHaveBeenCalledWith(expect.any(Object), {}); - }); - - it('throws error if requireNewPlatformShimeModule is undefined', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - requireNewPlatformShimModule: undefined, - }); - - expect(() => { - legacyPlatform.start(defaultStartDeps); - }).toThrowErrorMatchingInlineSnapshot( - `"requireNewPlatformShimModule must be specified when rendering a legacy application"` - ); - - expect(mockUiNewPlatformStart).not.toHaveBeenCalled(); - }); - - it('resolves getStartServices with core and plugin APIs', async () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - legacyPlatform.start(defaultStartDeps); - - const { getStartServices } = mockUiNewPlatformSetup.mock.calls[0][0]; - const [coreStart, pluginsStart] = await getStartServices(); - expect(coreStart).toEqual(expect.any(Object)); - expect(pluginsStart).toBe(defaultStartDeps.plugins); - }); - - it('passes the targetDomElement to legacy bootstrap module', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - legacyPlatform.start(defaultStartDeps); - - expect(mockUiChromeBootstrap).toHaveBeenCalledTimes(1); - expect(mockUiChromeBootstrap).toHaveBeenCalledWith(defaultStartDeps.targetDomElement); - }); - - describe('load order', () => { - it('loads ui/modules before ui/chrome, and both before legacy files', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - expect(mockLoadOrder).toEqual([]); - - legacyPlatform.setup(defaultSetupDeps); - legacyPlatform.start(defaultStartDeps); - - expect(mockLoadOrder).toMatchInlineSnapshot(` - Array [ - "ui/chrome", - "legacy files", - ] - `); - }); - }); -}); - -describe('#stop()', () => { - it('does nothing if angular was not bootstrapped to targetDomElement', () => { - const targetDomElement = document.createElement('div'); - targetDomElement.innerHTML = ` -

    this should not be removed

    - `; - - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.stop(); - expect(targetDomElement).toMatchInlineSnapshot(` -
    - - -

    - this should not be removed -

    - - -
    - `); - }); - - it('destroys the angular scope and empties the targetDomElement if angular is bootstrapped to targetDomElement', async () => { - const targetDomElement = document.createElement('div'); - const scopeDestroySpy = jest.fn(); - - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - // simulate bootstrapping with a module "foo" - angular.module('foo', []).directive('bar', () => ({ - restrict: 'E', - link($scope) { - $scope.$on('$destroy', scopeDestroySpy); - }, - })); - - targetDomElement.innerHTML = ` - - `; - - angular.bootstrap(targetDomElement, ['foo']); - - await legacyPlatform.setup(defaultSetupDeps); - legacyPlatform.start({ ...defaultStartDeps, targetDomElement }); - legacyPlatform.stop(); - - expect(targetDomElement).toMatchInlineSnapshot(` -
    - `); - expect(scopeDestroySpy).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts deleted file mode 100644 index 78a9219f3d6940..00000000000000 --- a/src/core/public/legacy/legacy_service.ts +++ /dev/null @@ -1,204 +0,0 @@ -/* - * 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 angular from 'angular'; -import { first } from 'rxjs/operators'; -import { Subject } from 'rxjs'; -import { InternalCoreSetup, InternalCoreStart } from '../core_system'; -import { LegacyCoreSetup, LegacyCoreStart, MountPoint } from '../'; - -/** @internal */ -export interface LegacyPlatformParams { - requireLegacyFiles?: () => void; - requireLegacyBootstrapModule?: () => BootstrapModule; - requireNewPlatformShimModule?: () => { - __setup__: (legacyCore: LegacyCoreSetup, plugins: Record) => void; - __start__: (legacyCore: LegacyCoreStart, plugins: Record) => void; - }; -} - -interface SetupDeps { - core: InternalCoreSetup; - plugins: Record; -} - -interface StartDeps { - core: InternalCoreStart; - plugins: Record; - lastSubUrlStorage?: Storage; - targetDomElement?: HTMLElement; -} - -interface BootstrapModule { - bootstrap: MountPoint; -} - -/** - * The LegacyPlatformService is responsible for initializing - * the legacy platform by injecting parts of the new platform - * services into the legacy platform modules, like ui/modules, - * and then bootstrapping the ui/chrome or ~~ui/test_harness~~ to - * setup either the app or browser tests. - */ -export class LegacyPlatformService { - /** Symbol to represent the legacy platform as a fake "plugin". Used by the ContextService */ - public readonly legacyId = Symbol(); - private bootstrapModule?: BootstrapModule; - private targetDomElement?: HTMLElement; - private readonly startDependencies$ = new Subject<[LegacyCoreStart, object, {}]>(); - private readonly startDependencies = this.startDependencies$.pipe(first()).toPromise(); - - constructor(private readonly params: LegacyPlatformParams) {} - - public setup({ core, plugins }: SetupDeps) { - // Always register legacy apps, even if not in legacy mode. - core.injectedMetadata.getLegacyMetadata().nav.forEach((navLink: any) => - core.application.registerLegacyApp({ - id: navLink.id, - order: navLink.order, - title: navLink.title, - euiIconType: navLink.euiIconType, - icon: navLink.icon, - appUrl: navLink.url, - subUrlBase: navLink.subUrlBase, - linkToLastSubUrl: navLink.linkToLastSubUrl, - category: navLink.category, - disableSubUrlTracking: navLink.disableSubUrlTracking, - }) - ); - - const legacyCore: LegacyCoreSetup = { - ...core, - getStartServices: () => this.startDependencies, - application: { - ...core.application, - register: notSupported(`core.application.register()`), - registerMountContext: notSupported(`core.application.registerMountContext()`), - }, - }; - - // Inject parts of the new platform into parts of the legacy platform - // so that legacy APIs/modules can mimic their new platform counterparts - if (core.injectedMetadata.getLegacyMode()) { - if (!this.params.requireNewPlatformShimModule) { - throw new Error( - `requireNewPlatformShimModule must be specified when rendering a legacy application` - ); - } - - this.params.requireNewPlatformShimModule().__setup__(legacyCore, plugins); - } - } - - public start({ - core, - targetDomElement, - plugins, - lastSubUrlStorage = window.sessionStorage, - }: StartDeps) { - // Initialize legacy sub urls - core.chrome.navLinks - .getAll() - .filter((link) => link.legacy) - .forEach((navLink) => { - const lastSubUrl = lastSubUrlStorage.getItem(`lastSubUrl:${navLink.baseUrl}`); - core.chrome.navLinks.update(navLink.id, { - url: lastSubUrl || navLink.url || navLink.baseUrl, - }); - }); - - // Only import and bootstrap legacy platform if we're in legacy mode. - if (!core.injectedMetadata.getLegacyMode()) { - return; - } - - const legacyCore: LegacyCoreStart = { - ...core, - application: { - applications$: core.application.applications$, - currentAppId$: core.application.currentAppId$, - capabilities: core.application.capabilities, - getUrlForApp: core.application.getUrlForApp, - navigateToApp: core.application.navigateToApp, - navigateToUrl: core.application.navigateToUrl, - registerMountContext: notSupported(`core.application.registerMountContext()`), - }, - }; - - this.startDependencies$.next([legacyCore, plugins, {}]); - - if (!this.params.requireNewPlatformShimModule) { - throw new Error( - `requireNewPlatformShimModule must be specified when rendering a legacy application` - ); - } - if (!this.params.requireLegacyBootstrapModule) { - throw new Error( - `requireLegacyBootstrapModule must be specified when rendering a legacy application` - ); - } - - // Inject parts of the new platform into parts of the legacy platform - // so that legacy APIs/modules can mimic their new platform counterparts - this.params.requireNewPlatformShimModule().__start__(legacyCore, plugins); - - // Load the bootstrap module before loading the legacy platform files so that - // the bootstrap module can modify the environment a bit first - this.bootstrapModule = this.params.requireLegacyBootstrapModule(); - - // require the files that will tie into the legacy platform - if (this.params.requireLegacyFiles) { - this.params.requireLegacyFiles(); - } - - if (!this.bootstrapModule) { - throw new Error('Bootstrap module must be loaded before `start`'); - } - - this.targetDomElement = targetDomElement; - - // `targetDomElement` is always defined when in legacy mode - this.bootstrapModule.bootstrap(this.targetDomElement!); - } - - public stop() { - if (!this.targetDomElement) { - return; - } - - const angularRoot = angular.element(this.targetDomElement); - const injector$ = angularRoot.injector(); - - // if we haven't gotten to the point of bootstrapping - // angular, injector$ won't be defined - if (!injector$) { - return; - } - - // destroy the root angular scope - injector$.get('$rootScope').$destroy(); - - // clear the inner html of the root angular element - this.targetDomElement.textContent = ''; - } -} - -const notSupported = (methodName: string) => (...args: any[]) => { - throw new Error(`${methodName} is not supported in the legacy platform.`); -}; diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index aefcb830d40bf0..8ed415c09806c7 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -42,7 +42,6 @@ export { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock export { httpServiceMock } from './http/http_service.mock'; export { i18nServiceMock } from './i18n/i18n_service.mock'; export { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock'; -export { legacyPlatformServiceMock } from './legacy/legacy_service.mock'; export { notificationServiceMock } from './notifications/notifications_service.mock'; export { overlayServiceMock } from './overlays/overlay_service.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index cc37a3c7c09246..c473ea67d9bcdc 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -42,25 +42,18 @@ import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/type // @internal (undocumented) export function __kbnBootstrap__(): void; -// @public -export interface App extends AppBase { - appRoute?: string; - chromeless?: boolean; - exactRoute?: boolean; - mount: AppMount | AppMountDeprecated; -} - // @public (undocumented) -export interface AppBase { +export interface App { + appRoute?: string; capabilities?: Partial; category?: AppCategory; chromeless?: boolean; defaultPath?: string; euiIconType?: string; + exactRoute?: boolean; icon?: string; id: string; - // @internal - legacy?: boolean; + mount: AppMount | AppMountDeprecated; navLinkStatus?: AppNavLinkStatus; order?: number; status?: AppStatus; @@ -124,7 +117,7 @@ export interface ApplicationSetup { // @public (undocumented) export interface ApplicationStart { - applications$: Observable>; + applications$: Observable>; capabilities: RecursiveReadonly; currentAppId$: Observable; getUrlForApp(appId: string, options?: { @@ -189,10 +182,10 @@ export enum AppStatus { export type AppUnmount = () => void; // @public -export type AppUpdatableFields = Pick; +export type AppUpdatableFields = Pick; // @public -export type AppUpdater = (app: AppBase) => Partial | undefined; +export type AppUpdater = (app: App) => Partial | undefined; // @public export function assertNever(x: never): never; @@ -230,10 +223,6 @@ export type ChromeBreadcrumb = EuiBreadcrumb; // @public export interface ChromeDocTitle { - // @internal (undocumented) - __legacy: { - setBaseTitle(baseTitle: string): void; - }; change(newTitle: string | string[]): void; reset(): void; } @@ -293,28 +282,18 @@ export interface ChromeNavControls { // @public (undocumented) export interface ChromeNavLink { - // @deprecated - readonly active?: boolean; readonly baseUrl: string; readonly category?: AppCategory; - // @deprecated readonly disabled?: boolean; - // @deprecated - readonly disableSubUrlTracking?: boolean; readonly euiIconType?: string; readonly hidden?: boolean; - readonly href?: string; + readonly href: string; readonly icon?: string; readonly id: string; - // @internal - readonly legacy: boolean; - // @deprecated - readonly linkToLastSubUrl?: boolean; readonly order?: number; - // @deprecated - readonly subUrlBase?: string; readonly title: string; readonly tooltip?: string; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AppBase" readonly url?: string; } @@ -327,12 +306,14 @@ export interface ChromeNavLinks { getNavLinks$(): Observable>>; has(id: string): boolean; showOnly(id: string): void; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AppBase" + // // @deprecated update(id: string, values: ChromeNavLinkUpdateableFields): ChromeNavLink | undefined; } // @public (undocumented) -export type ChromeNavLinkUpdateableFields = Partial>; +export type ChromeNavLinkUpdateableFields = Partial>; // @public export interface ChromeRecentlyAccessed { @@ -884,52 +865,6 @@ export interface IUiSettingsClient { set: (key: string, value: any) => Promise; } -// @public (undocumented) -export interface LegacyApp extends AppBase { - // (undocumented) - appUrl: string; - // (undocumented) - disableSubUrlTracking?: boolean; - // (undocumented) - linkToLastSubUrl?: boolean; - // (undocumented) - subUrlBase?: string; -} - -// @public @deprecated -export interface LegacyCoreSetup extends CoreSetup { - // Warning: (ae-forgotten-export) The symbol "InjectedMetadataSetup" needs to be exported by the entry point index.d.ts - // - // @deprecated (undocumented) - injectedMetadata: InjectedMetadataSetup; -} - -// @public @deprecated -export interface LegacyCoreStart extends CoreStart { - // Warning: (ae-forgotten-export) The symbol "InjectedMetadataStart" needs to be exported by the entry point index.d.ts - // - // @deprecated (undocumented) - injectedMetadata: InjectedMetadataStart; -} - -// @public (undocumented) -export interface LegacyNavLink { - // (undocumented) - category?: AppCategory; - // (undocumented) - euiIconType?: string; - // (undocumented) - icon?: string; - // (undocumented) - id: string; - // (undocumented) - order: number; - // (undocumented) - title: string; - // (undocumented) - url: string; -} - // @public export function modifyUrl(url: string, urlModifier: (urlParts: URLMeaningfulParts) => Partial | void): string; @@ -1043,19 +978,11 @@ export type PluginOpaqueId = symbol; // @public export type PublicAppInfo = Omit & { - legacy: false; status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; }; -// @public -export type PublicLegacyAppInfo = Omit & { - legacy: true; - status: AppStatus; - navLinkStatus: AppNavLinkStatus; -}; - // @public export type PublicUiSettingsParams = Omit; @@ -1548,6 +1475,6 @@ export interface UserProvidedValues { // Warnings were encountered during analysis: // -// src/core/public/core_system.ts:215:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts +// src/core/public/core_system.ts:185:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/public/rendering/index.ts b/src/core/public/rendering/index.ts index 7c1ea7031b763c..1de82a50a36b5b 100644 --- a/src/core/public/rendering/index.ts +++ b/src/core/public/rendering/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { RenderingService, RenderingStart } from './rendering_service'; +export { RenderingService } from './rendering_service'; diff --git a/src/core/public/rendering/rendering_service.mock.ts b/src/core/public/rendering/rendering_service.mock.ts index bb4e7cb49f1509..bb4723e69ab5e8 100644 --- a/src/core/public/rendering/rendering_service.mock.ts +++ b/src/core/public/rendering/rendering_service.mock.ts @@ -17,25 +17,16 @@ * under the License. */ -import { RenderingStart, RenderingService } from './rendering_service'; - -const createStartContractMock = () => { - const setupContract: jest.Mocked = { - legacyTargetDomElement: document.createElement('div'), - }; - return setupContract; -}; +import { RenderingService } from './rendering_service'; type RenderingServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { start: jest.fn(), }; - mocked.start.mockReturnValue(createStartContractMock()); return mocked; }; export const renderingServiceMock = { create: createMock, - createStartContract: createStartContractMock, }; diff --git a/src/core/public/rendering/rendering_service.test.tsx b/src/core/public/rendering/rendering_service.test.tsx index 437a602a3d4473..37658cb51c46f4 100644 --- a/src/core/public/rendering/rendering_service.test.tsx +++ b/src/core/public/rendering/rendering_service.test.tsx @@ -23,7 +23,6 @@ import { act } from 'react-dom/test-utils'; import { RenderingService } from './rendering_service'; import { applicationServiceMock } from '../application/application_service.mock'; import { chromeServiceMock } from '../chrome/chrome_service.mock'; -import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { overlayServiceMock } from '../overlays/overlay_service.mock'; import { BehaviorSubject } from 'rxjs'; @@ -31,7 +30,6 @@ describe('RenderingService#start', () => { let application: ReturnType; let chrome: ReturnType; let overlays: ReturnType; - let injectedMetadata: ReturnType; let targetDomElement: HTMLDivElement; let rendering: RenderingService; @@ -45,8 +43,6 @@ describe('RenderingService#start', () => { overlays = overlayServiceMock.createStartContract(); overlays.banners.getComponent.mockReturnValue(
    I'm a banner!
    ); - injectedMetadata = injectedMetadataServiceMock.createStartContract(); - targetDomElement = document.createElement('div'); rendering = new RenderingService(); @@ -56,20 +52,14 @@ describe('RenderingService#start', () => { return rendering.start({ application, chrome, - injectedMetadata, overlays, targetDomElement, }); }; - describe('standard mode', () => { - beforeEach(() => { - injectedMetadata.getLegacyMode.mockReturnValue(false); - }); - - it('renders application service into provided DOM element', () => { - startService(); - expect(targetDomElement.querySelector('div.application')).toMatchInlineSnapshot(` + it('renders application service into provided DOM element', () => { + startService(); + expect(targetDomElement.querySelector('div.application')).toMatchInlineSnapshot(`
    @@ -78,50 +68,50 @@ describe('RenderingService#start', () => {
    `); - }); + }); - it('adds the `chrome-hidden` class to the AppWrapper when chrome is hidden', () => { - const isVisible$ = new BehaviorSubject(true); - chrome.getIsVisible$.mockReturnValue(isVisible$); - startService(); + it('adds the `chrome-hidden` class to the AppWrapper when chrome is hidden', () => { + const isVisible$ = new BehaviorSubject(true); + chrome.getIsVisible$.mockReturnValue(isVisible$); + startService(); - const appWrapper = targetDomElement.querySelector('div.app-wrapper')!; - expect(appWrapper.className).toEqual('app-wrapper'); + const appWrapper = targetDomElement.querySelector('div.app-wrapper')!; + expect(appWrapper.className).toEqual('app-wrapper'); - act(() => isVisible$.next(false)); - expect(appWrapper.className).toEqual('app-wrapper hidden-chrome'); + act(() => isVisible$.next(false)); + expect(appWrapper.className).toEqual('app-wrapper hidden-chrome'); - act(() => isVisible$.next(true)); - expect(appWrapper.className).toEqual('app-wrapper'); - }); + act(() => isVisible$.next(true)); + expect(appWrapper.className).toEqual('app-wrapper'); + }); - it('adds the application classes to the AppContainer', () => { - const applicationClasses$ = new BehaviorSubject([]); - chrome.getApplicationClasses$.mockReturnValue(applicationClasses$); - startService(); + it('adds the application classes to the AppContainer', () => { + const applicationClasses$ = new BehaviorSubject([]); + chrome.getApplicationClasses$.mockReturnValue(applicationClasses$); + startService(); - const appContainer = targetDomElement.querySelector('div.application')!; - expect(appContainer.className).toEqual('application'); + const appContainer = targetDomElement.querySelector('div.application')!; + expect(appContainer.className).toEqual('application'); - act(() => applicationClasses$.next(['classA', 'classB'])); - expect(appContainer.className).toEqual('application classA classB'); + act(() => applicationClasses$.next(['classA', 'classB'])); + expect(appContainer.className).toEqual('application classA classB'); - act(() => applicationClasses$.next(['classC'])); - expect(appContainer.className).toEqual('application classC'); + act(() => applicationClasses$.next(['classC'])); + expect(appContainer.className).toEqual('application classC'); - act(() => applicationClasses$.next([])); - expect(appContainer.className).toEqual('application'); - }); + act(() => applicationClasses$.next([])); + expect(appContainer.className).toEqual('application'); + }); - it('contains wrapper divs', () => { - startService(); - expect(targetDomElement.querySelector('div.app-wrapper')).toBeDefined(); - expect(targetDomElement.querySelector('div.app-wrapper-pannel')).toBeDefined(); - }); + it('contains wrapper divs', () => { + startService(); + expect(targetDomElement.querySelector('div.app-wrapper')).toBeDefined(); + expect(targetDomElement.querySelector('div.app-wrapper-pannel')).toBeDefined(); + }); - it('renders the banner UI', () => { - startService(); - expect(targetDomElement.querySelector('#globalBannerList')).toMatchInlineSnapshot(` + it('renders the banner UI', () => { + startService(); + expect(targetDomElement.querySelector('#globalBannerList')).toMatchInlineSnapshot(`
    @@ -130,36 +120,5 @@ describe('RenderingService#start', () => {
    `); - }); - }); - - describe('legacy mode', () => { - beforeEach(() => { - injectedMetadata.getLegacyMode.mockReturnValue(true); - }); - - it('renders into provided DOM element', () => { - startService(); - - expect(targetDomElement).toMatchInlineSnapshot(` -
    -
    -
    - Hello chrome! -
    -
    -
    -
    - `); - }); - - it('returns a div for the legacy service to render into', () => { - const { legacyTargetDomElement } = startService(); - - expect(targetDomElement.contains(legacyTargetDomElement!)).toBe(true); - }); }); }); diff --git a/src/core/public/rendering/rendering_service.tsx b/src/core/public/rendering/rendering_service.tsx index 58b8c1921e333b..a20e14dbf61c5b 100644 --- a/src/core/public/rendering/rendering_service.tsx +++ b/src/core/public/rendering/rendering_service.tsx @@ -23,14 +23,12 @@ import { I18nProvider } from '@kbn/i18n/react'; import { InternalChromeStart } from '../chrome'; import { InternalApplicationStart } from '../application'; -import { InjectedMetadataStart } from '../injected_metadata'; import { OverlayStart } from '../overlays'; import { AppWrapper, AppContainer } from './app_containers'; interface StartDeps { application: InternalApplicationStart; chrome: InternalChromeStart; - injectedMetadata: InjectedMetadataStart; overlays: OverlayStart; targetDomElement: HTMLDivElement; } @@ -41,53 +39,28 @@ interface StartDeps { * @internalRemarks Currently this only renders Chrome UI. Notifications and * Overlays UI should be moved here as well. * - * @returns a DOM element for the legacy platform to render into. - * * @internal */ export class RenderingService { - start({ - application, - chrome, - injectedMetadata, - overlays, - targetDomElement, - }: StartDeps): RenderingStart { + start({ application, chrome, overlays, targetDomElement }: StartDeps) { const chromeUi = chrome.getHeaderComponent(); const appUi = application.getComponent(); const bannerUi = overlays.banners.getComponent(); - const legacyMode = injectedMetadata.getLegacyMode(); - const legacyRef = legacyMode ? React.createRef() : null; - ReactDOM.render(
    {chromeUi} - {!legacyMode && ( - -
    -
    {bannerUi}
    - {appUi} -
    -
    - )} - - {legacyMode &&
    } + +
    +
    {bannerUi}
    + {appUi} +
    +
    , targetDomElement ); - - return { - // When in legacy mode, return legacy div, otherwise undefined. - legacyTargetDomElement: legacyRef ? legacyRef.current! : undefined, - }; } } - -/** @internal */ -export interface RenderingStart { - legacyTargetDomElement?: HTMLDivElement; -} diff --git a/src/core/server/legacy/plugins/__snapshots__/get_nav_links.test.ts.snap b/src/core/server/legacy/plugins/__snapshots__/get_nav_links.test.ts.snap deleted file mode 100644 index c1b7164908ed68..00000000000000 --- a/src/core/server/legacy/plugins/__snapshots__/get_nav_links.test.ts.snap +++ /dev/null @@ -1,56 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` 1`] = ` -Array [ - Object { - "category": undefined, - "disableSubUrlTracking": undefined, - "disabled": false, - "euiIconType": undefined, - "hidden": false, - "icon": undefined, - "id": "link-a", - "linkToLastSubUrl": true, - "order": 0, - "subUrlBase": "/some-custom-url", - "title": "AppA", - "tooltip": "", - "url": "/some-custom-url", - }, - Object { - "category": undefined, - "disableSubUrlTracking": true, - "disabled": false, - "euiIconType": undefined, - "hidden": false, - "icon": undefined, - "id": "link-b", - "linkToLastSubUrl": true, - "order": 0, - "subUrlBase": "/url-b", - "title": "AppB", - "tooltip": "", - "url": "/url-b", - }, - Object { - "category": undefined, - "euiIconType": undefined, - "icon": undefined, - "id": "app-a", - "linkToLastSubUrl": true, - "order": 0, - "title": "AppA", - "url": "/app/app-a", - }, - Object { - "category": undefined, - "euiIconType": undefined, - "icon": undefined, - "id": "app-b", - "linkToLastSubUrl": true, - "order": 0, - "title": "AppB", - "url": "/app/app-b", - }, -] -`; diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts index f3ec2ed8335c5a..82e04496ffc3e6 100644 --- a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts +++ b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts @@ -31,7 +31,6 @@ import { collectUiExports as collectLegacyUiExports } from '../../../../legacy/u import { LoggerFactory } from '../../logging'; import { PackageInfo } from '../../config'; import { LegacyPluginSpec, LegacyPluginPack, LegacyConfig } from '../types'; -import { getNavLinks } from './get_nav_links'; export async function findLegacyPluginSpecs( settings: unknown, @@ -125,13 +124,12 @@ export async function findLegacyPluginSpecs( log$.pipe(toArray()) ).toPromise(); const uiExports = collectLegacyUiExports(pluginSpecs); - const navLinks = getNavLinks(uiExports, pluginSpecs); return { disabledPluginSpecs, pluginSpecs, pluginExtendedConfig: configToMutate, uiExports, - navLinks, + navLinks: [], }; } diff --git a/src/core/server/legacy/plugins/get_nav_links.test.ts b/src/core/server/legacy/plugins/get_nav_links.test.ts deleted file mode 100644 index af10706d0ea088..00000000000000 --- a/src/core/server/legacy/plugins/get_nav_links.test.ts +++ /dev/null @@ -1,288 +0,0 @@ -/* - * 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 { LegacyUiExports, LegacyPluginSpec, LegacyAppSpec, LegacyNavLinkSpec } from '../types'; -import { getNavLinks } from './get_nav_links'; - -const createLegacyExports = ({ - uiAppSpecs = [], - navLinkSpecs = [], -}: { - uiAppSpecs?: LegacyAppSpec[]; - navLinkSpecs?: LegacyNavLinkSpec[]; -}): LegacyUiExports => ({ - uiAppSpecs, - navLinkSpecs, - injectedVarsReplacers: [], - defaultInjectedVarProviders: [], - savedObjectMappings: [], - savedObjectSchemas: {}, - savedObjectMigrations: {}, - savedObjectValidations: {}, - savedObjectsManagement: {}, -}); - -const createPluginSpecs = (...ids: string[]): LegacyPluginSpec[] => - ids.map( - (id) => - ({ - getId: () => id, - } as LegacyPluginSpec) - ); - -describe('getNavLinks', () => { - describe('generating from uiAppSpecs', () => { - it('generates navlinks from legacy app specs', () => { - const navlinks = getNavLinks( - createLegacyExports({ - uiAppSpecs: [ - { - id: 'app-a', - title: 'AppA', - pluginId: 'pluginA', - }, - { - id: 'app-b', - title: 'AppB', - pluginId: 'pluginA', - }, - ], - }), - createPluginSpecs('pluginA') - ); - - expect(navlinks.length).toEqual(2); - expect(navlinks[0]).toEqual( - expect.objectContaining({ - id: 'app-a', - title: 'AppA', - url: '/app/app-a', - }) - ); - expect(navlinks[1]).toEqual( - expect.objectContaining({ - id: 'app-b', - title: 'AppB', - url: '/app/app-b', - }) - ); - }); - - it('uses the app id to generates the navlink id even if pluginId is specified', () => { - const navlinks = getNavLinks( - createLegacyExports({ - uiAppSpecs: [ - { - id: 'app-a', - title: 'AppA', - pluginId: 'pluginA', - }, - { - id: 'app-b', - title: 'AppB', - pluginId: 'pluginA', - }, - ], - }), - createPluginSpecs('pluginA') - ); - - expect(navlinks.length).toEqual(2); - expect(navlinks[0].id).toEqual('app-a'); - expect(navlinks[1].id).toEqual('app-b'); - }); - - it('throws if an app reference a missing plugin', () => { - expect(() => { - getNavLinks( - createLegacyExports({ - uiAppSpecs: [ - { - id: 'app-a', - title: 'AppA', - pluginId: 'notExistingPlugin', - }, - ], - }), - createPluginSpecs('pluginA') - ); - }).toThrowErrorMatchingInlineSnapshot(`"Unknown plugin id \\"notExistingPlugin\\""`); - }); - - it('uses all known properties of the navlink', () => { - const navlinks = getNavLinks( - createLegacyExports({ - uiAppSpecs: [ - { - id: 'app-a', - title: 'AppA', - category: { - id: 'foo', - label: 'My Category', - }, - order: 42, - url: '/some-custom-url', - icon: 'fa-snowflake', - euiIconType: 'euiIcon', - linkToLastSubUrl: true, - hidden: false, - }, - ], - }), - [] - ); - expect(navlinks.length).toBe(1); - expect(navlinks[0]).toEqual({ - id: 'app-a', - title: 'AppA', - category: { - id: 'foo', - label: 'My Category', - }, - order: 42, - url: '/some-custom-url', - icon: 'fa-snowflake', - euiIconType: 'euiIcon', - linkToLastSubUrl: true, - }); - }); - }); - - describe('generating from navLinkSpecs', () => { - it('generates navlinks from legacy navLink specs', () => { - const navlinks = getNavLinks( - createLegacyExports({ - navLinkSpecs: [ - { - id: 'link-a', - title: 'AppA', - url: '/some-custom-url', - }, - { - id: 'link-b', - title: 'AppB', - url: '/some-other-url', - disableSubUrlTracking: true, - }, - ], - }), - createPluginSpecs('pluginA') - ); - - expect(navlinks.length).toEqual(2); - expect(navlinks[0]).toEqual( - expect.objectContaining({ - id: 'link-a', - title: 'AppA', - url: '/some-custom-url', - hidden: false, - disabled: false, - }) - ); - expect(navlinks[1]).toEqual( - expect.objectContaining({ - id: 'link-b', - title: 'AppB', - url: '/some-other-url', - disableSubUrlTracking: true, - }) - ); - }); - - it('only uses known properties to create the navlink', () => { - const navlinks = getNavLinks( - createLegacyExports({ - navLinkSpecs: [ - { - id: 'link-a', - title: 'AppA', - category: { - id: 'foo', - label: 'My Second Cat', - }, - order: 72, - url: '/some-other-custom', - subUrlBase: '/some-other-custom/sub', - disableSubUrlTracking: true, - icon: 'fa-corn', - euiIconType: 'euiIconBis', - linkToLastSubUrl: false, - hidden: false, - tooltip: 'My other tooltip', - }, - ], - }), - [] - ); - expect(navlinks.length).toBe(1); - expect(navlinks[0]).toEqual({ - id: 'link-a', - title: 'AppA', - category: { - id: 'foo', - label: 'My Second Cat', - }, - order: 72, - url: '/some-other-custom', - subUrlBase: '/some-other-custom/sub', - disableSubUrlTracking: true, - icon: 'fa-corn', - euiIconType: 'euiIconBis', - linkToLastSubUrl: false, - hidden: false, - disabled: false, - tooltip: 'My other tooltip', - }); - }); - }); - - describe('generating from both apps and navlinks', () => { - const navlinks = getNavLinks( - createLegacyExports({ - uiAppSpecs: [ - { - id: 'app-a', - title: 'AppA', - }, - { - id: 'app-b', - title: 'AppB', - }, - ], - navLinkSpecs: [ - { - id: 'link-a', - title: 'AppA', - url: '/some-custom-url', - }, - { - id: 'link-b', - title: 'AppB', - url: '/url-b', - disableSubUrlTracking: true, - }, - ], - }), - [] - ); - - expect(navlinks.length).toBe(4); - expect(navlinks).toMatchSnapshot(); - }); -}); diff --git a/src/core/server/legacy/plugins/get_nav_links.ts b/src/core/server/legacy/plugins/get_nav_links.ts deleted file mode 100644 index b1d22df41e3454..00000000000000 --- a/src/core/server/legacy/plugins/get_nav_links.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 { - LegacyUiExports, - LegacyNavLink, - LegacyPluginSpec, - LegacyNavLinkSpec, - LegacyAppSpec, -} from '../types'; - -function legacyAppToNavLink(spec: LegacyAppSpec): LegacyNavLink { - if (!spec.id) { - throw new Error('Every app must specify an id'); - } - return { - id: spec.id, - category: spec.category, - title: spec.title ?? spec.id, - order: typeof spec.order === 'number' ? spec.order : 0, - icon: spec.icon, - euiIconType: spec.euiIconType, - url: spec.url || `/app/${spec.id}`, - linkToLastSubUrl: spec.linkToLastSubUrl ?? true, - }; -} - -function legacyLinkToNavLink(spec: LegacyNavLinkSpec): LegacyNavLink { - return { - id: spec.id, - category: spec.category, - title: spec.title, - order: typeof spec.order === 'number' ? spec.order : 0, - url: spec.url, - subUrlBase: spec.subUrlBase || spec.url, - disableSubUrlTracking: spec.disableSubUrlTracking, - icon: spec.icon, - euiIconType: spec.euiIconType, - linkToLastSubUrl: spec.linkToLastSubUrl ?? true, - hidden: spec.hidden ?? false, - disabled: spec.disabled ?? false, - tooltip: spec.tooltip ?? '', - }; -} - -function isHidden(app: LegacyAppSpec) { - return app.listed === false || app.hidden === true; -} - -export function getNavLinks(uiExports: LegacyUiExports, pluginSpecs: LegacyPluginSpec[]) { - const navLinkSpecs = uiExports.navLinkSpecs || []; - const appSpecs = (uiExports.uiAppSpecs || []).filter( - (app) => app !== undefined && !isHidden(app) - ) as LegacyAppSpec[]; - - const pluginIds = (pluginSpecs || []).map((spec) => spec.getId()); - appSpecs.forEach((spec) => { - if (spec.pluginId && !pluginIds.includes(spec.pluginId)) { - throw new Error(`Unknown plugin id "${spec.pluginId}"`); - } - }); - - return [...navLinkSpecs.map(legacyLinkToNavLink), ...appSpecs.map(legacyAppToNavLink)].sort( - (a, b) => a.order - b.order - ); -} diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap index 5ff5d69f96f707..ab828a1780425a 100644 --- a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -46,7 +46,6 @@ Object { }, "version": Any, }, - "legacyMode": false, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, @@ -100,7 +99,6 @@ Object { }, "version": Any, }, - "legacyMode": false, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, @@ -158,7 +156,6 @@ Object { }, "version": Any, }, - "legacyMode": false, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, @@ -212,7 +209,6 @@ Object { }, "version": Any, }, - "legacyMode": false, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, @@ -266,7 +262,6 @@ Object { }, "version": Any, }, - "legacyMode": false, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index e7ee0b16fce081..7761c89044f6fa 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -82,7 +82,6 @@ export class RenderingService implements CoreService | undefined + private appList?: ReadonlyMap | undefined ) {} /** diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index fb09729ab71c32..2ca31994b722d8 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -29,7 +29,6 @@ import { Plugin, ScopedHistory, PublicAppInfo, - PublicLegacyAppInfo, } from '../../../core/public'; import { EmbeddableFactoryRegistry, EmbeddableFactoryProvider } from './types'; import { bootstrap } from './bootstrap'; @@ -92,7 +91,7 @@ export class EmbeddablePublicPlugin implements Plugin; + private appList?: ReadonlyMap; private appListSubscription?: Subscription; constructor(initializerContext: PluginInitializerContext) {} diff --git a/src/plugins/kibana_legacy/public/angular/angular_config.tsx b/src/plugins/kibana_legacy/public/angular/angular_config.tsx index eafcbfda3db003..9dae615bc3848d 100644 --- a/src/plugins/kibana_legacy/public/angular/angular_config.tsx +++ b/src/plugins/kibana_legacy/public/angular/angular_config.tsx @@ -27,12 +27,12 @@ import { } from 'angular'; import $ from 'jquery'; import { set } from '@elastic/safer-lodash-set'; -import { cloneDeep, forOwn, get } from 'lodash'; +import { get } from 'lodash'; import * as Rx from 'rxjs'; import { ChromeBreadcrumb, EnvironmentMode, PackageInfo } from 'kibana/public'; import { History } from 'history'; -import { CoreStart, LegacyCoreStart } from 'kibana/public'; +import { CoreStart } from 'kibana/public'; import { isSystemApiRequest } from '../utils'; import { formatAngularHttpError, isAngularHttpError } from '../notify/lib'; @@ -72,32 +72,18 @@ function isDummyRoute($route: any, isLocalAngular: boolean) { export const configureAppAngularModule = ( angularModule: IModule, - newPlatform: - | LegacyCoreStart - | { - core: CoreStart; - readonly env: { - mode: Readonly; - packageInfo: Readonly; - }; - }, + newPlatform: { + core: CoreStart; + readonly env: { + mode: Readonly; + packageInfo: Readonly; + }; + }, isLocalAngular: boolean, getHistory?: () => History ) => { const core = 'core' in newPlatform ? newPlatform.core : newPlatform; - const packageInfo = - 'env' in newPlatform - ? newPlatform.env.packageInfo - : newPlatform.injectedMetadata.getLegacyMetadata(); - - if ('injectedMetadata' in newPlatform) { - forOwn(newPlatform.injectedMetadata.getInjectedVars(), (val, name) => { - if (name !== undefined) { - // The legacy platform modifies some of these values, clone to an unfrozen object. - angularModule.value(name, cloneDeep(val)); - } - }); - } + const packageInfo = newPlatform.env.packageInfo; angularModule .value('kbnVersion', packageInfo.version) @@ -105,13 +91,7 @@ export const configureAppAngularModule = ( .value('buildSha', packageInfo.buildSha) .value('esUrl', getEsUrl(core)) .value('uiCapabilities', core.application.capabilities) - .config( - setupCompileProvider( - 'injectedMetadata' in newPlatform - ? newPlatform.injectedMetadata.getLegacyMetadata().devMode - : newPlatform.env.mode.dev - ) - ) + .config(setupCompileProvider(newPlatform.env.mode.dev)) .config(setupLocationProvider()) .config($setupXsrfRequestInterceptor(packageInfo.version)) .run(capture$httpLoadingCount(core)) diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts index e3133fd6d31c2a..e50469546417cb 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts @@ -21,7 +21,7 @@ import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; import { createMemoryHistory, History } from 'history'; import { createKbnUrlTracker, KbnUrlTracker } from './kbn_url_tracker'; import { BehaviorSubject, Subject } from 'rxjs'; -import { AppBase, AppUpdater, ToastsSetup } from 'kibana/public'; +import { App, AppUpdater, ToastsSetup } from 'kibana/public'; import { coreMock } from '../../../../../core/public/mocks'; import { unhashUrl } from './hash_unhash_url'; @@ -62,7 +62,7 @@ describe('kbnUrlTracker', () => { } function getActiveNavLinkUrl() { - return navLinkUpdaterSubject.getValue()({} as AppBase)?.defaultPath; + return navLinkUpdaterSubject.getValue()({} as App)?.defaultPath; } beforeEach(() => { diff --git a/src/plugins/saved_objects/public/saved_object/saved_object_loader.ts b/src/plugins/saved_objects/public/saved_object/saved_object_loader.ts index 9e7346f3b673cd..6c791f5339def4 100644 --- a/src/plugins/saved_objects/public/saved_object/saved_object_loader.ts +++ b/src/plugins/saved_objects/public/saved_object/saved_object_loader.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { ChromeStart, SavedObjectsClientContract, SavedObjectsFindOptions } from 'kibana/public'; +import { SavedObjectsClientContract, SavedObjectsFindOptions } from 'kibana/public'; import { SavedObject } from '../types'; import { StringUtils } from './helpers/string_utils'; @@ -36,8 +36,7 @@ export class SavedObjectLoader { constructor( SavedObjectClass: any, - private readonly savedObjectsClient: SavedObjectsClientContract, - private readonly chrome: ChromeStart + private readonly savedObjectsClient: SavedObjectsClientContract ) { this.type = SavedObjectClass.type; this.Class = SavedObjectClass; @@ -78,19 +77,6 @@ export class SavedObjectLoader { return savedObject.delete(); }); await Promise.all(deletions); - - const coreNavLinks = this.chrome.navLinks; - /** - * Modify last url for deleted saved objects to avoid loading pages with "Could not locate..." - */ - coreNavLinks - .getAll() - .filter( - (link) => - link.linkToLastSubUrl && - idsUsed.find((deletedId) => link.url && link.url.includes(deletedId)) !== undefined - ) - .forEach((link) => coreNavLinks.update(link.id, { url: link.baseUrl })); } /** diff --git a/src/plugins/timelion/public/services/saved_sheets.ts b/src/plugins/timelion/public/services/saved_sheets.ts index a3e7f66d9ee475..9ad529cb0134b6 100644 --- a/src/plugins/timelion/public/services/saved_sheets.ts +++ b/src/plugins/timelion/public/services/saved_sheets.ts @@ -33,7 +33,7 @@ export function initSavedSheetService(app: angular.IModule, deps: RenderDeps) { const SavedSheet = createSavedSheetClass(services, deps.core.uiSettings); - const savedSheetLoader = new SavedObjectLoader(SavedSheet, savedObjectsClient, deps.core.chrome); + const savedSheetLoader = new SavedObjectLoader(SavedSheet, savedObjectsClient); savedSheetLoader.urlFor = (id) => `#/${encodeURIComponent(id)}`; // Customize loader properties since adding an 's' on type doesn't work for type 'timelion-sheet'. savedSheetLoader.loaderProperties = { diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts index 94538b4081aef6..0ec3c0dab2e977 100644 --- a/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts +++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts @@ -76,9 +76,7 @@ export function createSavedVisLoader(services: SavedObjectKibanaServicesWithVisu } } const SavedVis = createSavedVisClass(services); - return new SavedObjectLoaderVisualize( - SavedVis, - savedObjectsClient, - services.chrome - ) as SavedObjectLoader & { findListItems: (search: string, size: number) => any }; + return new SavedObjectLoaderVisualize(SavedVis, savedObjectsClient) as SavedObjectLoader & { + findListItems: (search: string, size: number) => any; + }; } diff --git a/test/plugin_functional/test_suites/core_plugins/applications.ts b/test/plugin_functional/test_suites/core_plugins/applications.ts index 9ecef65f5f09a1..4c72c091a2bee4 100644 --- a/test/plugin_functional/test_suites/core_plugins/applications.ts +++ b/test/plugin_functional/test_suites/core_plugins/applications.ts @@ -145,16 +145,5 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const windowHeight = (await browser.getWindowSize()).height; expect(wrapperHeight).to.be.below(windowHeight); }); - - // Not sure if we need this test or not. If yes, we need to find another legacy app - it.skip('can navigate from NP apps to legacy apps', async () => { - await appsMenu.clickLink('Stack Management'); - await testSubjects.existOrFail('managementNav'); - }); - - it('can navigate from legacy apps to NP apps', async () => { - await appsMenu.clickLink('Foo'); - await testSubjects.existOrFail('fooAppHome'); - }); }); } diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 5b83d2d5654111..9931a3fabcd8d3 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -51,11 +51,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide window.__RENDERING_SESSION__.push(window.location.pathname); }); }; - const getLegacyMode = () => - browser.execute(() => { - return JSON.parse(document.querySelector('kbn-injected-metadata')!.getAttribute('data')!) - .legacyMode; - }); + const getUserSettings = () => browser.execute(() => { return JSON.parse(document.querySelector('kbn-injected-metadata')!.getAttribute('data')!) @@ -73,13 +69,11 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide it('renders "core" application', async () => { await navigateTo('/render/core'); - const [loadingMessage, legacyMode, userSettings] = await Promise.all([ + const [loadingMessage, userSettings] = await Promise.all([ findLoadingMessage(), - getLegacyMode(), getUserSettings(), ]); - expect(legacyMode).to.be(false); expect(userSettings).to.not.be.empty(); await find.waitForElementStale(loadingMessage); @@ -90,13 +84,11 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide it('renders "core" application without user settings', async () => { await navigateTo('/render/core?includeUserSettings=false'); - const [loadingMessage, legacyMode, userSettings] = await Promise.all([ + const [loadingMessage, userSettings] = await Promise.all([ findLoadingMessage(), - getLegacyMode(), getUserSettings(), ]); - expect(legacyMode).to.be(false); expect(userSettings).to.be.empty(); await find.waitForElementStale(loadingMessage); diff --git a/x-pack/plugins/global_search_providers/public/providers/application.test.ts b/x-pack/plugins/global_search_providers/public/providers/application.test.ts index 7d4143f9bcfa72..385ce91d8f9810 100644 --- a/x-pack/plugins/global_search_providers/public/providers/application.test.ts +++ b/x-pack/plugins/global_search_providers/public/providers/application.test.ts @@ -25,7 +25,6 @@ const createApp = (props: Partial = {}): PublicAppInfo => ({ id: 'app1', title: 'App 1', appRoute: '/app/app1', - legacy: false, status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, chromeless: false, diff --git a/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts b/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts index 1c5a446b8e5645..0c9daadf14fc62 100644 --- a/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts +++ b/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts @@ -4,30 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AppNavLinkStatus, AppStatus, PublicAppInfo, PublicLegacyAppInfo } from 'src/core/public'; +import { AppNavLinkStatus, AppStatus, PublicAppInfo } from 'src/core/public'; import { appToResult, getAppResults, scoreApp } from './get_app_results'; const createApp = (props: Partial = {}): PublicAppInfo => ({ id: 'app1', title: 'App 1', appRoute: '/app/app1', - legacy: false, status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, chromeless: false, ...props, }); -const createLegacyApp = (props: Partial = {}): PublicLegacyAppInfo => ({ - id: 'app1', - title: 'App 1', - appUrl: '/app/app1', - legacy: true, - status: AppStatus.accessible, - navLinkStatus: AppNavLinkStatus.visible, - ...props, -}); - describe('getAppResults', () => { it('retrieves the matching results', () => { const apps = [ @@ -72,14 +61,6 @@ describe('scoreApp', () => { expect(scoreApp('1-2-3-4-5', createApp({ title: '123456789' }))).toBe(0); }); }); - - it('works with legacy apps', () => { - expect(scoreApp('dashboard', createLegacyApp({ title: 'dashboard' }))).toBe(100); - expect(scoreApp('dash', createLegacyApp({ title: 'dashboard' }))).toBe(90); - expect(scoreApp('board', createLegacyApp({ title: 'dashboard' }))).toBe(75); - expect(scoreApp('0123456789', createLegacyApp({ title: '012345' }))).toBe(60); - expect(scoreApp('0123456789', createLegacyApp({ title: '12345' }))).toBe(0); - }); }); describe('appToResult', () => { @@ -99,21 +80,4 @@ describe('appToResult', () => { score: 42, }); }); - - it('converts a legacy app to a result', () => { - const app = createLegacyApp({ - id: 'legacy', - title: 'Legacy', - euiIconType: 'legacyIcon', - appUrl: '/app/legacy', - }); - expect(appToResult(app, 69)).toEqual({ - id: 'legacy', - title: 'Legacy', - type: 'application', - icon: 'legacyIcon', - url: '/app/legacy', - score: 69, - }); - }); }); diff --git a/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts b/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts index 1a1939230105b2..4b2901254076cf 100644 --- a/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts +++ b/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts @@ -5,12 +5,12 @@ */ import levenshtein from 'js-levenshtein'; -import { PublicAppInfo, PublicLegacyAppInfo } from 'src/core/public'; +import { PublicAppInfo } from 'src/core/public'; import { GlobalSearchProviderResult } from '../../../global_search/public'; export const getAppResults = ( term: string, - apps: Array + apps: PublicAppInfo[] ): GlobalSearchProviderResult[] => { return apps .map((app) => ({ app, score: scoreApp(term, app) })) @@ -18,7 +18,7 @@ export const getAppResults = ( .map(({ app, score }) => appToResult(app, score)); }; -export const scoreApp = (term: string, { title }: PublicAppInfo | PublicLegacyAppInfo): number => { +export const scoreApp = (term: string, { title }: PublicAppInfo): number => { term = term.toLowerCase(); title = title.toLowerCase(); @@ -43,16 +43,13 @@ export const scoreApp = (term: string, { title }: PublicAppInfo | PublicLegacyAp return 0; }; -export const appToResult = ( - app: PublicAppInfo | PublicLegacyAppInfo, - score: number -): GlobalSearchProviderResult => { +export const appToResult = (app: PublicAppInfo, score: number): GlobalSearchProviderResult => { return { id: app.id, title: app.title, type: 'application', icon: app.euiIconType, - url: app.legacy ? app.appUrl : app.appRoute, + url: app.appRoute, score, }; }; diff --git a/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.js b/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.js index 6c7e141c2ab5a7..66af92c7a687ba 100644 --- a/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.js @@ -25,5 +25,5 @@ export const getMapsSavedObjectLoader = _.once(function () { }; const SavedGisMap = createSavedGisMapClass(services); - return new SavedObjectLoader(SavedGisMap, getSavedObjectsClient(), getCoreChrome()); + return new SavedObjectLoader(SavedGisMap, getSavedObjectsClient()); }); From 2a547914b7c70e186e3c0d184648b65a9b5e3e34 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 3 Sep 2020 14:02:23 +0300 Subject: [PATCH 211/216] remove server_extensions mixin (#76606) --- src/legacy/server/kbn_server.d.ts | 4 - src/legacy/server/kbn_server.js | 3 - .../add_memoized_factory_to_request.test.js | 161 ------------------ src/legacy/server/server_extensions/index.js | 20 --- .../server_extensions_mixin.js | 64 ------- 5 files changed, 252 deletions(-) delete mode 100644 src/legacy/server/server_extensions/add_memoized_factory_to_request.test.js delete mode 100644 src/legacy/server/server_extensions/index.js delete mode 100644 src/legacy/server/server_extensions/server_extensions_mixin.js diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 1a1f43b93f26ed..627e9f4f86bc36 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -73,10 +73,6 @@ declare module 'hapi' { ) => void; getInjectedUiAppVars: (pluginName: string) => { [key: string]: any }; getUiNavLinks(): Array<{ _id: string }>; - addMemoizedFactoryToRequest: ( - name: string, - factoryFn: (request: Request) => Record - ) => void; logWithMetadata: (tags: string[], message: string, meta: Record) => void; newPlatform: KbnServer['newPlatform']; } diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 320086b6d65310..4692262d99bb54 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -34,7 +34,6 @@ import configCompleteMixin from './config/complete'; import { optimizeMixin } from '../../optimize'; import * as Plugins from './plugins'; import { savedObjectsMixin } from './saved_objects/saved_objects_mixin'; -import { serverExtensionsMixin } from './server_extensions'; import { uiMixin } from '../ui'; import { i18nMixin } from './i18n'; @@ -91,8 +90,6 @@ export default class KbnServer { coreMixin, - // adds methods for extending this.server - serverExtensionsMixin, loggingMixin, warningsMixin, statusMixin, diff --git a/src/legacy/server/server_extensions/add_memoized_factory_to_request.test.js b/src/legacy/server/server_extensions/add_memoized_factory_to_request.test.js deleted file mode 100644 index 48bd0824680619..00000000000000 --- a/src/legacy/server/server_extensions/add_memoized_factory_to_request.test.js +++ /dev/null @@ -1,161 +0,0 @@ -/* - * 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 sinon from 'sinon'; - -import { serverExtensionsMixin } from './server_extensions_mixin'; - -describe('server.addMemoizedFactoryToRequest()', () => { - const setup = () => { - class Request {} - - class Server { - constructor() { - sinon.spy(this, 'decorate'); - } - decorate(type, name, value) { - switch (type) { - case 'request': - return (Request.prototype[name] = value); - case 'server': - return (Server.prototype[name] = value); - default: - throw new Error(`Unexpected decorate type ${type}`); - } - } - } - - const server = new Server(); - serverExtensionsMixin({}, server); - return { server, Request }; - }; - - it('throws when propertyName is not a string', () => { - const { server } = setup(); - expect(() => server.addMemoizedFactoryToRequest()).toThrowError('methodName must be a string'); - expect(() => server.addMemoizedFactoryToRequest(null)).toThrowError( - 'methodName must be a string' - ); - expect(() => server.addMemoizedFactoryToRequest(1)).toThrowError('methodName must be a string'); - expect(() => server.addMemoizedFactoryToRequest(true)).toThrowError( - 'methodName must be a string' - ); - expect(() => server.addMemoizedFactoryToRequest(/abc/)).toThrowError( - 'methodName must be a string' - ); - expect(() => server.addMemoizedFactoryToRequest(['foo'])).toThrowError( - 'methodName must be a string' - ); - expect(() => server.addMemoizedFactoryToRequest([1])).toThrowError( - 'methodName must be a string' - ); - expect(() => server.addMemoizedFactoryToRequest({})).toThrowError( - 'methodName must be a string' - ); - }); - - it('throws when factory is not a function', () => { - const { server } = setup(); - expect(() => server.addMemoizedFactoryToRequest('name')).toThrowError( - 'factory must be a function' - ); - expect(() => server.addMemoizedFactoryToRequest('name', null)).toThrowError( - 'factory must be a function' - ); - expect(() => server.addMemoizedFactoryToRequest('name', 1)).toThrowError( - 'factory must be a function' - ); - expect(() => server.addMemoizedFactoryToRequest('name', true)).toThrowError( - 'factory must be a function' - ); - expect(() => server.addMemoizedFactoryToRequest('name', /abc/)).toThrowError( - 'factory must be a function' - ); - expect(() => server.addMemoizedFactoryToRequest('name', ['foo'])).toThrowError( - 'factory must be a function' - ); - expect(() => server.addMemoizedFactoryToRequest('name', [1])).toThrowError( - 'factory must be a function' - ); - expect(() => server.addMemoizedFactoryToRequest('name', {})).toThrowError( - 'factory must be a function' - ); - }); - - it('throws when factory takes more than one arg', () => { - const { server } = setup(); - /* eslint-disable no-unused-vars */ - expect(() => server.addMemoizedFactoryToRequest('name', () => {})).not.toThrowError( - 'more than one argument' - ); - expect(() => server.addMemoizedFactoryToRequest('name', (a) => {})).not.toThrowError( - 'more than one argument' - ); - expect(() => server.addMemoizedFactoryToRequest('name', (a, b) => {})).toThrowError( - 'more than one argument' - ); - expect(() => server.addMemoizedFactoryToRequest('name', (a, b, c) => {})).toThrowError( - 'more than one argument' - ); - expect(() => server.addMemoizedFactoryToRequest('name', (a, b, c, d) => {})).toThrowError( - 'more than one argument' - ); - expect(() => server.addMemoizedFactoryToRequest('name', (a, b, c, d, e) => {})).toThrowError( - 'more than one argument' - ); - /* eslint-enable no-unused-vars */ - }); - - it('decorates request objects with a function at `propertyName`', () => { - const { server, Request } = setup(); - - expect(new Request()).not.toHaveProperty('decorated'); - server.addMemoizedFactoryToRequest('decorated', () => {}); - expect(typeof new Request().decorated).toBe('function'); - }); - - it('caches invocations of the factory to the request instance', () => { - const { server, Request } = setup(); - const factory = sinon.stub().returnsArg(0); - server.addMemoizedFactoryToRequest('foo', factory); - - const request1 = new Request(); - const request2 = new Request(); - - // call `foo()` on both requests a bunch of times, each time - // the return value should be exactly the same - expect(request1.foo()).toBe(request1); - expect(request1.foo()).toBe(request1); - expect(request1.foo()).toBe(request1); - expect(request1.foo()).toBe(request1); - expect(request1.foo()).toBe(request1); - expect(request1.foo()).toBe(request1); - - expect(request2.foo()).toBe(request2); - expect(request2.foo()).toBe(request2); - expect(request2.foo()).toBe(request2); - expect(request2.foo()).toBe(request2); - - // only two different requests, so factory should have only been - // called twice with the two request objects - sinon.assert.calledTwice(factory); - sinon.assert.calledWithExactly(factory, request1); - sinon.assert.calledWithExactly(factory, request2); - }); -}); diff --git a/src/legacy/server/server_extensions/index.js b/src/legacy/server/server_extensions/index.js deleted file mode 100644 index e17bd488897f73..00000000000000 --- a/src/legacy/server/server_extensions/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 { serverExtensionsMixin } from './server_extensions_mixin'; diff --git a/src/legacy/server/server_extensions/server_extensions_mixin.js b/src/legacy/server/server_extensions/server_extensions_mixin.js deleted file mode 100644 index 19c0b24ae15a15..00000000000000 --- a/src/legacy/server/server_extensions/server_extensions_mixin.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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 function serverExtensionsMixin(kbnServer, server) { - /** - * Decorate all request objects with a new method, `methodName`, - * that will call the `factory` on first invocation and return - * the result of the first call to subsequent invocations. - * - * @method server.addMemoizedFactoryToRequest - * @param {string} methodName location on the request this - * factory should be added - * @param {Function} factory the factory to add to the request, - * which will be called once per request - * with a single argument, the request. - * @return {undefined} - */ - server.decorate('server', 'addMemoizedFactoryToRequest', (methodName, factory) => { - if (typeof methodName !== 'string') { - throw new TypeError('methodName must be a string'); - } - - if (typeof factory !== 'function') { - throw new TypeError('factory must be a function'); - } - - if (factory.length > 1) { - throw new TypeError(` - factory must not take more than one argument, the request object. - Memoization is done based on the request instance and is cached and reused - regardless of other arguments. If you want to have a per-request cache that - also does some sort of secondary memoization then return an object or function - from the memoized decorator and do secondary memoization there. - `); - } - - const requestCache = new WeakMap(); - server.decorate('request', methodName, function () { - const request = this; - - if (!requestCache.has(request)) { - requestCache.set(request, factory(request)); - } - - return requestCache.get(request); - }); - }); -} From e32c282bc5e57caa901bfe0a2e6d6e61d09c3228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 3 Sep 2020 12:06:49 +0100 Subject: [PATCH 212/216] [Newsfeed] Ensure the version format when calling the API (#76381) Co-authored-by: Elastic Machine --- src/plugins/newsfeed/public/lib/api.test.ts | 14 +++++++------- src/plugins/newsfeed/public/lib/api.ts | 8 ++++++-- .../fixtures/plugins/newsfeed/server/plugin.ts | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/plugins/newsfeed/public/lib/api.test.ts b/src/plugins/newsfeed/public/lib/api.test.ts index be4274e8bc0541..21331b9d825755 100644 --- a/src/plugins/newsfeed/public/lib/api.test.ts +++ b/src/plugins/newsfeed/public/lib/api.test.ts @@ -48,7 +48,7 @@ Object.defineProperty(window, 'sessionStorage', { }); describe('NewsfeedApiDriver', () => { - const kibanaVersion = 'test_version'; + const kibanaVersion = '99.999.9-test_version'; // It'll remove the `-test_version` bit const userLanguage = 'en'; const fetchInterval = 2000; const getDriver = () => new NewsfeedApiDriver(kibanaVersion, userLanguage, fetchInterval); @@ -179,7 +179,7 @@ describe('NewsfeedApiDriver', () => { "error": null, "feedItems": Array [], "hasNew": false, - "kibanaVersion": "test_version", + "kibanaVersion": "99.999.9", } `); }); @@ -227,7 +227,7 @@ describe('NewsfeedApiDriver', () => { }, ], hasNew: true, - kibanaVersion: 'test_version', + kibanaVersion: '99.999.9', }); }); @@ -309,7 +309,7 @@ describe('NewsfeedApiDriver', () => { }, ], hasNew: true, - kibanaVersion: 'test_version', + kibanaVersion: '99.999.9', }); }); @@ -375,7 +375,7 @@ describe('NewsfeedApiDriver', () => { }, ], hasNew: true, - kibanaVersion: 'test_version', + kibanaVersion: '99.999.9', }); }); @@ -405,7 +405,7 @@ describe('NewsfeedApiDriver', () => { "error": null, "feedItems": Array [], "hasNew": false, - "kibanaVersion": "test_version", + "kibanaVersion": "99.999.9", } `); }); @@ -436,7 +436,7 @@ describe('NewsfeedApiDriver', () => { "error": null, "feedItems": Array [], "hasNew": false, - "kibanaVersion": "test_version", + "kibanaVersion": "99.999.9", } `); }); diff --git a/src/plugins/newsfeed/public/lib/api.ts b/src/plugins/newsfeed/public/lib/api.ts index ac38e064d26866..7253c0f3e294ca 100644 --- a/src/plugins/newsfeed/public/lib/api.ts +++ b/src/plugins/newsfeed/public/lib/api.ts @@ -32,13 +32,17 @@ import { ApiItem, NewsfeedItem, FetchResult, NewsfeedPluginBrowserConfig } from type ApiConfig = NewsfeedPluginBrowserConfig['service']; export class NewsfeedApiDriver { + private readonly kibanaVersion: string; private readonly loadedTime = moment().utc(); // the date is compared to time in UTC format coming from the service constructor( - private readonly kibanaVersion: string, + kibanaVersion: string, private readonly userLanguage: string, private readonly fetchInterval: number - ) {} + ) { + // The API only accepts versions in the format `X.Y.Z`, so we need to drop the `-SNAPSHOT` or any other label after it + this.kibanaVersion = kibanaVersion.replace(/^(\d+\.\d+\.\d+).*/, '$1'); + } shouldFetch(): boolean { const lastFetchUtc: string | null = sessionStorage.getItem(NEWSFEED_LAST_FETCH_STORAGE_KEY); diff --git a/test/common/fixtures/plugins/newsfeed/server/plugin.ts b/test/common/fixtures/plugins/newsfeed/server/plugin.ts index 60044d4e0b2dfd..3d64c7ff58e5d0 100644 --- a/test/common/fixtures/plugins/newsfeed/server/plugin.ts +++ b/test/common/fixtures/plugins/newsfeed/server/plugin.ts @@ -25,7 +25,7 @@ export class NewsFeedSimulatorPlugin implements Plugin { public setup({ http }: CoreSetup) { const router = http.createRouter(); - const version = this.initializerContext.env.packageInfo.version; + const version = this.initializerContext.env.packageInfo.version.replace('-SNAPSHOT', ''); router.get( { From 4c5e75e437e1002e2f0c1d2bbf0cdd04a5b9ddf2 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Thu, 3 Sep 2020 15:17:23 +0300 Subject: [PATCH 213/216] fix bad merge (#76629) Merged to fix master --- .../public/hosts/containers/authentications/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx index c11de9100e71ed..19294fc5e47804 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx @@ -136,7 +136,7 @@ export const useAuthentications = ({ const searchSubscription$ = data.search .search(request, { strategy: 'securitySolutionSearchStrategy', - signal: abortCtrl.current.signal, + abortSignal: abortCtrl.current.signal, }) .subscribe({ next: (response) => { From e9e0ca37820e8a2d00d5180b6357ae386d56d619 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 3 Sep 2020 15:20:04 +0300 Subject: [PATCH 214/216] Introduce TS incremental builds & move src/test_utils to TS project (#76082) * move test_helpers to the core * create base tsconfig * all tsconfigs use the base one * use test_helpers exposed from the src/core * move getFieldFormatsRegistry to data plugin * add test_utils project * compile types after checkout * add a stub for platform tsconfig.json * fix broken import * fix broken path to the base config * set tsBuildInfoFile for project without outDir * do not commit tsbuildinfo file * do not check output d.ts files * fix type error * use separate config to build types * rollback changes to include paths * mute import zone error * rename files to avoid references to tsd * do not use tsd for type tests * include all ts files in project * run buildRefs before type check to ensure the latest version * store tsbuildinfo locally * update paths to base config * comment out core/tsconfig.json * remove ui path * fix wrong tsbuildinfo path --- .gitignore | 3 + examples/alerting_example/tsconfig.json | 2 +- examples/bfetch_explorer/tsconfig.json | 2 +- .../tsconfig.json | 2 +- examples/developer_examples/tsconfig.json | 2 +- examples/embeddable_examples/tsconfig.json | 2 +- examples/embeddable_explorer/tsconfig.json | 2 +- examples/routing_example/tsconfig.json | 2 +- examples/search_examples/tsconfig.json | 2 +- .../state_containers_examples/tsconfig.json | 2 +- examples/ui_action_examples/tsconfig.json | 2 +- examples/ui_actions_explorer/tsconfig.json | 2 +- .../url_generators_examples/tsconfig.json | 2 +- .../url_generators_explorer/tsconfig.json | 2 +- package.json | 2 +- packages/elastic-datemath/tsconfig.json | 7 ++- .../elastic-safer-lodash-set/package.json | 5 +- .../elastic-safer-lodash-set/scripts/tsd.sh | 17 ----- .../test/{fp.test-d.ts => fp.ts} | 0 .../test/{fp_assoc.test-d.ts => fp_assoc.ts} | 0 ...fp_assocPath.test-d.ts => fp_assocPath.ts} | 0 .../test/{fp_set.test-d.ts => fp_set.ts} | 0 .../{fp_setWith.test-d.ts => fp_setWith.ts} | 0 .../test/{index.test-d.ts => index.ts} | 2 +- .../test/{set.test-d.ts => set.ts} | 0 .../test/{setWith.test-d.ts => setWith.ts} | 0 .../elastic-safer-lodash-set/tsconfig.json | 10 +-- packages/kbn-analytics/scripts/build.js | 1 - packages/kbn-analytics/tsconfig.json | 7 ++- packages/kbn-config-schema/tsconfig.json | 2 +- packages/kbn-dev-utils/tsconfig.json | 2 +- packages/kbn-es-archiver/tsconfig.json | 2 +- packages/kbn-es/tsconfig.json | 7 ++- packages/kbn-expect/tsconfig.json | 5 +- packages/kbn-i18n/scripts/build.js | 1 - packages/kbn-i18n/tsconfig.json | 5 +- packages/kbn-interpreter/tsconfig.json | 5 +- packages/kbn-monaco/tsconfig.json | 2 +- packages/kbn-optimizer/tsconfig.json | 5 +- packages/kbn-plugin-generator/tsconfig.json | 2 +- packages/kbn-plugin-helpers/tsconfig.json | 2 +- packages/kbn-pm/tsconfig.json | 5 +- packages/kbn-release-notes/tsconfig.json | 2 +- packages/kbn-telemetry-tools/tsconfig.json | 5 +- packages/kbn-test-subj-selector/tsconfig.json | 7 ++- packages/kbn-test/tsconfig.json | 5 +- packages/kbn-ui-shared-deps/tsconfig.json | 5 +- packages/kbn-utility-types/tsconfig.json | 2 +- scripts/build_ts_refs.js | 20 ++++++ src/core/TESTING.md | 2 +- .../ui_settings/ui_settings_api.test.ts | 2 +- .../config_deprecation.test.ts | 2 +- .../default_route_provider_config.test.ts | 2 +- .../integration_tests/static_assets.test.ts | 2 +- .../integration_tests/core_services.test.ts | 2 +- .../http_resources_service.test.ts | 2 +- .../integration_tests/legacy_service.test.ts | 2 +- .../legacy/integration_tests/logging.test.ts | 2 +- .../logging/integration_tests/logging.test.ts | 2 +- .../routes/integration_tests/migrate.test.ts | 2 +- .../create_or_upgrade.test.ts | 2 +- .../integration_tests/lib/servers.ts | 2 +- .../integration_tests/routes.test.ts | 2 +- .../test_helpers}/http_test_setup.ts | 6 +- .../test_helpers}/kbn_server.ts | 12 ++-- src/core/tsconfig.json | 23 +++++++ src/dev/typescript/build_refs.ts | 37 +++++++++++ src/dev/typescript/projects.ts | 1 + src/dev/typescript/run_type_check_cli.ts | 14 ++++- .../max_payload_size.test.js | 2 +- .../lib/spec_definitions/js/mappings.ts | 45 ++++++-------- .../field_formats_registry.stub.ts} | 9 +-- .../index_patterns_api_client.test.mock.ts | 2 +- src/plugins/data/public/test_utils.ts | 20 ++++++ .../public/agg_table/agg_table.test.js | 2 +- .../public/agg_table/agg_table_group.test.js | 2 +- .../components/lib/tick_formatter.test.js | 2 +- src/test_utils/public/stub_index_pattern.js | 3 +- src/test_utils/tsconfig.json | 16 +++++ .../plugins/kbn_tp_run_pipeline/tsconfig.json | 2 +- .../plugins/app_link_test/tsconfig.json | 2 +- .../plugins/core_app_status/tsconfig.json | 2 +- .../plugins/core_plugin_a/tsconfig.json | 2 +- .../core_plugin_appleave/tsconfig.json | 2 +- .../plugins/core_plugin_b/tsconfig.json | 2 +- .../core_plugin_chromeless/tsconfig.json | 2 +- .../core_plugin_route_timeouts/tsconfig.json | 2 +- .../core_plugin_static_assets/tsconfig.json | 2 +- .../core_provider_plugin/tsconfig.json | 2 +- .../plugins/doc_views_plugin/tsconfig.json | 2 +- .../elasticsearch_client_plugin/tsconfig.json | 2 +- .../plugins/index_patterns/tsconfig.json | 2 +- .../kbn_sample_panel_action/tsconfig.json | 2 +- .../plugins/kbn_top_nav/tsconfig.json | 2 +- .../tsconfig.json | 6 +- .../management_test_plugin/tsconfig.json | 2 +- .../plugins/rendering_plugin/tsconfig.json | 2 +- .../plugins/ui_settings_plugin/tsconfig.json | 2 +- test/tsconfig.json | 10 +-- tsconfig.base.json | 60 ++++++++++++++++++ tsconfig.browser.json | 2 +- tsconfig.json | 62 +++---------------- tsconfig.refs.json | 6 ++ tsconfig.types.json | 4 +- .../tsconfig.json | 4 +- x-pack/plugins/apm/e2e/tsconfig.json | 2 +- x-pack/plugins/apm/scripts/tsconfig.json | 2 +- .../canvas/storybook/addon/tsconfig.json | 2 +- .../routes/apidoc_scripts/tsconfig.json | 2 +- x-pack/plugins/ml/tsconfig.json | 3 - .../session_timeout_http_interceptor.test.ts | 2 +- ...thorized_response_http_interceptor.test.ts | 2 +- .../security_solution/cypress/tsconfig.json | 3 +- .../on_post_auth_interceptor.test.ts | 2 +- .../on_request_interceptor.test.ts | 2 +- x-pack/test/tsconfig.json | 3 +- x-pack/tsconfig.json | 11 +--- 117 files changed, 381 insertions(+), 240 deletions(-) delete mode 100755 packages/elastic-safer-lodash-set/scripts/tsd.sh rename packages/elastic-safer-lodash-set/test/{fp.test-d.ts => fp.ts} (100%) rename packages/elastic-safer-lodash-set/test/{fp_assoc.test-d.ts => fp_assoc.ts} (100%) rename packages/elastic-safer-lodash-set/test/{fp_assocPath.test-d.ts => fp_assocPath.ts} (100%) rename packages/elastic-safer-lodash-set/test/{fp_set.test-d.ts => fp_set.ts} (100%) rename packages/elastic-safer-lodash-set/test/{fp_setWith.test-d.ts => fp_setWith.ts} (100%) rename packages/elastic-safer-lodash-set/test/{index.test-d.ts => index.ts} (96%) rename packages/elastic-safer-lodash-set/test/{set.test-d.ts => set.ts} (100%) rename packages/elastic-safer-lodash-set/test/{setWith.test-d.ts => setWith.ts} (100%) create mode 100644 scripts/build_ts_refs.js rename src/{test_utils/public => core/test_helpers}/http_test_setup.ts (85%) rename src/{test_utils => core/test_helpers}/kbn_server.ts (96%) create mode 100644 src/core/tsconfig.json create mode 100644 src/dev/typescript/build_refs.ts rename src/{test_utils/public/stub_field_formats.ts => plugins/data/public/field_formats/field_formats_registry.stub.ts} (81%) create mode 100644 src/plugins/data/public/test_utils.ts create mode 100644 src/test_utils/tsconfig.json create mode 100644 tsconfig.base.json create mode 100644 tsconfig.refs.json delete mode 100644 x-pack/plugins/ml/tsconfig.json diff --git a/.gitignore b/.gitignore index 1d12ef2a9cff3a..1bbd38debbf0f8 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,6 @@ apm.tsconfig.json # release notes script output report.csv report.asciidoc + +# TS incremental build cache +*.tsbuildinfo diff --git a/examples/alerting_example/tsconfig.json b/examples/alerting_example/tsconfig.json index fbcec9de439bdb..09c130aca46429 100644 --- a/examples/alerting_example/tsconfig.json +++ b/examples/alerting_example/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target" }, diff --git a/examples/bfetch_explorer/tsconfig.json b/examples/bfetch_explorer/tsconfig.json index d508076b331990..798a9c222c5ab7 100644 --- a/examples/bfetch_explorer/tsconfig.json +++ b/examples/bfetch_explorer/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/dashboard_embeddable_examples/tsconfig.json b/examples/dashboard_embeddable_examples/tsconfig.json index d508076b331990..798a9c222c5ab7 100644 --- a/examples/dashboard_embeddable_examples/tsconfig.json +++ b/examples/dashboard_embeddable_examples/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/developer_examples/tsconfig.json b/examples/developer_examples/tsconfig.json index d508076b331990..798a9c222c5ab7 100644 --- a/examples/developer_examples/tsconfig.json +++ b/examples/developer_examples/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/embeddable_examples/tsconfig.json b/examples/embeddable_examples/tsconfig.json index 7fa03739119b43..caeed2c1a434fa 100644 --- a/examples/embeddable_examples/tsconfig.json +++ b/examples/embeddable_examples/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/embeddable_explorer/tsconfig.json b/examples/embeddable_explorer/tsconfig.json index d508076b331990..798a9c222c5ab7 100644 --- a/examples/embeddable_explorer/tsconfig.json +++ b/examples/embeddable_explorer/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/routing_example/tsconfig.json b/examples/routing_example/tsconfig.json index 9bbd9021b2e0a2..761a5c4da65ba9 100644 --- a/examples/routing_example/tsconfig.json +++ b/examples/routing_example/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/search_examples/tsconfig.json b/examples/search_examples/tsconfig.json index 8a3ced743d0fa3..8bec69ca40ccc0 100644 --- a/examples/search_examples/tsconfig.json +++ b/examples/search_examples/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/state_containers_examples/tsconfig.json b/examples/state_containers_examples/tsconfig.json index 3f43072c2aade0..007322e2d95257 100644 --- a/examples/state_containers_examples/tsconfig.json +++ b/examples/state_containers_examples/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/ui_action_examples/tsconfig.json b/examples/ui_action_examples/tsconfig.json index d508076b331990..798a9c222c5ab7 100644 --- a/examples/ui_action_examples/tsconfig.json +++ b/examples/ui_action_examples/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/ui_actions_explorer/tsconfig.json b/examples/ui_actions_explorer/tsconfig.json index 199fbe1fcfa269..119209114a7bb8 100644 --- a/examples/ui_actions_explorer/tsconfig.json +++ b/examples/ui_actions_explorer/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/url_generators_examples/tsconfig.json b/examples/url_generators_examples/tsconfig.json index 091130487791bd..327b4642a8e7f8 100644 --- a/examples/url_generators_examples/tsconfig.json +++ b/examples/url_generators_examples/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/url_generators_explorer/tsconfig.json b/examples/url_generators_explorer/tsconfig.json index 091130487791bd..327b4642a8e7f8 100644 --- a/examples/url_generators_explorer/tsconfig.json +++ b/examples/url_generators_explorer/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/package.json b/package.json index 28f2025300f395..c734e69affbdb2 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "kbn:watch": "node scripts/kibana --dev --logging.json=false", "build:types": "tsc --p tsconfig.types.json", "docs:acceptApiChanges": "node --max-old-space-size=6144 scripts/check_published_api_changes.js --accept", - "kbn:bootstrap": "node scripts/register_git_hook", + "kbn:bootstrap": "node scripts/build_ts_refs && node scripts/register_git_hook", "spec_to_console": "node scripts/spec_to_console", "backport-skip-ci": "backport --prDescription \"[skip-ci]\"", "storybook": "node scripts/storybook", diff --git a/packages/elastic-datemath/tsconfig.json b/packages/elastic-datemath/tsconfig.json index 3604f1004cf6c4..cbfe1e80474336 100644 --- a/packages/elastic-datemath/tsconfig.json +++ b/packages/elastic-datemath/tsconfig.json @@ -1,6 +1,9 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/elastic-datemath" + }, "include": [ "index.d.ts" - ], + ] } diff --git a/packages/elastic-safer-lodash-set/package.json b/packages/elastic-safer-lodash-set/package.json index f0f425661f605a..7602f2fa5924fd 100644 --- a/packages/elastic-safer-lodash-set/package.json +++ b/packages/elastic-safer-lodash-set/package.json @@ -16,7 +16,7 @@ "scripts": { "lint": "dependency-check --no-dev package.json set.js setWith.js fp/*.js", "test": "npm run lint && tape test/*.js && npm run test:types", - "test:types": "./scripts/tsd.sh", + "test:types": "tsc --noEmit", "update": "./scripts/update.sh", "save_state": "./scripts/save_state.sh" }, @@ -42,8 +42,5 @@ "ignore": [ "/lodash/" ] - }, - "tsd": { - "directory": "test" } } diff --git a/packages/elastic-safer-lodash-set/scripts/tsd.sh b/packages/elastic-safer-lodash-set/scripts/tsd.sh deleted file mode 100755 index 4572367df415df..00000000000000 --- a/packages/elastic-safer-lodash-set/scripts/tsd.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -# Elasticsearch B.V licenses this file to you under the MIT License. -# See `packages/elastic-safer-lodash-set/LICENSE` for more information. - -# tsd will get confused if it finds a tsconfig.json file in the project -# directory and start to scan the entirety of Kibana. We don't want that. -mv tsconfig.json tsconfig.tmp - -clean_up () { - exit_code=$? - mv tsconfig.tmp tsconfig.json - exit $exit_code -} -trap clean_up EXIT - -./node_modules/.bin/tsd diff --git a/packages/elastic-safer-lodash-set/test/fp.test-d.ts b/packages/elastic-safer-lodash-set/test/fp.ts similarity index 100% rename from packages/elastic-safer-lodash-set/test/fp.test-d.ts rename to packages/elastic-safer-lodash-set/test/fp.ts diff --git a/packages/elastic-safer-lodash-set/test/fp_assoc.test-d.ts b/packages/elastic-safer-lodash-set/test/fp_assoc.ts similarity index 100% rename from packages/elastic-safer-lodash-set/test/fp_assoc.test-d.ts rename to packages/elastic-safer-lodash-set/test/fp_assoc.ts diff --git a/packages/elastic-safer-lodash-set/test/fp_assocPath.test-d.ts b/packages/elastic-safer-lodash-set/test/fp_assocPath.ts similarity index 100% rename from packages/elastic-safer-lodash-set/test/fp_assocPath.test-d.ts rename to packages/elastic-safer-lodash-set/test/fp_assocPath.ts diff --git a/packages/elastic-safer-lodash-set/test/fp_set.test-d.ts b/packages/elastic-safer-lodash-set/test/fp_set.ts similarity index 100% rename from packages/elastic-safer-lodash-set/test/fp_set.test-d.ts rename to packages/elastic-safer-lodash-set/test/fp_set.ts diff --git a/packages/elastic-safer-lodash-set/test/fp_setWith.test-d.ts b/packages/elastic-safer-lodash-set/test/fp_setWith.ts similarity index 100% rename from packages/elastic-safer-lodash-set/test/fp_setWith.test-d.ts rename to packages/elastic-safer-lodash-set/test/fp_setWith.ts diff --git a/packages/elastic-safer-lodash-set/test/index.test-d.ts b/packages/elastic-safer-lodash-set/test/index.ts similarity index 96% rename from packages/elastic-safer-lodash-set/test/index.test-d.ts rename to packages/elastic-safer-lodash-set/test/index.ts index ab29d7de5a03fb..2090c1adcfce14 100644 --- a/packages/elastic-safer-lodash-set/test/index.test-d.ts +++ b/packages/elastic-safer-lodash-set/test/index.ts @@ -4,7 +4,7 @@ */ import { expectType } from 'tsd'; -import { set, setWith } from '../'; +import { set, setWith } from '..'; const someObj: object = {}; const anyValue: any = 'any value'; diff --git a/packages/elastic-safer-lodash-set/test/set.test-d.ts b/packages/elastic-safer-lodash-set/test/set.ts similarity index 100% rename from packages/elastic-safer-lodash-set/test/set.test-d.ts rename to packages/elastic-safer-lodash-set/test/set.ts diff --git a/packages/elastic-safer-lodash-set/test/setWith.test-d.ts b/packages/elastic-safer-lodash-set/test/setWith.ts similarity index 100% rename from packages/elastic-safer-lodash-set/test/setWith.test-d.ts rename to packages/elastic-safer-lodash-set/test/setWith.ts diff --git a/packages/elastic-safer-lodash-set/tsconfig.json b/packages/elastic-safer-lodash-set/tsconfig.json index bc1d1a3a7e413d..6517e5c60ee01a 100644 --- a/packages/elastic-safer-lodash-set/tsconfig.json +++ b/packages/elastic-safer-lodash-set/tsconfig.json @@ -1,9 +1,9 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/elastic-safer-lodash-set" + }, "include": [ - "**/*" + "**/*", ], - "exclude": [ - "**/*.test-d.ts" - ] } diff --git a/packages/kbn-analytics/scripts/build.js b/packages/kbn-analytics/scripts/build.js index 448d1ca9332f24..0e00a144d0b92c 100644 --- a/packages/kbn-analytics/scripts/build.js +++ b/packages/kbn-analytics/scripts/build.js @@ -71,7 +71,6 @@ run( proc.run(padRight(10, 'tsc'), { cmd: 'tsc', args: [ - '--emitDeclarationOnly', ...(flags.watch ? ['--watch', '--preserveWatchOutput', 'true'] : []), ...(flags['source-maps'] ? ['--declarationMap', 'true'] : []), ], diff --git a/packages/kbn-analytics/tsconfig.json b/packages/kbn-analytics/tsconfig.json index fdd9e8281fba88..861e0204a31a28 100644 --- a/packages/kbn-analytics/tsconfig.json +++ b/packages/kbn-analytics/tsconfig.json @@ -1,8 +1,9 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "declaration": true, - "declarationDir": "./target/types", + "emitDeclarationOnly": true, + "outDir": "./target/types", "stripInternal": true, "declarationMap": true, "types": [ @@ -11,7 +12,7 @@ ] }, "include": [ - "./src/**/*.ts" + "src/**/*" ], "exclude": [ "target" diff --git a/packages/kbn-config-schema/tsconfig.json b/packages/kbn-config-schema/tsconfig.json index f6c61268da17c9..6a268f2e7c0165 100644 --- a/packages/kbn-config-schema/tsconfig.json +++ b/packages/kbn-config-schema/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "declaration": true, "declarationDir": "./target/types", diff --git a/packages/kbn-dev-utils/tsconfig.json b/packages/kbn-dev-utils/tsconfig.json index 0ec058eeb8a280..1c6c671d0b7683 100644 --- a/packages/kbn-dev-utils/tsconfig.json +++ b/packages/kbn-dev-utils/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "target", "target": "ES2019", diff --git a/packages/kbn-es-archiver/tsconfig.json b/packages/kbn-es-archiver/tsconfig.json index 6ffa64d91fba0f..02209a29e58171 100644 --- a/packages/kbn-es-archiver/tsconfig.json +++ b/packages/kbn-es-archiver/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "declaration": true, diff --git a/packages/kbn-es/tsconfig.json b/packages/kbn-es/tsconfig.json index 6bb61453c99e75..9487a282326845 100644 --- a/packages/kbn-es/tsconfig.json +++ b/packages/kbn-es/tsconfig.json @@ -1,6 +1,9 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-es" + }, "include": [ - "src/**/*.ts" + "src/**/*" ] } diff --git a/packages/kbn-expect/tsconfig.json b/packages/kbn-expect/tsconfig.json index a09ae2d7ae6415..ae7e9ff090cc28 100644 --- a/packages/kbn-expect/tsconfig.json +++ b/packages/kbn-expect/tsconfig.json @@ -1,5 +1,8 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-expect" + }, "include": [ "expect.js.d.ts" ] diff --git a/packages/kbn-i18n/scripts/build.js b/packages/kbn-i18n/scripts/build.js index 62e1a35f003994..1d2b5031e37d74 100644 --- a/packages/kbn-i18n/scripts/build.js +++ b/packages/kbn-i18n/scripts/build.js @@ -71,7 +71,6 @@ run( proc.run(padRight(10, 'tsc'), { cmd: 'tsc', args: [ - '--emitDeclarationOnly', ...(flags.watch ? ['--watch', '--preserveWatchOutput', 'true'] : []), ...(flags['source-maps'] ? ['--declarationMap', 'true'] : []), ], diff --git a/packages/kbn-i18n/tsconfig.json b/packages/kbn-i18n/tsconfig.json index d3dae3078c1d7d..c6380f1cde9697 100644 --- a/packages/kbn-i18n/tsconfig.json +++ b/packages/kbn-i18n/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "include": [ "src/**/*.ts", "src/**/*.tsx", @@ -11,7 +11,8 @@ ], "compilerOptions": { "declaration": true, - "declarationDir": "./target/types", + "emitDeclarationOnly": true, + "outDir": "./target/types", "types": [ "jest", "node" diff --git a/packages/kbn-interpreter/tsconfig.json b/packages/kbn-interpreter/tsconfig.json index 63376a7ca1ae8b..3b81bbb118a55e 100644 --- a/packages/kbn-interpreter/tsconfig.json +++ b/packages/kbn-interpreter/tsconfig.json @@ -1,4 +1,7 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-interpreter" + }, "include": ["index.d.ts", "src/**/*.d.ts"] } diff --git a/packages/kbn-monaco/tsconfig.json b/packages/kbn-monaco/tsconfig.json index 95acfd32b24dd3..6d3f433c6a6d10 100644 --- a/packages/kbn-monaco/tsconfig.json +++ b/packages/kbn-monaco/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "declaration": true, diff --git a/packages/kbn-optimizer/tsconfig.json b/packages/kbn-optimizer/tsconfig.json index e2994f4d024149..20b06b5658cbcb 100644 --- a/packages/kbn-optimizer/tsconfig.json +++ b/packages/kbn-optimizer/tsconfig.json @@ -1,5 +1,8 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-optimizer" + }, "include": [ "index.d.ts", "src/**/*" diff --git a/packages/kbn-plugin-generator/tsconfig.json b/packages/kbn-plugin-generator/tsconfig.json index fc88223dae4b20..c54ff041d7065b 100644 --- a/packages/kbn-plugin-generator/tsconfig.json +++ b/packages/kbn-plugin-generator/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "target", "target": "ES2019", diff --git a/packages/kbn-plugin-helpers/tsconfig.json b/packages/kbn-plugin-helpers/tsconfig.json index e794b11b14afad..651bc79d6e7071 100644 --- a/packages/kbn-plugin-helpers/tsconfig.json +++ b/packages/kbn-plugin-helpers/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "target", "declaration": true, diff --git a/packages/kbn-pm/tsconfig.json b/packages/kbn-pm/tsconfig.json index c13a9243c50aa9..175c4701f2e5ba 100644 --- a/packages/kbn-pm/tsconfig.json +++ b/packages/kbn-pm/tsconfig.json @@ -1,12 +1,13 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "include": [ "./index.d.ts", "./src/**/*.ts", - "./dist/*.d.ts", + "./dist/*.d.ts" ], "exclude": [], "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-pm", "types": [ "jest", "node" diff --git a/packages/kbn-release-notes/tsconfig.json b/packages/kbn-release-notes/tsconfig.json index 6ffa64d91fba0f..02209a29e58171 100644 --- a/packages/kbn-release-notes/tsconfig.json +++ b/packages/kbn-release-notes/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "declaration": true, diff --git a/packages/kbn-telemetry-tools/tsconfig.json b/packages/kbn-telemetry-tools/tsconfig.json index 13ce8ef2bad60b..98512053a5c92b 100644 --- a/packages/kbn-telemetry-tools/tsconfig.json +++ b/packages/kbn-telemetry-tools/tsconfig.json @@ -1,5 +1,8 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-telemetry-tools" + }, "include": [ "src/**/*", ] diff --git a/packages/kbn-test-subj-selector/tsconfig.json b/packages/kbn-test-subj-selector/tsconfig.json index 3604f1004cf6c4..a1e1c1af372c69 100644 --- a/packages/kbn-test-subj-selector/tsconfig.json +++ b/packages/kbn-test-subj-selector/tsconfig.json @@ -1,6 +1,9 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-test-subj-selector" + }, "include": [ "index.d.ts" - ], + ] } diff --git a/packages/kbn-test/tsconfig.json b/packages/kbn-test/tsconfig.json index fdb53de52687bb..fec35e45b2a159 100644 --- a/packages/kbn-test/tsconfig.json +++ b/packages/kbn-test/tsconfig.json @@ -1,5 +1,8 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-test" + }, "include": [ "types/**/*", "src/**/*", diff --git a/packages/kbn-ui-shared-deps/tsconfig.json b/packages/kbn-ui-shared-deps/tsconfig.json index cef9a442d17bc0..88699027f85de8 100644 --- a/packages/kbn-ui-shared-deps/tsconfig.json +++ b/packages/kbn-ui-shared-deps/tsconfig.json @@ -1,5 +1,8 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-ui-shared-deps" + }, "include": [ "index.d.ts", "theme.ts" diff --git a/packages/kbn-utility-types/tsconfig.json b/packages/kbn-utility-types/tsconfig.json index 202df37faf5615..03cace5b9cb2c3 100644 --- a/packages/kbn-utility-types/tsconfig.json +++ b/packages/kbn-utility-types/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "declaration": true, "declarationDir": "./target", diff --git a/scripts/build_ts_refs.js b/scripts/build_ts_refs.js new file mode 100644 index 00000000000000..29fd66bab4ca99 --- /dev/null +++ b/scripts/build_ts_refs.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. + */ +require('../src/setup_node_env'); +require('../src/dev/typescript/build_refs').runBuildRefs(); diff --git a/src/core/TESTING.md b/src/core/TESTING.md index a62922d9b5d64b..a0fd0a6ffc2552 100644 --- a/src/core/TESTING.md +++ b/src/core/TESTING.md @@ -330,7 +330,7 @@ Cons: To have access to Kibana TestUtils, you should create `integration_tests` folder and import `test_utils` within a test file: ```typescript // src/plugins/my_plugin/server/integration_tests/formatter.test.ts -import * as kbnTestServer from 'src/test_utils/kbn_server'; +import * as kbnTestServer from 'src/core/test_helpers/kbn_server'; describe('myPlugin', () => { describe('GET /myPlugin/formatter', () => { diff --git a/src/core/public/ui_settings/ui_settings_api.test.ts b/src/core/public/ui_settings/ui_settings_api.test.ts index 14791407d25508..b15754e5f13839 100644 --- a/src/core/public/ui_settings/ui_settings_api.test.ts +++ b/src/core/public/ui_settings/ui_settings_api.test.ts @@ -22,7 +22,7 @@ import fetchMock from 'fetch-mock/es5/client'; import * as Rx from 'rxjs'; import { takeUntil, toArray } from 'rxjs/operators'; -import { setup as httpSetup } from '../../../test_utils/public/http_test_setup'; +import { setup as httpSetup } from '../../test_helpers/http_test_setup'; import { UiSettingsApi } from './ui_settings_api'; function setup() { diff --git a/src/core/server/config/integration_tests/config_deprecation.test.ts b/src/core/server/config/integration_tests/config_deprecation.test.ts index 65f5bbdac52489..3ebf6d507a2fdf 100644 --- a/src/core/server/config/integration_tests/config_deprecation.test.ts +++ b/src/core/server/config/integration_tests/config_deprecation.test.ts @@ -19,7 +19,7 @@ import { mockLoggingSystem } from './config_deprecation.test.mocks'; import { loggingSystemMock } from '../../logging/logging_system.mock'; -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_helpers/kbn_server'; describe('configuration deprecations', () => { let root: ReturnType; diff --git a/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts b/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts index 3284be5ba4750e..340f45a0a2c189 100644 --- a/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts +++ b/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_helpers/kbn_server'; import { Root } from '../../root'; const { startES } = kbnTestServer.createTestServers({ diff --git a/src/core/server/core_app/integration_tests/static_assets.test.ts b/src/core/server/core_app/integration_tests/static_assets.test.ts index 160ef064a14d96..ca03c4228221ff 100644 --- a/src/core/server/core_app/integration_tests/static_assets.test.ts +++ b/src/core/server/core_app/integration_tests/static_assets.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_helpers/kbn_server'; import { Root } from '../../root'; describe('Platform assets', function () { diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 2b9193a280aeca..f30ff66ed803a8 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -30,7 +30,7 @@ import { LegacyElasticsearchErrorHelpers } from '../../elasticsearch/legacy'; import { elasticsearchClientMock } from '../../elasticsearch/client/mocks'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_helpers/kbn_server'; import { InternalElasticsearchServiceStart } from '../../elasticsearch'; interface User { diff --git a/src/core/server/http_resources/integration_tests/http_resources_service.test.ts b/src/core/server/http_resources/integration_tests/http_resources_service.test.ts index eee7dc27860761..624cdbb7f9655a 100644 --- a/src/core/server/http_resources/integration_tests/http_resources_service.test.ts +++ b/src/core/server/http_resources/integration_tests/http_resources_service.test.ts @@ -17,7 +17,7 @@ * under the License. */ import { schema } from '@kbn/config-schema'; -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_helpers/kbn_server'; describe('http resources service', () => { describe('register', () => { diff --git a/src/core/server/legacy/integration_tests/legacy_service.test.ts b/src/core/server/legacy/integration_tests/legacy_service.test.ts index 1dc8d53e7c3d64..ca3573e730d3f4 100644 --- a/src/core/server/legacy/integration_tests/legacy_service.test.ts +++ b/src/core/server/legacy/integration_tests/legacy_service.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_helpers/kbn_server'; describe('legacy service', () => { describe('http server', () => { diff --git a/src/core/server/legacy/integration_tests/logging.test.ts b/src/core/server/legacy/integration_tests/logging.test.ts index 2581c85debf267..2ebe17ea92978c 100644 --- a/src/core/server/legacy/integration_tests/logging.test.ts +++ b/src/core/server/legacy/integration_tests/logging.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_helpers/kbn_server'; import { getPlatformLogsFromMock, diff --git a/src/core/server/logging/integration_tests/logging.test.ts b/src/core/server/logging/integration_tests/logging.test.ts index 841c1ce15af472..7f6059567c46ef 100644 --- a/src/core/server/logging/integration_tests/logging.test.ts +++ b/src/core/server/logging/integration_tests/logging.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_helpers/kbn_server'; import { InternalCoreSetup } from '../../internal_types'; import { LoggerContextConfigInput } from '../logging_config'; import { Subject } from 'rxjs'; diff --git a/src/core/server/saved_objects/routes/integration_tests/migrate.test.ts b/src/core/server/saved_objects/routes/integration_tests/migrate.test.ts index 7a0e39b71afb81..e003d564c1ea25 100644 --- a/src/core/server/saved_objects/routes/integration_tests/migrate.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/migrate.test.ts @@ -18,7 +18,7 @@ */ import { migratorInstanceMock } from './migrate.test.mocks'; -import * as kbnTestServer from '../../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../../test_helpers/kbn_server'; describe('SavedObjects /_migrate endpoint', () => { let root: ReturnType; diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts index d2e31dad58e55e..61b71f8c5de07d 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts @@ -24,7 +24,7 @@ import { TestElasticsearchUtils, TestKibanaUtils, TestUtils, -} from '../../../../../test_utils/kbn_server'; +} from '../../../../test_helpers/kbn_server'; import { createOrUpgradeSavedConfig } from '../create_or_upgrade_saved_config'; import { loggingSystemMock } from '../../../logging/logging_system.mock'; import { httpServerMock } from '../../../http/http_server.mocks'; diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts index 04979b69b32b9a..297deb0233c57e 100644 --- a/src/core/server/ui_settings/integration_tests/lib/servers.ts +++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts @@ -24,7 +24,7 @@ import { TestElasticsearchUtils, TestKibanaUtils, TestUtils, -} from '../../../../../test_utils/kbn_server'; +} from '../../../../test_helpers/kbn_server'; import { LegacyAPICaller } from '../../../elasticsearch/'; import { httpServerMock } from '../../../http/http_server.mocks'; diff --git a/src/core/server/ui_settings/integration_tests/routes.test.ts b/src/core/server/ui_settings/integration_tests/routes.test.ts index b18cc370fac3cd..063d68e3866b71 100644 --- a/src/core/server/ui_settings/integration_tests/routes.test.ts +++ b/src/core/server/ui_settings/integration_tests/routes.test.ts @@ -18,7 +18,7 @@ */ import { schema } from '@kbn/config-schema'; -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_helpers/kbn_server'; describe('ui settings service', () => { describe('routes', () => { diff --git a/src/test_utils/public/http_test_setup.ts b/src/core/test_helpers/http_test_setup.ts similarity index 85% rename from src/test_utils/public/http_test_setup.ts rename to src/core/test_helpers/http_test_setup.ts index 7c70f64887af11..50ea43fb22b5e4 100644 --- a/src/test_utils/public/http_test_setup.ts +++ b/src/core/test_helpers/http_test_setup.ts @@ -17,9 +17,9 @@ * under the License. */ -import { HttpService } from '../../core/public/http'; -import { fatalErrorsServiceMock } from '../../core/public/fatal_errors/fatal_errors_service.mock'; -import { injectedMetadataServiceMock } from '../../core/public/injected_metadata/injected_metadata_service.mock'; +import { HttpService } from '../public/http'; +import { fatalErrorsServiceMock } from '../public/fatal_errors/fatal_errors_service.mock'; +import { injectedMetadataServiceMock } from '../public/injected_metadata/injected_metadata_service.mock'; export type SetupTap = ( injectedMetadata: ReturnType, diff --git a/src/test_utils/kbn_server.ts b/src/core/test_helpers/kbn_server.ts similarity index 96% rename from src/test_utils/kbn_server.ts rename to src/core/test_helpers/kbn_server.ts index e44ce0de403d94..a494c6aa31d6fe 100644 --- a/src/test_utils/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -26,16 +26,16 @@ import { kibanaServerTestUser, kibanaTestUser, setupUsers, - // @ts-ignore: implicit any for JS file } from '@kbn/test'; import { defaultsDeep, get } from 'lodash'; import { resolve } from 'path'; import { BehaviorSubject } from 'rxjs'; import supertest from 'supertest'; -import { LegacyAPICaller } from '../core/server'; -import { CliArgs, Env } from '../core/server/config'; -import { Root } from '../core/server/root'; -import KbnServer from '../legacy/server/kbn_server'; + +import { LegacyAPICaller } from '../server/elasticsearch'; +import { CliArgs, Env } from '../server/config'; +import { Root } from '../server/root'; +import KbnServer from '../../legacy/server/kbn_server'; export type HttpMethod = 'delete' | 'get' | 'head' | 'post' | 'put'; @@ -53,7 +53,7 @@ const DEFAULTS_SETTINGS = { }; const DEFAULT_SETTINGS_WITH_CORE_PLUGINS = { - plugins: { scanDirs: [resolve(__dirname, '../legacy/core_plugins')] }, + plugins: { scanDirs: [resolve(__dirname, '../../legacy/core_plugins')] }, elasticsearch: { hosts: [esTestConfig.getUrl()], username: kibanaServerTestUser.username, diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json new file mode 100644 index 00000000000000..1a9e6253bff705 --- /dev/null +++ b/src/core/tsconfig.json @@ -0,0 +1,23 @@ +// { +// "extends": "../../tsconfig.base.json", +// "compilerOptions": { +// // "composite": true, +// "outDir": "./target", +// "emitDeclarationOnly": true, +// "declaration": true, +// "declarationMap": true +// }, +// "include": [ +// "public", +// "server", +// "types", +// "test_helpers", +// "utils", +// "index.ts", +// "../../kibana.d.ts", +// "../../typings/**/*" +// ], +// "references": [ +// { "path": "../test_utils" } +// ] +// } diff --git a/src/dev/typescript/build_refs.ts b/src/dev/typescript/build_refs.ts new file mode 100644 index 00000000000000..cbb596c185f8b3 --- /dev/null +++ b/src/dev/typescript/build_refs.ts @@ -0,0 +1,37 @@ +/* + * 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 execa from 'execa'; +import { run, ToolingLog } from '@kbn/dev-utils'; + +export async function buildRefs(log: ToolingLog) { + try { + log.info('Building TypeScript projects refs...'); + await execa(require.resolve('typescript/bin/tsc'), ['-b', 'tsconfig.refs.json']); + } catch (e) { + log.error(e); + process.exit(1); + } +} + +export async function runBuildRefs() { + run(async ({ log }) => { + await buildRefs(log); + }); +} diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index 065321e355256a..e18c82b5b9e96e 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -24,6 +24,7 @@ import { Project } from './project'; export const PROJECTS = [ new Project(resolve(REPO_ROOT, 'tsconfig.json')), + new Project(resolve(REPO_ROOT, 'src/test_utils/tsconfig.json')), new Project(resolve(REPO_ROOT, 'test/tsconfig.json'), { name: 'kibana/test' }), new Project(resolve(REPO_ROOT, 'x-pack/tsconfig.json')), new Project(resolve(REPO_ROOT, 'x-pack/test/tsconfig.json'), { name: 'x-pack/test' }), diff --git a/src/dev/typescript/run_type_check_cli.ts b/src/dev/typescript/run_type_check_cli.ts index 9eeaeb4da70424..e1fca23274a5aa 100644 --- a/src/dev/typescript/run_type_check_cli.ts +++ b/src/dev/typescript/run_type_check_cli.ts @@ -24,8 +24,9 @@ import getopts from 'getopts'; import { execInProjects } from './exec_in_projects'; import { filterProjectsByFlag } from './projects'; +import { buildRefs } from './build_refs'; -export function runTypeCheckCli() { +export async function runTypeCheckCli() { const extraFlags: string[] = []; const opts = getopts(process.argv.slice(2), { boolean: ['skip-lib-check', 'help'], @@ -79,7 +80,16 @@ export function runTypeCheckCli() { process.exit(); } - const tscArgs = ['--noEmit', '--pretty', ...(opts['skip-lib-check'] ? ['--skipLibCheck'] : [])]; + await buildRefs(log); + + const tscArgs = [ + // composite project cannot be used with --noEmit + ...['--composite', 'false'], + ...['--emitDeclarationOnly', 'false'], + '--noEmit', + '--pretty', + ...(opts['skip-lib-check'] ? ['--skipLibCheck'] : []), + ]; const projects = filterProjectsByFlag(opts.project).filter((p) => !p.disableTypeCheck); if (!projects.length) { diff --git a/src/legacy/server/http/integration_tests/max_payload_size.test.js b/src/legacy/server/http/integration_tests/max_payload_size.test.js index 789a54f681ba67..2d0718dd356064 100644 --- a/src/legacy/server/http/integration_tests/max_payload_size.test.js +++ b/src/legacy/server/http/integration_tests/max_payload_size.test.js @@ -17,7 +17,7 @@ * under the License. */ -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../../core/test_helpers/kbn_server'; let root; beforeAll(async () => { diff --git a/src/plugins/console/server/lib/spec_definitions/js/mappings.ts b/src/plugins/console/server/lib/spec_definitions/js/mappings.ts index d09637b05a3cb5..aa09278d075538 100644 --- a/src/plugins/console/server/lib/spec_definitions/js/mappings.ts +++ b/src/plugins/console/server/lib/spec_definitions/js/mappings.ts @@ -17,8 +17,6 @@ * under the License. */ -import _ from 'lodash'; - import { SpecDefinitionsService } from '../../../services'; import { BOOLEAN } from './shared'; @@ -159,28 +157,25 @@ export const mappings = (specService: SpecDefinitionsService) => { // dates format: { - __one_of: _.flatten([ - _.map( - [ - 'date', - 'date_time', - 'date_time_no_millis', - 'ordinal_date', - 'ordinal_date_time', - 'ordinal_date_time_no_millis', - 'time', - 'time_no_millis', - 't_time', - 't_time_no_millis', - 'week_date', - 'week_date_time', - 'week_date_time_no_millis', - ], - function (s) { - return ['basic_' + s, 'strict_' + s]; - } - ), - [ + __one_of: [ + ...[ + 'date', + 'date_time', + 'date_time_no_millis', + 'ordinal_date', + 'ordinal_date_time', + 'ordinal_date_time_no_millis', + 'time', + 'time_no_millis', + 't_time', + 't_time_no_millis', + 'week_date', + 'week_date_time', + 'week_date_time_no_millis', + ].map(function (s) { + return ['basic_' + s, 'strict_' + s]; + }), + ...[ 'date', 'date_hour', 'date_hour_minute', @@ -214,7 +209,7 @@ export const mappings = (specService: SpecDefinitionsService) => { 'epoch_millis', 'epoch_second', ], - ]), + ], }, fielddata: { diff --git a/src/test_utils/public/stub_field_formats.ts b/src/plugins/data/public/field_formats/field_formats_registry.stub.ts similarity index 81% rename from src/test_utils/public/stub_field_formats.ts rename to src/plugins/data/public/field_formats/field_formats_registry.stub.ts index 589e93fd600c2c..e8741ca41036ba 100644 --- a/src/test_utils/public/stub_field_formats.ts +++ b/src/plugins/data/public/field_formats/field_formats_registry.stub.ts @@ -16,10 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { CoreSetup } from 'kibana/public'; -import { DataPublicPluginStart, fieldFormats } from '../../plugins/data/public'; -import { deserializeFieldFormat } from '../../plugins/data/public/field_formats/utils/deserialize'; -import { baseFormattersPublic } from '../../plugins/data/public'; + +import { CoreSetup } from 'src/core/public'; +import { deserializeFieldFormat } from './utils/deserialize'; +import { baseFormattersPublic } from './constants'; +import { DataPublicPluginStart, fieldFormats } from '..'; export const getFieldFormatsRegistry = (core: CoreSetup) => { const fieldFormatsRegistry = new fieldFormats.FieldFormatsRegistry(); diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts index 5e0eeaee3c0d09..f1f5f8737b389b 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts @@ -17,7 +17,7 @@ * under the License. */ -import { setup } from 'test_utils/http_test_setup'; +import { setup } from '../../../../../core/test_helpers/http_test_setup'; export const { http } = setup((injectedMetadata) => { injectedMetadata.getBasePath.mockReturnValue('/hola/daro/'); diff --git a/src/plugins/data/public/test_utils.ts b/src/plugins/data/public/test_utils.ts new file mode 100644 index 00000000000000..f04b20e96fa1da --- /dev/null +++ b/src/plugins/data/public/test_utils.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 { getFieldFormatsRegistry } from './field_formats/field_formats_registry.stub'; diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table.test.js b/src/plugins/vis_type_table/public/agg_table/agg_table.test.js index 0362bd55963d9b..29a10151a9418b 100644 --- a/src/plugins/vis_type_table/public/agg_table/agg_table.test.js +++ b/src/plugins/vis_type_table/public/agg_table/agg_table.test.js @@ -24,7 +24,7 @@ import 'angular-mocks'; import sinon from 'sinon'; import { round } from 'lodash'; -import { getFieldFormatsRegistry } from '../../../../test_utils/public/stub_field_formats'; +import { getFieldFormatsRegistry } from '../../../data/public/test_utils'; import { coreMock } from '../../../../core/public/mocks'; import { initAngularBootstrap } from '../../../kibana_legacy/public'; import { setUiSettings } from '../../../data/public/services'; diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js b/src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js index 43913eed32f90a..04cf624331d819 100644 --- a/src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js +++ b/src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js @@ -22,7 +22,7 @@ import angular from 'angular'; import 'angular-mocks'; import expect from '@kbn/expect'; -import { getFieldFormatsRegistry } from '../../../../test_utils/public/stub_field_formats'; +import { getFieldFormatsRegistry } from '../../../data/public/test_utils'; import { coreMock } from '../../../../core/public/mocks'; import { initAngularBootstrap } from '../../../kibana_legacy/public'; import { setUiSettings } from '../../../data/public/services'; 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 308579126eeb1d..7aa7d53554d576 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 @@ -18,7 +18,7 @@ */ import { createTickFormatter } from './tick_formatter'; -import { getFieldFormatsRegistry } from '../../../../../../test_utils/public/stub_field_formats'; +import { getFieldFormatsRegistry } from '../../../../../data/public/test_utils'; import { setFieldFormats } from '../../../services'; import { UI_SETTINGS } from '../../../../../data/public'; diff --git a/src/test_utils/public/stub_index_pattern.js b/src/test_utils/public/stub_index_pattern.js index 3289610a062b0e..6130a7f9f9e877 100644 --- a/src/test_utils/public/stub_index_pattern.js +++ b/src/test_utils/public/stub_index_pattern.js @@ -22,6 +22,7 @@ import sinon from 'sinon'; // because it is one of the few places that we need to access the IndexPattern class itself, rather // than just the type. Doing this as a temporary measure; it will be left behind when migrating to NP. +import { getFieldFormatsRegistry } from '../../plugins/data/public/test_utils'; import { IndexPattern, indexPatterns, KBN_FIELD_TYPES, fieldList } from '../../plugins/data/public'; import { setFieldFormats } from '../../plugins/data/public/services'; @@ -33,8 +34,6 @@ setFieldFormats({ }), }); -import { getFieldFormatsRegistry } from './stub_field_formats'; - export default function StubIndexPattern(pattern, getConfig, timeField, fields, core) { const registeredFieldFormats = getFieldFormatsRegistry(core); diff --git a/src/test_utils/tsconfig.json b/src/test_utils/tsconfig.json new file mode 100644 index 00000000000000..2fa89e29898914 --- /dev/null +++ b/src/test_utils/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "composite": true, + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*" + ], + "exclude": [ + "target" + ] +} diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/tsconfig.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/tsconfig.json index 5fcaeafbb0d852..4d979fbf7f15fd 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/tsconfig.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/test/plugin_functional/plugins/app_link_test/tsconfig.json b/test/plugin_functional/plugins/app_link_test/tsconfig.json index 5fcaeafbb0d852..4d979fbf7f15fd 100644 --- a/test/plugin_functional/plugins/app_link_test/tsconfig.json +++ b/test/plugin_functional/plugins/app_link_test/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/test/plugin_functional/plugins/core_app_status/tsconfig.json b/test/plugin_functional/plugins/core_app_status/tsconfig.json index 5fcaeafbb0d852..4d979fbf7f15fd 100644 --- a/test/plugin_functional/plugins/core_app_status/tsconfig.json +++ b/test/plugin_functional/plugins/core_app_status/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/test/plugin_functional/plugins/core_plugin_a/tsconfig.json b/test/plugin_functional/plugins/core_plugin_a/tsconfig.json index 1ba21f11b7de2a..ccbffc34bce4a4 100644 --- a/test/plugin_functional/plugins/core_plugin_a/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_a/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/test/plugin_functional/plugins/core_plugin_appleave/tsconfig.json b/test/plugin_functional/plugins/core_plugin_appleave/tsconfig.json index 5fcaeafbb0d852..4d979fbf7f15fd 100644 --- a/test/plugin_functional/plugins/core_plugin_appleave/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_appleave/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/test/plugin_functional/plugins/core_plugin_b/tsconfig.json b/test/plugin_functional/plugins/core_plugin_b/tsconfig.json index 1ba21f11b7de2a..ccbffc34bce4a4 100644 --- a/test/plugin_functional/plugins/core_plugin_b/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_b/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json b/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json index 5fcaeafbb0d852..4d979fbf7f15fd 100644 --- a/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/test/plugin_functional/plugins/core_plugin_route_timeouts/tsconfig.json b/test/plugin_functional/plugins/core_plugin_route_timeouts/tsconfig.json index d0751f31ecc5ea..0e27246a49980d 100644 --- a/test/plugin_functional/plugins/core_plugin_route_timeouts/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_route_timeouts/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/test/plugin_functional/plugins/core_plugin_static_assets/tsconfig.json b/test/plugin_functional/plugins/core_plugin_static_assets/tsconfig.json index 4a564ee1e5578f..433b041b9af3f6 100644 --- a/test/plugin_functional/plugins/core_plugin_static_assets/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_static_assets/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json b/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json index baedb5f2f621bf..0aac2eb570987c 100644 --- a/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/test/plugin_functional/plugins/doc_views_plugin/tsconfig.json b/test/plugin_functional/plugins/doc_views_plugin/tsconfig.json index 4a564ee1e5578f..433b041b9af3f6 100644 --- a/test/plugin_functional/plugins/doc_views_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/doc_views_plugin/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json b/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json index d0751f31ecc5ea..0e27246a49980d 100644 --- a/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/test/plugin_functional/plugins/index_patterns/tsconfig.json b/test/plugin_functional/plugins/index_patterns/tsconfig.json index 6f0c32ad30601f..d5ccf7adddb2dc 100644 --- a/test/plugin_functional/plugins/index_patterns/tsconfig.json +++ b/test/plugin_functional/plugins/index_patterns/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/tsconfig.json b/test/plugin_functional/plugins/kbn_sample_panel_action/tsconfig.json index 5fcaeafbb0d852..4d979fbf7f15fd 100644 --- a/test/plugin_functional/plugins/kbn_sample_panel_action/tsconfig.json +++ b/test/plugin_functional/plugins/kbn_sample_panel_action/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/test/plugin_functional/plugins/kbn_top_nav/tsconfig.json b/test/plugin_functional/plugins/kbn_top_nav/tsconfig.json index 1ba21f11b7de2a..ccbffc34bce4a4 100644 --- a/test/plugin_functional/plugins/kbn_top_nav/tsconfig.json +++ b/test/plugin_functional/plugins/kbn_top_nav/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json index d8096d9aab27ab..7e2f7c97eb0586 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true, @@ -13,7 +13,7 @@ "index.ts", "public/**/*.ts", "public/**/*.tsx", - "../../../../typings/**/*", + "../../../../typings/**/*" ], "exclude": [] -} \ No newline at end of file +} diff --git a/test/plugin_functional/plugins/management_test_plugin/tsconfig.json b/test/plugin_functional/plugins/management_test_plugin/tsconfig.json index 5fcaeafbb0d852..4d979fbf7f15fd 100644 --- a/test/plugin_functional/plugins/management_test_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/management_test_plugin/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/test/plugin_functional/plugins/rendering_plugin/tsconfig.json b/test/plugin_functional/plugins/rendering_plugin/tsconfig.json index 1ba21f11b7de2a..ccbffc34bce4a4 100644 --- a/test/plugin_functional/plugins/rendering_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/rendering_plugin/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json b/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json index 7c170405bbfc7f..33b2d6c0f64ad8 100644 --- a/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/test/tsconfig.json b/test/tsconfig.json index 87e79b295315f0..325db489780226 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,19 +1,15 @@ { - "extends": "../tsconfig.json", + "extends": "../tsconfig.base.json", "compilerOptions": { + "tsBuildInfoFile": "../build/tsbuildinfo/test", "types": [ "node", "mocha", "flot" - ], - "lib": [ - "esnext", - "dom" ] }, "include": [ - "**/*.ts", - "**/*.tsx", + "**/*", "../typings/elastic__node_crypto.d.ts", "typings/**/*" ], diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 00000000000000..58a6237846a547 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,60 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + // Allows for importing from `kibana` package for the exported types. + "kibana": ["./kibana"], + "kibana/public": ["src/core/public"], + "kibana/server": ["src/core/server"], + "plugins/*": ["src/legacy/core_plugins/*/public/"], + "test_utils/*": [ + "src/test_utils/public/*" + ], + "fixtures/*": ["src/fixtures/*"] + }, + // Support .tsx files and transform JSX into calls to React.createElement + "jsx": "react", + // Enables all strict type checking options. + "strict": true, + // save information about the project graph on disk + "incremental": true, + // enables "core language features" + "lib": [ + "esnext", + // includes support for browser APIs + "dom" + ], + // Node 8 should support everything output by esnext, we override this + // in webpack with loader-level compiler options + "target": "esnext", + // Use commonjs for node, overridden in webpack to keep import statements + // to maintain support for things like `await import()` + "module": "commonjs", + // Allows default imports from modules with no default export. This does not affect code emit, just type checking. + // We have to enable this option explicitly since `esModuleInterop` doesn't enable it automatically when ES2015 or + // ESNext module format is used. + "allowSyntheticDefaultImports": true, + // Emits __importStar and __importDefault helpers for runtime babel ecosystem compatibility. + "esModuleInterop": true, + // Resolve modules in the same way as Node.js. Aka make `require` works the + // same in TypeScript as it does in Node.js. + "moduleResolution": "node", + // Disallow inconsistently-cased references to the same file. + "forceConsistentCasingInFileNames": true, + // Forbid unused local variables as the rule was deprecated by ts-lint + "noUnusedLocals": true, + // Provide full support for iterables in for..of, spread and destructuring when targeting ES5 or ES3. + "downlevelIteration": true, + // import tslib helpers rather than inlining helpers for iteration or spreading, for instance + "importHelpers": true, + // adding global typings + "types": [ + "node", + "jest", + "react", + "flot", + "jest-styled-components", + "@testing-library/jest-dom" + ] + } +} diff --git a/tsconfig.browser.json b/tsconfig.browser.json index fdfc868157e0d7..b0886bf3e5e289 100644 --- a/tsconfig.browser.json +++ b/tsconfig.browser.json @@ -1,5 +1,5 @@ { - "extends": "./tsconfig.json", + "extends": "./tsconfig.base.json", "compilerOptions": { "target": "es5", "module": "esnext", diff --git a/tsconfig.json b/tsconfig.json index 819cb5dc4c1e3c..21a817213c62f4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,59 +1,7 @@ { + "extends": "./tsconfig.base.json", "compilerOptions": { - "baseUrl": ".", - "paths": { - // Allows for importing from `kibana` package for the exported types. - "kibana": ["./kibana"], - "kibana/public": ["src/core/public"], - "kibana/server": ["src/core/server"], - "plugins/*": ["src/legacy/core_plugins/*/public/"], - "test_utils/*": [ - "src/test_utils/public/*" - ], - "fixtures/*": ["src/fixtures/*"] - }, - // Support .tsx files and transform JSX into calls to React.createElement - "jsx": "react", - // Enables all strict type checking options. - "strict": true, - // enables "core language features" - "lib": [ - "esnext", - // includes support for browser APIs - "dom" - ], - // Node 8 should support everything output by esnext, we override this - // in webpack with loader-level compiler options - "target": "esnext", - // Use commonjs for node, overridden in webpack to keep import statements - // to maintain support for things like `await import()` - "module": "commonjs", - // Allows default imports from modules with no default export. This does not affect code emit, just type checking. - // We have to enable this option explicitly since `esModuleInterop` doesn't enable it automatically when ES2015 or - // ESNext module format is used. - "allowSyntheticDefaultImports": true, - // Emits __importStar and __importDefault helpers for runtime babel ecosystem compatibility. - "esModuleInterop": true, - // Resolve modules in the same way as Node.js. Aka make `require` works the - // same in TypeScript as it does in Node.js. - "moduleResolution": "node", - // Disallow inconsistently-cased references to the same file. - "forceConsistentCasingInFileNames": true, - // Forbid unused local variables as the rule was deprecated by ts-lint - "noUnusedLocals": true, - // Provide full support for iterables in for..of, spread and destructuring when targeting ES5 or ES3. - "downlevelIteration": true, - // import tslib helpers rather than inlining helpers for iteration or spreading, for instance - "importHelpers": true, - // adding global typings - "types": [ - "node", - "jest", - "react", - "flot", - "jest-styled-components", - "@testing-library/jest-dom" - ] + "tsBuildInfoFile": "./build/tsbuildinfo/kibana", }, "include": [ "kibana.d.ts", @@ -62,11 +10,15 @@ "test_utils/**/*" ], "exclude": [ - "src/**/__fixtures__/**/*" + "src/**/__fixtures__/**/*", + "src/test_utils/**/*" // In the build we actually exclude **/public/**/* from this config so that // we can run the TSC on both this and the .browser version of this config // file, but if we did it during development IDEs would not be able to find // the tsconfig.json file for public files correctly. // "src/**/public/**/*" + ], + "references": [ + { "path": "./src/test_utils/tsconfig.json" } ] } diff --git a/tsconfig.refs.json b/tsconfig.refs.json new file mode 100644 index 00000000000000..0e4cc1bd32da30 --- /dev/null +++ b/tsconfig.refs.json @@ -0,0 +1,6 @@ +{ + "include": [], + "references": [ + { "path": "./src/test_utils" } + ] +} diff --git a/tsconfig.types.json b/tsconfig.types.json index 2f5919e413e514..e8cd0a5209bbeb 100644 --- a/tsconfig.types.json +++ b/tsconfig.types.json @@ -1,8 +1,8 @@ { - "extends": "./tsconfig", + "extends": "./tsconfig.base.json", "compilerOptions": { "declaration": true, - "declarationDir": "./target/types", + "outDir": "./target/types", "stripInternal": false, "emitDeclarationOnly": true, "declarationMap": true diff --git a/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json b/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json index d508076b331990..cae75bb81c7232 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json +++ b/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true @@ -9,7 +9,7 @@ "public/**/*.ts", "public/**/*.tsx", "server/**/*.ts", - "../../typings/**/*", + "../../typings/**/*" ], "exclude": [] } diff --git a/x-pack/plugins/apm/e2e/tsconfig.json b/x-pack/plugins/apm/e2e/tsconfig.json index a7091a20186b2c..c4587349c7ad77 100644 --- a/x-pack/plugins/apm/e2e/tsconfig.json +++ b/x-pack/plugins/apm/e2e/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "exclude": ["tmp"], "include": ["./**/*"], "compilerOptions": { diff --git a/x-pack/plugins/apm/scripts/tsconfig.json b/x-pack/plugins/apm/scripts/tsconfig.json index 350db55e72446a..64602bc6b27699 100644 --- a/x-pack/plugins/apm/scripts/tsconfig.json +++ b/x-pack/plugins/apm/scripts/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "include": [ "./**/*" ], diff --git a/x-pack/plugins/canvas/storybook/addon/tsconfig.json b/x-pack/plugins/canvas/storybook/addon/tsconfig.json index 9cab0af235f2e5..2ab1856de661a1 100644 --- a/x-pack/plugins/canvas/storybook/addon/tsconfig.json +++ b/x-pack/plugins/canvas/storybook/addon/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../../tsconfig.base.json", "include": [ "src/**/*.ts", "src/**/*.tsx" diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json b/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json index e3108b8c759f4d..6d01a853698b8e 100644 --- a/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.json", + "extends": "../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "target": "es6", diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json deleted file mode 100644 index 4082f16a5d91ce..00000000000000 --- a/x-pack/plugins/ml/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../tsconfig.json" -} diff --git a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.test.ts b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.test.ts index 40cbd00858b5f0..0a795ad7678985 100644 --- a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.test.ts +++ b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.test.ts @@ -7,7 +7,7 @@ // @ts-ignore import fetchMock from 'fetch-mock/es5/client'; import { SessionTimeoutHttpInterceptor } from './session_timeout_http_interceptor'; -import { setup } from '../../../../../src/test_utils/public/http_test_setup'; +import { setup } from '../../../../../src/core/test_helpers/http_test_setup'; import { createSessionTimeoutMock } from './session_timeout.mock'; const mockCurrentUrl = (url: string) => window.history.pushState({}, '', url); diff --git a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts index 78c82cbc3a9a66..71ef6496ef6cad 100644 --- a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts +++ b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts @@ -7,7 +7,7 @@ // @ts-ignore import fetchMock from 'fetch-mock/es5/client'; import { SessionExpired } from './session_expired'; -import { setup } from '../../../../../src/test_utils/public/http_test_setup'; +import { setup } from '../../../../../src/core/test_helpers/http_test_setup'; import { UnauthorizedResponseHttpInterceptor } from './unauthorized_response_http_interceptor'; jest.mock('./session_expired'); diff --git a/x-pack/plugins/security_solution/cypress/tsconfig.json b/x-pack/plugins/security_solution/cypress/tsconfig.json index 929a3fb39babb0..d0669ccb782818 100644 --- a/x-pack/plugins/security_solution/cypress/tsconfig.json +++ b/x-pack/plugins/security_solution/cypress/tsconfig.json @@ -1,10 +1,11 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "exclude": [], "include": [ "./**/*" ], "compilerOptions": { + "tsBuildInfoFile": "../../../../build/tsbuildinfo/security_solution/cypress", "types": [ "cypress", "node" diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index 8375296d869e6d..dabdcf553edb4a 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -20,7 +20,7 @@ import { loggingSystemMock, coreMock, } from '../../../../../../src/core/server/mocks'; -import * as kbnTestServer from '../../../../../../src/test_utils/kbn_server'; +import * as kbnTestServer from '../../../../../../src/core/test_helpers/kbn_server'; import { SpacesService } from '../../spaces_service'; import { SpacesAuditLogger } from '../audit_logger'; import { convertSavedObjectToSpace } from '../../routes/lib'; diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts index 1558c6425f542d..998c6ca18983d7 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts @@ -15,7 +15,7 @@ import { IRouter, } from '../../../../../../src/core/server'; -import * as kbnTestServer from '../../../../../../src/test_utils/kbn_server'; +import * as kbnTestServer from '../../../../../../src/core/test_helpers/kbn_server'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; // FAILING: https://github.com/elastic/kibana/issues/58942 diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index df1d7e789507b3..d0e2ea952e1089 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -1,6 +1,7 @@ { - "extends": "../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/x-pack/test", "types": [ "mocha", "node", diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 7c6210bb9ce191..b5080f83f2edad 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../tsconfig.json", + "extends": "../tsconfig.base.json", "include": [ "mocks.ts", "typings/**/*", @@ -33,12 +33,7 @@ "plugins/*": ["src/legacy/core_plugins/*/public/"], "fixtures/*": ["src/fixtures/*"], }, - "types": [ - "node", - "jest", - "flot", - "jest-styled-components", - "@testing-library/jest-dom" - ] + // overhead is too significant + "incremental": false, } } From 299c3f5a54bd75d73f4a3fa65b826d3aeb6a9da6 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 3 Sep 2020 09:10:32 -0400 Subject: [PATCH 215/216] [Security Solution] add an excess validation instead of the exact match (#76472) * add an excess validation instead of the exact match * fix readble type + unit test * review I * remove buildRouteValidation to use buildRouteValidationWithExcess --- .../routes/__mocks__/request_responses.ts | 16 +- .../routes/clean_draft_timelines_route.ts | 4 +- .../timeline/routes/create_timelines_route.ts | 4 +- .../routes/export_timelines_route.test.ts | 4 +- .../timeline/routes/export_timelines_route.ts | 6 +- .../routes/get_draft_timelines_route.ts | 4 +- .../lib/timeline/routes/get_timeline_route.ts | 4 +- .../routes/import_timelines_route.test.ts | 102 ++++---- .../timeline/routes/import_timelines_route.ts | 7 +- .../routes/schemas/create_timelines_schema.ts | 10 +- .../routes/schemas/export_timelines_schema.ts | 8 +- .../schemas/get_timeline_by_id_schema.ts | 11 +- .../routes/schemas/import_timelines_schema.ts | 31 ++- .../timeline/routes/update_timelines_route.ts | 4 +- .../build_validation/route_validation.test.ts | 245 +++++++++++++----- .../build_validation/route_validation.ts | 18 ++ .../server/utils/runtime_types.ts | 130 ++++++++++ 17 files changed, 442 insertions(+), 166 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/utils/runtime_types.ts diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts index c5d69398b7f0c7..026ec1fa847f99 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts @@ -3,8 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import path, { join, resolve } from 'path'; import * as rt from 'io-ts'; -import stream from 'stream'; import { TIMELINE_DRAFT_URL, @@ -20,8 +21,8 @@ import { requestMock } from '../../../detection_engine/routes/__mocks__'; import { updateTimelineSchema } from '../schemas/update_timelines_schema'; import { createTimelineSchema } from '../schemas/create_timelines_schema'; import { GetTimelineByIdSchemaQuery } from '../schemas/get_timeline_by_id_schema'; +import { getReadables } from '../utils/common'; -const readable = new stream.Readable(); export const getExportTimelinesRequest = () => requestMock.create({ method: 'get', @@ -34,15 +35,20 @@ export const getExportTimelinesRequest = () => }, }); -export const getImportTimelinesRequest = (filename?: string) => - requestMock.create({ +export const getImportTimelinesRequest = async (fileName?: string) => { + const dir = resolve(join(__dirname, '../../../detection_engine/rules/prepackaged_timelines')); + const file = fileName ?? 'index.ndjson'; + const dataPath = path.join(dir, file); + const readable = await getReadables(dataPath); + return requestMock.create({ method: 'post', path: TIMELINE_IMPORT_URL, query: { overwrite: false }, body: { - file: { ...readable, hapi: { filename: filename ?? 'filename.ndjson' } }, + file: { ...readable, hapi: { filename: file } }, }, }); +}; export const inputTimeline: SavedTimeline = { columns: [ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/clean_draft_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/clean_draft_timelines_route.ts index 8cabd84a965b75..67fc3167a4a29f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/clean_draft_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/clean_draft_timelines_route.ts @@ -11,7 +11,7 @@ import { transformError, buildSiemResponse } from '../../detection_engine/routes import { TIMELINE_DRAFT_URL } from '../../../../common/constants'; import { buildFrameworkRequest } from './utils/common'; import { SetupPlugins } from '../../../plugin'; -import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; +import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; import { getDraftTimeline, resetTimeline, getTimeline, persistTimeline } from '../saved_object'; import { draftTimelineDefaults } from '../default_timeline'; import { cleanDraftTimelineSchema } from './schemas/clean_draft_timelines_schema'; @@ -26,7 +26,7 @@ export const cleanDraftTimelinesRoute = ( { path: TIMELINE_DRAFT_URL, validate: { - body: buildRouteValidation(cleanDraftTimelineSchema), + body: buildRouteValidationWithExcess(cleanDraftTimelineSchema), }, options: { tags: ['access:securitySolution'], diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts index 7abcb390d0221c..77cd49406baa16 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts @@ -9,7 +9,7 @@ import { TIMELINE_URL } from '../../../../common/constants'; import { ConfigType } from '../../..'; import { SetupPlugins } from '../../../plugin'; -import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; +import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; @@ -31,7 +31,7 @@ export const createTimelinesRoute = ( { path: TIMELINE_URL, validate: { - body: buildRouteValidation(createTimelineSchema), + body: buildRouteValidationWithExcess(createTimelineSchema), }, options: { tags: ['access:securitySolution'], diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts index 5a976ee7521af4..5d7cb1c8d3f6cf 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts @@ -96,7 +96,7 @@ describe('export timelines', () => { const result = server.validate(request); expect(result.badRequest.mock.calls[0][0]).toEqual( - 'Invalid value "undefined" supplied to "file_name"' + 'Invalid value {"id":"someId"}, excess properties: ["id"]' ); }); @@ -110,7 +110,7 @@ describe('export timelines', () => { const result = server.validate(request); expect(result.badRequest.mock.calls[0][0]).toEqual( - 'Invalid value "someId" supplied to "ids",Invalid value "{"ids":"someId"}" supplied to "(Partial<{ ids: (Array | null) }> | null)"' + 'Invalid value "someId" supplied to "ids"' ); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.ts index 89e38753ac9263..38ee51fb7aa0c8 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.ts @@ -14,7 +14,7 @@ import { exportTimelinesQuerySchema, exportTimelinesRequestBodySchema, } from './schemas/export_timelines_schema'; -import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; +import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; import { buildFrameworkRequest } from './utils/common'; import { SetupPlugins } from '../../../plugin'; @@ -27,8 +27,8 @@ export const exportTimelinesRoute = ( { path: TIMELINE_EXPORT_URL, validate: { - query: buildRouteValidation(exportTimelinesQuerySchema), - body: buildRouteValidation(exportTimelinesRequestBodySchema), + query: buildRouteValidationWithExcess(exportTimelinesQuerySchema), + body: buildRouteValidationWithExcess(exportTimelinesRequestBodySchema), }, options: { tags: ['access:securitySolution'], diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_draft_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_draft_timelines_route.ts index 4db434ec816aa5..43129f0e15f0e6 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_draft_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_draft_timelines_route.ts @@ -10,7 +10,7 @@ import { transformError, buildSiemResponse } from '../../detection_engine/routes import { TIMELINE_DRAFT_URL } from '../../../../common/constants'; import { buildFrameworkRequest } from './utils/common'; import { SetupPlugins } from '../../../plugin'; -import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; +import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; import { getDraftTimeline, persistTimeline } from '../saved_object'; import { draftTimelineDefaults } from '../default_timeline'; import { getDraftTimelineSchema } from './schemas/get_draft_timelines_schema'; @@ -24,7 +24,7 @@ export const getDraftTimelinesRoute = ( { path: TIMELINE_DRAFT_URL, validate: { - query: buildRouteValidation(getDraftTimelineSchema), + query: buildRouteValidationWithExcess(getDraftTimelineSchema), }, options: { tags: ['access:securitySolution'], diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts index f36adb648cc036..e46a644d6820e5 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts @@ -10,7 +10,7 @@ import { TIMELINE_URL } from '../../../../common/constants'; import { ConfigType } from '../../..'; import { SetupPlugins } from '../../../plugin'; -import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; +import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; import { buildSiemResponse, transformError } from '../../detection_engine/routes/utils'; @@ -28,7 +28,7 @@ export const getTimelineRoute = ( router.get( { path: `${TIMELINE_URL}`, - validate: { query: buildRouteValidation(getTimelineByIdSchemaQuery) }, + validate: { query: buildRouteValidationWithExcess(getTimelineByIdSchemaQuery) }, options: { tags: ['access:securitySolution'], }, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts index 388ab5db3e852b..13cc71840ec962 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts @@ -155,31 +155,31 @@ describe('import timelines', () => { }); test('should use given timelineId to check if the timeline savedObject already exist', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockGetTimeline.mock.calls[0][1]).toEqual(mockUniqueParsedObjects[0].savedObjectId); }); test('should Create a new timeline savedObject', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline).toHaveBeenCalled(); }); test('should Create a new timeline savedObject without timelineId', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][1]).toBeNull(); }); test('should Create a new timeline savedObject without timeline version', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][2]).toBeNull(); }); test('should Create a new timeline savedObject with given timeline', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][3]).toEqual({ ...mockParsedTimelineObject, @@ -199,7 +199,7 @@ describe('import timelines', () => { }, ], ]); - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); const response = await server.inject(mockRequest, context); expect(response.body).toEqual({ success: false, @@ -219,19 +219,19 @@ describe('import timelines', () => { }); test('should Create new pinned events', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistPinnedEventOnTimeline).toHaveBeenCalled(); }); test('should Create a new pinned event without pinnedEventSavedObjectId', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistPinnedEventOnTimeline.mock.calls[0][1]).toBeNull(); }); test('should Create a new pinned event with pinnedEventId', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistPinnedEventOnTimeline.mock.calls[0][2]).toEqual( mockUniqueParsedObjects[0].pinnedEventIds[0] @@ -239,7 +239,7 @@ describe('import timelines', () => { }); test('should Create a new pinned event with new timelineSavedObjectId', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistPinnedEventOnTimeline.mock.calls[0][3]).toEqual( mockCreatedTimeline.savedObjectId @@ -247,7 +247,7 @@ describe('import timelines', () => { }); test('should Check if note exists', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockGetNote.mock.calls[0][1]).toEqual( mockUniqueParsedObjects[0].globalNotes[0].noteId @@ -255,31 +255,31 @@ describe('import timelines', () => { }); test('should Create notes', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote).toHaveBeenCalled(); }); test('should provide no noteSavedObjectId when Creating notes for a timeline', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][1]).toBeNull(); }); test('should provide new timeline version when Creating notes for a timeline', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][1]).toBeNull(); }); test('should provide note content when Creating notes for a timeline', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][2]).toEqual(mockCreatedTimeline.version); }); test('should provide new notes with original author info when Creating notes for a timeline', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][3]).toEqual({ eventId: undefined, @@ -314,7 +314,7 @@ describe('import timelines', () => { mockGetNote.mockReset(); mockGetNote.mockRejectedValue(new Error()); - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][3]).toEqual({ created: mockUniqueParsedObjects[0].globalNotes[0].created, @@ -346,7 +346,8 @@ describe('import timelines', () => { }); test('returns 200 when import timeline successfully', async () => { - const response = await server.inject(getImportTimelinesRequest(), context); + const mockRequest = await getImportTimelinesRequest(); + const response = await server.inject(mockRequest, context); expect(response.status).toEqual(200); }); }); @@ -379,7 +380,8 @@ describe('import timelines', () => { }); test('returns error message', async () => { - const response = await server.inject(getImportTimelinesRequest(), context); + const mockRequest = await getImportTimelinesRequest(); + const response = await server.inject(mockRequest, context); expect(response.body).toEqual({ success: false, success_count: 0, @@ -407,7 +409,7 @@ describe('import timelines', () => { }, ], ]); - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); const response = await server.inject(mockRequest, context); expect(response.body).toEqual({ success: false, @@ -436,7 +438,7 @@ describe('import timelines', () => { }, ], ]); - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); const response = await server.inject(mockRequest, context); expect(response.body).toEqual({ success: false, @@ -494,7 +496,7 @@ describe('import timelines', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - 'Invalid value "undefined" supplied to "file"' + 'Invalid value {"id":"someId"}, excess properties: ["id"]' ); }); }); @@ -592,7 +594,7 @@ describe('import timeline templates', () => { }); test('should use given timelineId to check if the timeline savedObject already exist', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockGetTimeline.mock.calls[0][1]).toEqual( mockUniqueParsedTemplateTimelineObjects[0].savedObjectId @@ -600,7 +602,7 @@ describe('import timeline templates', () => { }); test('should use given templateTimelineId to check if the timeline savedObject already exist', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockGetTemplateTimeline.mock.calls[0][1]).toEqual( mockUniqueParsedTemplateTimelineObjects[0].templateTimelineId @@ -608,25 +610,25 @@ describe('import timeline templates', () => { }); test('should Create a new timeline savedObject', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline).toHaveBeenCalled(); }); test('should Create a new timeline savedObject without timelineId', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][1]).toBeNull(); }); test('should Create a new timeline savedObject without timeline version', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][2]).toBeNull(); }); test('should Create a new timeline savedObject witn given timeline and skip the omitted fields', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][3]).toEqual({ ...mockParsedTemplateTimelineObject, @@ -635,25 +637,25 @@ describe('import timeline templates', () => { }); test('should NOT Create new pinned events', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistPinnedEventOnTimeline).not.toHaveBeenCalled(); }); test('should provide no noteSavedObjectId when Creating notes for a timeline', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][1]).toBeNull(); }); test('should provide new timeline version when Creating notes for a timeline', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][2]).toEqual(mockCreatedTemplateTimeline.version); }); test('should exclude event notes when creating notes', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][3]).toEqual({ eventId: undefined, @@ -667,7 +669,8 @@ describe('import timeline templates', () => { }); test('returns 200 when import timeline successfully', async () => { - const response = await server.inject(getImportTimelinesRequest(), context); + const mockRequest = await getImportTimelinesRequest(); + const response = await server.inject(mockRequest, context); expect(response.status).toEqual(200); }); @@ -681,7 +684,7 @@ describe('import timeline templates', () => { }, ], ]); - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][3].templateTimelineId).toEqual( mockNewTemplateTimelineId @@ -699,7 +702,7 @@ describe('import timeline templates', () => { }, ], ]); - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); const result = await server.inject(mockRequest, context); expect(result.body).toEqual({ errors: [], @@ -743,7 +746,7 @@ describe('import timeline templates', () => { }); test('should use given timelineId to check if the timeline savedObject already exist', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockGetTimeline.mock.calls[0][1]).toEqual( mockUniqueParsedTemplateTimelineObjects[0].savedObjectId @@ -751,7 +754,7 @@ describe('import timeline templates', () => { }); test('should use given templateTimelineId to check if the timeline savedObject already exist', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockGetTemplateTimeline.mock.calls[0][1]).toEqual( mockUniqueParsedTemplateTimelineObjects[0].templateTimelineId @@ -759,13 +762,13 @@ describe('import timeline templates', () => { }); test('should UPDATE timeline savedObject', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline).toHaveBeenCalled(); }); test('should UPDATE timeline savedObject with timelineId', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][1]).toEqual( mockUniqueParsedTemplateTimelineObjects[0].savedObjectId @@ -773,7 +776,7 @@ describe('import timeline templates', () => { }); test('should UPDATE timeline savedObject without timeline version', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][2]).toEqual( mockUniqueParsedTemplateTimelineObjects[0].version @@ -781,31 +784,31 @@ describe('import timeline templates', () => { }); test('should UPDATE a new timeline savedObject witn given timeline and skip the omitted fields', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][3]).toEqual(mockParsedTemplateTimelineObject); }); test('should NOT Create new pinned events', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistPinnedEventOnTimeline).not.toHaveBeenCalled(); }); test('should provide noteSavedObjectId when Creating notes for a timeline', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][1]).toBeNull(); }); test('should provide new timeline version when Creating notes for a timeline', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][2]).toEqual(mockCreatedTemplateTimeline.version); }); test('should exclude event notes when creating notes', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][3]).toEqual({ eventId: undefined, @@ -819,7 +822,8 @@ describe('import timeline templates', () => { }); test('returns 200 when import timeline successfully', async () => { - const response = await server.inject(getImportTimelinesRequest(), context); + const mockRequest = await getImportTimelinesRequest(); + const response = await server.inject(mockRequest, context); expect(response.status).toEqual(200); }); @@ -833,7 +837,7 @@ describe('import timeline templates', () => { }, ], ]); - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); const response = await server.inject(mockRequest, context); expect(response.body).toEqual({ success: false, @@ -862,7 +866,7 @@ describe('import timeline templates', () => { }, ], ]); - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); const response = await server.inject(mockRequest, context); expect(response.body).toEqual({ success: false, @@ -920,7 +924,7 @@ describe('import timeline templates', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - 'Invalid value "undefined" supplied to "file"' + 'Invalid value {"id":"someId"}, excess properties: ["id"]' ); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts index c93983e499fb54..811d4531b86a71 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts @@ -5,6 +5,7 @@ */ import { extname } from 'path'; +import { Readable } from 'stream'; import { IRouter } from '../../../../../../../src/core/server'; @@ -12,7 +13,7 @@ import { TIMELINE_IMPORT_URL } from '../../../../common/constants'; import { SetupPlugins } from '../../../plugin'; import { ConfigType } from '../../../config'; -import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; +import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; import { buildSiemResponse, transformError } from '../../detection_engine/routes/utils'; import { importTimelines } from './utils/import_timelines'; @@ -28,7 +29,7 @@ export const importTimelinesRoute = ( { path: `${TIMELINE_IMPORT_URL}`, validate: { - body: buildRouteValidation(ImportTimelinesPayloadSchemaRt), + body: buildRouteValidationWithExcess(ImportTimelinesPayloadSchemaRt), }, options: { tags: ['access:securitySolution'], @@ -60,7 +61,7 @@ export const importTimelinesRoute = ( const frameworkRequest = await buildFrameworkRequest(context, security, request); const res = await importTimelines( - file, + (file as unknown) as Readable, config.maxTimelineImportExportSize, frameworkRequest, isImmutable === 'true' diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/create_timelines_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/create_timelines_schema.ts index 241d266a14c78f..8d542201f61086 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/create_timelines_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/create_timelines_schema.ts @@ -5,7 +5,11 @@ */ import * as rt from 'io-ts'; -import { SavedTimelineRuntimeType } from '../../../../../common/types/timeline'; +import { + SavedTimelineRuntimeType, + TimelineStatusLiteralRt, + TimelineTypeLiteralRt, +} from '../../../../../common/types/timeline'; import { unionWithNullType } from '../../../../../common/utility_types'; export const createTimelineSchema = rt.intersection([ @@ -13,7 +17,11 @@ export const createTimelineSchema = rt.intersection([ timeline: SavedTimelineRuntimeType, }), rt.partial({ + status: unionWithNullType(TimelineStatusLiteralRt), timelineId: unionWithNullType(rt.string), + templateTimelineId: unionWithNullType(rt.string), + templateTimelineVersion: unionWithNullType(rt.number), + timelineType: unionWithNullType(TimelineTypeLiteralRt), version: unionWithNullType(rt.string), }), ]); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts index ce8eb93bdbdbd4..4599d2bb571a2e 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts @@ -11,8 +11,6 @@ export const exportTimelinesQuerySchema = rt.type({ file_name: rt.string, }); -export const exportTimelinesRequestBodySchema = unionWithNullType( - rt.partial({ - ids: unionWithNullType(rt.array(rt.string)), - }) -); +export const exportTimelinesRequestBodySchema = rt.partial({ + ids: unionWithNullType(rt.array(rt.string)), +}); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts index 65c956ed604400..2c6098bc75500f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts @@ -4,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import * as rt from 'io-ts'; -import { unionWithNullType } from '../../../../../common/utility_types'; -export const getTimelineByIdSchemaQuery = unionWithNullType( - rt.partial({ - template_timeline_id: rt.string, - id: rt.string, - }) -); +export const getTimelineByIdSchemaQuery = rt.partial({ + template_timeline_id: rt.string, + id: rt.string, +}); export type GetTimelineByIdSchemaQuery = rt.TypeOf; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/import_timelines_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/import_timelines_schema.ts index afce9d6cdcb24e..89f3f9ddec1fc1 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/import_timelines_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/import_timelines_schema.ts @@ -5,9 +5,6 @@ */ import * as rt from 'io-ts'; -import { Readable } from 'stream'; -import { either } from 'fp-ts/lib/Either'; - import { SavedTimelineRuntimeType } from '../../../../../common/types/timeline'; import { eventNotes, globalNotes, pinnedEventIds } from './schemas'; @@ -28,16 +25,17 @@ export const ImportTimelinesSchemaRt = rt.intersection([ export type ImportTimelinesSchema = rt.TypeOf; -const ReadableRt = new rt.Type( - 'ReadableRt', - (u): u is Readable => u instanceof Readable, - (u, c) => - either.chain(rt.object.validate(u, c), (s) => { - const d = s as Readable; - return d.readable ? rt.success(d) : rt.failure(u, c); - }), - (a) => a -); +const ReadableRt = rt.partial({ + _maxListeners: rt.unknown, + _readableState: rt.unknown, + _read: rt.unknown, + readable: rt.boolean, + _events: rt.unknown, + _eventsCount: rt.number, + _data: rt.unknown, + _position: rt.number, + _encoding: rt.string, +}); const booleanInString = rt.union([rt.literal('true'), rt.literal('false')]); @@ -46,9 +44,14 @@ export const ImportTimelinesPayloadSchemaRt = rt.intersection([ file: rt.intersection([ ReadableRt, rt.type({ - hapi: rt.type({ filename: rt.string }), + hapi: rt.type({ + filename: rt.string, + headers: rt.unknown, + }), }), ]), }), rt.partial({ isImmutable: booleanInString }), ]); + +export type ImportTimelinesPayloadSchema = rt.TypeOf; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts index 07ce9a7336d4d7..6b8ceea80c31a1 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts @@ -9,7 +9,7 @@ import { IRouter } from '../../../../../../../src/core/server'; import { TIMELINE_URL } from '../../../../common/constants'; import { SetupPlugins } from '../../../plugin'; -import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; +import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; import { ConfigType } from '../../..'; import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; @@ -28,7 +28,7 @@ export const updateTimelinesRoute = ( { path: TIMELINE_URL, validate: { - body: buildRouteValidation(updateTimelineSchema), + body: buildRouteValidationWithExcess(updateTimelineSchema), }, options: { tags: ['access:securitySolution'], diff --git a/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.test.ts b/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.test.ts index 9559e442e2159d..ffc12d2bce2612 100644 --- a/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.test.ts +++ b/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.test.ts @@ -3,84 +3,195 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { buildRouteValidation } from './route_validation'; import * as rt from 'io-ts'; import { RouteValidationResultFactory } from 'src/core/server'; -describe('buildRouteValidation', () => { - const schema = rt.exact( - rt.type({ - ids: rt.array(rt.string), - }) - ); - type Schema = rt.TypeOf; - - /** - * If your schema is using exact all the way down then the validation will - * catch any additional keys that should not be present within the validation - * when the route_validation uses the exact check. - */ - const deepSchema = rt.exact( - rt.type({ - topLevel: rt.exact( - rt.type({ - secondLevel: rt.exact( - rt.type({ - thirdLevel: rt.string, - }) - ), - }) - ), - }) - ); - type DeepSchema = rt.TypeOf; - - const validationResult: RouteValidationResultFactory = { - ok: jest.fn().mockImplementation((validatedInput) => validatedInput), - badRequest: jest.fn().mockImplementation((e) => e), - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); +import { buildRouteValidation, buildRouteValidationWithExcess } from './route_validation'; - test('return validation error', () => { - const input: Omit & { id: string } = { id: 'someId' }; - const result = buildRouteValidation(schema)(input, validationResult); +describe('Route Validation with ', () => { + describe('buildRouteValidation', () => { + const schema = rt.exact( + rt.type({ + ids: rt.array(rt.string), + }) + ); + type Schema = rt.TypeOf; - expect(result).toEqual('Invalid value "undefined" supplied to "ids"'); - }); + /** + * If your schema is using exact all the way down then the validation will + * catch any additional keys that should not be present within the validation + * when the route_validation uses the exact check. + */ + const deepSchema = rt.exact( + rt.type({ + topLevel: rt.exact( + rt.type({ + secondLevel: rt.exact( + rt.type({ + thirdLevel: rt.string, + }) + ), + }) + ), + }) + ); + type DeepSchema = rt.TypeOf; - test('return validated input', () => { - const input: Schema = { ids: ['someId'] }; - const result = buildRouteValidation(schema)(input, validationResult); + const validationResult: RouteValidationResultFactory = { + ok: jest.fn().mockImplementation((validatedInput) => validatedInput), + badRequest: jest.fn().mockImplementation((e) => e), + }; - expect(result).toEqual(input); - }); + beforeEach(() => { + jest.clearAllMocks(); + }); - test('returns validation error if given extra keys on input for an array', () => { - const input: Schema & { somethingExtra: string } = { - ids: ['someId'], - somethingExtra: 'hello', - }; - const result = buildRouteValidation(schema)(input, validationResult); - expect(result).toEqual('invalid keys "somethingExtra"'); - }); + test('return validation error', () => { + const input: Omit & { id: string } = { id: 'someId' }; + const result = buildRouteValidation(schema)(input, validationResult); + + expect(result).toEqual('Invalid value "undefined" supplied to "ids"'); + }); + + test('return validated input', () => { + const input: Schema = { ids: ['someId'] }; + const result = buildRouteValidation(schema)(input, validationResult); + + expect(result).toEqual(input); + }); - test('return validation input for a deep 3rd level object', () => { - const input: DeepSchema = { topLevel: { secondLevel: { thirdLevel: 'hello' } } }; - const result = buildRouteValidation(deepSchema)(input, validationResult); - expect(result).toEqual(input); + test('returns validation error if given extra keys on input for an array', () => { + const input: Schema & { somethingExtra: string } = { + ids: ['someId'], + somethingExtra: 'hello', + }; + const result = buildRouteValidation(schema)(input, validationResult); + expect(result).toEqual('invalid keys "somethingExtra"'); + }); + + test('return validation input for a deep 3rd level object', () => { + const input: DeepSchema = { topLevel: { secondLevel: { thirdLevel: 'hello' } } }; + const result = buildRouteValidation(deepSchema)(input, validationResult); + expect(result).toEqual(input); + }); + + test('return validation error for a deep 3rd level object that has an extra key value of "somethingElse"', () => { + const input: DeepSchema & { + topLevel: { secondLevel: { thirdLevel: string; somethingElse: string } }; + } = { + topLevel: { secondLevel: { thirdLevel: 'hello', somethingElse: 'extraKey' } }, + }; + const result = buildRouteValidation(deepSchema)(input, validationResult); + expect(result).toEqual('invalid keys "somethingElse"'); + }); }); - test('return validation error for a deep 3rd level object that has an extra key value of "somethingElse"', () => { - const input: DeepSchema & { - topLevel: { secondLevel: { thirdLevel: string; somethingElse: string } }; - } = { - topLevel: { secondLevel: { thirdLevel: 'hello', somethingElse: 'extraKey' } }, + describe('buildRouteValidationwithExcess', () => { + const schema = rt.type({ + ids: rt.array(rt.string), + }); + type Schema = rt.TypeOf; + + /** + * If your schema is using exact all the way down then the validation will + * catch any additional keys that should not be present within the validation + * when the route_validation uses the exact check. + */ + const deepSchema = rt.type({ + topLevel: rt.type({ + secondLevel: rt.type({ + thirdLevel: rt.string, + }), + }), + }); + type DeepSchema = rt.TypeOf; + + const validationResult: RouteValidationResultFactory = { + ok: jest.fn().mockImplementation((validatedInput) => validatedInput), + badRequest: jest.fn().mockImplementation((e) => e), }; - const result = buildRouteValidation(deepSchema)(input, validationResult); - expect(result).toEqual('invalid keys "somethingElse"'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('return validation error', () => { + const input: Omit & { id: string } = { id: 'someId' }; + const result = buildRouteValidationWithExcess(schema)(input, validationResult); + + expect(result).toEqual('Invalid value {"id":"someId"}, excess properties: ["id"]'); + }); + + test('return validation error with intersection', () => { + const schemaI = rt.intersection([ + rt.type({ + ids: rt.array(rt.string), + }), + rt.partial({ + valid: rt.array(rt.string), + }), + ]); + type SchemaI = rt.TypeOf; + const input: Omit & { id: string } = { id: 'someId', valid: ['yes'] }; + const result = buildRouteValidationWithExcess(schemaI)(input, validationResult); + + expect(result).toEqual( + 'Invalid value {"id":"someId","valid":["yes"]}, excess properties: ["id"]' + ); + }); + + test('return NO validation error with a partial intersection', () => { + const schemaI = rt.intersection([ + rt.type({ + id: rt.array(rt.string), + }), + rt.partial({ + valid: rt.array(rt.string), + }), + ]); + const input = { id: ['someId'] }; + const result = buildRouteValidationWithExcess(schemaI)(input, validationResult); + + expect(result).toEqual({ id: ['someId'] }); + }); + + test('return validated input', () => { + const input: Schema = { ids: ['someId'] }; + const result = buildRouteValidationWithExcess(schema)(input, validationResult); + + expect(result).toEqual(input); + }); + + test('returns validation error if given extra keys on input for an array', () => { + const input: Schema & { somethingExtra: string } = { + ids: ['someId'], + somethingExtra: 'hello', + }; + const result = buildRouteValidationWithExcess(schema)(input, validationResult); + expect(result).toEqual( + 'Invalid value {"ids":["someId"],"somethingExtra":"hello"}, excess properties: ["somethingExtra"]' + ); + }); + + test('return validation input for a deep 3rd level object', () => { + const input: DeepSchema = { topLevel: { secondLevel: { thirdLevel: 'hello' } } }; + const result = buildRouteValidationWithExcess(deepSchema)(input, validationResult); + expect(result).toEqual(input); + }); + + test('return validation error for a deep 3rd level object that has an extra key value of "somethingElse"', () => { + const input: DeepSchema & { + topLevel: { secondLevel: { thirdLevel: string; somethingElse: string } }; + } = { + topLevel: { secondLevel: { thirdLevel: 'hello', somethingElse: 'extraKey' } }, + }; + const result = buildRouteValidationWithExcess(deepSchema)(input, validationResult); + expect(result).toEqual( + 'Invalid value {"topLevel":{"secondLevel":{"thirdLevel":"hello","somethingElse":"extraKey"}}}, excess properties: ["somethingElse"]' + ); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts b/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts index d7ab9affa6c1c1..51f807d6aad818 100644 --- a/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts +++ b/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts @@ -14,6 +14,7 @@ import { RouteValidationResultFactory, RouteValidationError, } from '../../../../../../src/core/server'; +import { excess, GenericIntersectionC } from '../runtime_types'; type RequestValidationResult = | { @@ -39,3 +40,20 @@ export const buildRouteValidation = >( (validatedInput: A) => validationResult.ok(validatedInput) ) ); + +export const buildRouteValidationWithExcess = < + T extends rt.InterfaceType | GenericIntersectionC | rt.PartialType, + A = rt.TypeOf +>( + schema: T +): RouteValidationFunction
    => ( + inputValue: unknown, + validationResult: RouteValidationResultFactory +) => + pipe( + excess(schema).decode(inputValue), + fold>( + (errors: rt.Errors) => validationResult.badRequest(formatErrors(errors).join()), + (validatedInput: A) => validationResult.ok(validatedInput) + ) + ); diff --git a/x-pack/plugins/security_solution/server/utils/runtime_types.ts b/x-pack/plugins/security_solution/server/utils/runtime_types.ts new file mode 100644 index 00000000000000..7177cc5765f8a5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/utils/runtime_types.ts @@ -0,0 +1,130 @@ +/* + * 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 { either, fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as rt from 'io-ts'; +import { failure } from 'io-ts/lib/PathReporter'; +import get from 'lodash/get'; + +type ErrorFactory = (message: string) => Error; + +export type GenericIntersectionC = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any]> + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any, any]> + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any, any, any]> + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any, any, any, any]>; + +export const createPlainError = (message: string) => new Error(message); + +export const throwErrors = (createError: ErrorFactory) => (errors: rt.Errors) => { + throw createError(failure(errors).join('\n')); +}; + +export const decodeOrThrow = ( + runtimeType: rt.Type, + createError: ErrorFactory = createPlainError +) => (inputValue: I) => + pipe(runtimeType.decode(inputValue), fold(throwErrors(createError), identity)); + +const getProps = ( + codec: + | rt.HasProps + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.RecordC + | GenericIntersectionC +): rt.Props | null => { + if (codec == null) { + return null; + } + switch (codec._tag) { + case 'DictionaryType': + if (codec.codomain.props != null) { + return codec.codomain.props; + } + const dTypes: rt.HasProps[] = codec.codomain.types; + return dTypes.reduce((props, type) => Object.assign(props, getProps(type)), {}); + case 'RefinementType': + case 'ReadonlyType': + return getProps(codec.type); + case 'InterfaceType': + case 'StrictType': + case 'PartialType': + return codec.props; + case 'IntersectionType': + const iTypes = codec.types as rt.HasProps[]; + return iTypes.reduce((props, type) => { + return Object.assign(props, getProps(type) as rt.Props); + }, {} as rt.Props) as rt.Props; + default: + return null; + } +}; + +const getExcessProps = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + props: rt.Props | rt.RecordC, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + r: any +): string[] => { + return Object.keys(r).reduce((acc, k) => { + const codecChildren = get(props, [k]); + const childrenProps = getProps(codecChildren); + const childrenObject = r[k] as Record; + if (codecChildren != null && childrenProps != null && codecChildren._tag === 'DictionaryType') { + const keys = Object.keys(childrenObject); + return [ + ...acc, + ...keys.reduce( + (kAcc, i) => [...kAcc, ...getExcessProps(childrenProps, childrenObject[i])], + [] + ), + ]; + } + if (codecChildren != null && childrenProps != null) { + return [...acc, ...getExcessProps(childrenProps, childrenObject)]; + } else if (codecChildren == null) { + return [...acc, k]; + } + return acc; + }, []); +}; + +export const excess = < + C extends rt.InterfaceType | GenericIntersectionC | rt.PartialType +>( + codec: C +): C => { + const codecProps = getProps(codec); + + const r = new rt.InterfaceType( + codec.name, + codec.is, + (i, c) => + either.chain(rt.UnknownRecord.validate(i, c), (s) => { + if (codecProps == null) { + return rt.failure(i, c, 'unknown codec'); + } + const ex = getExcessProps(codecProps, s); + + return ex.length > 0 + ? rt.failure( + i, + c, + `Invalid value ${JSON.stringify(i)}, excess properties: ${JSON.stringify(ex)}` + ) + : codec.validate(i, c); + }), + codec.encode, + codecProps + ); + return r as C; +}; From 2db789598bb9353fd3e690f43b1eacbd2e75839c Mon Sep 17 00:00:00 2001 From: John Schulz Date: Thu, 3 Sep 2020 09:12:43 -0400 Subject: [PATCH 216/216] [Ingest Manager] Split Registry errors into Connection & Response (#76558) * Split Registry errors into Connection & Response * Ensure a url in ResponseError message. Add tests --- .../plugins/ingest_manager/server/errors.ts | 11 +-- .../services/epm/registry/requests.test.ts | 98 ++++++++++++++++--- .../server/services/epm/registry/requests.ts | 32 +++--- 3 files changed, 108 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/errors.ts b/x-pack/plugins/ingest_manager/server/errors.ts index 401211409ebf75..e6ef4a51284b04 100644 --- a/x-pack/plugins/ingest_manager/server/errors.ts +++ b/x-pack/plugins/ingest_manager/server/errors.ts @@ -17,15 +17,14 @@ export const getHTTPResponseCode = (error: IngestManagerError): number => { return 502; // Bad Gateway } if (error instanceof PackageNotFoundError) { - return 404; - } - if (error instanceof PackageOutdatedError) { - return 400; - } else { - return 400; // Bad Request + return 404; // Not Found } + + return 400; // Bad Request }; export class RegistryError extends IngestManagerError {} +export class RegistryConnectionError extends RegistryError {} +export class RegistryResponseError extends RegistryError {} export class PackageNotFoundError extends IngestManagerError {} export class PackageOutdatedError extends IngestManagerError {} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.test.ts index f836a133a78a01..2f1d4000187407 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { fetchUrl } from './requests'; -import { RegistryError } from '../../../errors'; +import { RegistryError, RegistryConnectionError, RegistryResponseError } from '../../../errors'; jest.mock('node-fetch'); const { Response, FetchError } = jest.requireActual('node-fetch'); @@ -53,13 +53,7 @@ describe('setupIngestManager', () => { throw new FetchError('message 3', 'system', { code: 'ESOMETHING' }); }) // this one succeeds - .mockImplementationOnce(() => Promise.resolve(new Response(successValue))) - .mockImplementationOnce(() => { - throw new FetchError('message 5', 'system', { code: 'ESOMETHING' }); - }) - .mockImplementationOnce(() => { - throw new FetchError('message 6', 'system', { code: 'ESOMETHING' }); - }); + .mockImplementationOnce(() => Promise.resolve(new Response(successValue))); const promise = fetchUrl(''); await expect(promise).resolves.toEqual(successValue); @@ -69,7 +63,7 @@ describe('setupIngestManager', () => { expect(actualResultsOrder).toEqual(['throw', 'throw', 'throw', 'return']); }); - it('or error after 1 failure & 5 retries with RegistryError', async () => { + it('or error after 1 failure & 5 retries with RegistryConnectionError', async () => { fetchMock .mockImplementationOnce(() => { throw new FetchError('message 1', 'system', { code: 'ESOMETHING' }); @@ -88,21 +82,93 @@ describe('setupIngestManager', () => { }) .mockImplementationOnce(() => { throw new FetchError('message 6', 'system', { code: 'ESOMETHING' }); - }) - .mockImplementationOnce(() => { - throw new FetchError('message 7', 'system', { code: 'ESOMETHING' }); - }) - .mockImplementationOnce(() => { - throw new FetchError('message 8', 'system', { code: 'ESOMETHING' }); }); const promise = fetchUrl(''); - await expect(promise).rejects.toThrow(RegistryError); + await expect(promise).rejects.toThrow(RegistryConnectionError); // doesn't retry after 1 failure & 5 failed retries expect(fetchMock).toHaveBeenCalledTimes(6); const actualResultsOrder = fetchMock.mock.results.map(({ type }: { type: string }) => type); expect(actualResultsOrder).toEqual(['throw', 'throw', 'throw', 'throw', 'throw', 'throw']); }); }); + + describe('4xx or 5xx from Registry become RegistryResponseError', () => { + it('404', async () => { + fetchMock.mockImplementationOnce(() => ({ + ok: false, + status: 404, + statusText: 'Not Found', + url: 'https://example.com', + })); + const promise = fetchUrl(''); + await expect(promise).rejects.toThrow(RegistryResponseError); + await expect(promise).rejects.toThrow( + `'404 Not Found' error response from package registry at https://example.com` + ); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it('429', async () => { + fetchMock.mockImplementationOnce(() => ({ + ok: false, + status: 429, + statusText: 'Too Many Requests', + url: 'https://example.com', + })); + const promise = fetchUrl(''); + await expect(promise).rejects.toThrow(RegistryResponseError); + await expect(promise).rejects.toThrow( + `'429 Too Many Requests' error response from package registry at https://example.com` + ); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it('500', async () => { + fetchMock.mockImplementationOnce(() => ({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + url: 'https://example.com', + })); + const promise = fetchUrl(''); + await expect(promise).rejects.toThrow(RegistryResponseError); + await expect(promise).rejects.toThrow( + `'500 Internal Server Error' error response from package registry at https://example.com` + ); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + }); + + describe('url in RegistryResponseError message is response.url || requested_url', () => { + it('given response.url, use that', async () => { + fetchMock.mockImplementationOnce(() => ({ + ok: false, + status: 404, + statusText: 'Not Found', + url: 'https://example.com/?from_response=true', + })); + const promise = fetchUrl('https://example.com/?requested=true'); + await expect(promise).rejects.toThrow(RegistryResponseError); + await expect(promise).rejects.toThrow( + `'404 Not Found' error response from package registry at https://example.com/?from_response=true` + ); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it('no response.url, use requested url', async () => { + fetchMock.mockImplementationOnce(() => ({ + ok: false, + status: 404, + statusText: 'Not Found', + })); + const promise = fetchUrl('https://example.com/?requested=true'); + await expect(promise).rejects.toThrow(RegistryResponseError); + await expect(promise).rejects.toThrow( + `'404 Not Found' error response from package registry at https://example.com/?requested=true` + ); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + }); }); }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.ts index 5939dc204aae66..e549d6b1f71aa1 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.ts @@ -7,7 +7,7 @@ import fetch, { FetchError, Response } from 'node-fetch'; import pRetry from 'p-retry'; import { streamToString } from './streams'; -import { RegistryError } from '../../../errors'; +import { RegistryError, RegistryConnectionError, RegistryResponseError } from '../../../errors'; type FailedAttemptErrors = pRetry.FailedAttemptError | FetchError | Error; @@ -19,10 +19,13 @@ async function registryFetch(url: string) { return response; } else { // 4xx & 5xx responses - // exit without retry & throw RegistryError - throw new pRetry.AbortError( - new RegistryError(`Error connecting to package registry at ${url}: ${response.statusText}`) - ); + const { status, statusText, url: resUrl } = response; + const message = `'${status} ${statusText}' error response from package registry at ${ + resUrl || url + }`; + const responseError = new RegistryResponseError(message); + + throw new pRetry.AbortError(responseError); } } @@ -38,17 +41,24 @@ export async function getResponse(url: string): Promise { // and let the others through without retrying // // throwing in onFailedAttempt will abandon all retries & fail the request - // we only want to retry system errors, so throw a RegistryError for everything else + // we only want to retry system errors, so re-throw for everything else if (!isSystemError(error)) { - throw new RegistryError( - `Error connecting to package registry at ${url}: ${error.message}` - ); + throw error; } }, }); return response; - } catch (e) { - throw new RegistryError(`Error connecting to package registry at ${url}: ${e.message}`); + } catch (error) { + // isSystemError here means we didn't succeed after max retries + if (isSystemError(error)) { + throw new RegistryConnectionError(`Error connecting to package registry: ${error.message}`); + } + // don't wrap our own errors + if (error instanceof RegistryError) { + throw error; + } else { + throw new RegistryError(error); + } } }