diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.customname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.customlabel.md similarity index 58% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.customname.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.customlabel.md index b30201f9e3991..6a997d517e98d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.customname.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.customlabel.md @@ -1,11 +1,11 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IFieldType](./kibana-plugin-plugins-data-public.ifieldtype.md) > [customName](./kibana-plugin-plugins-data-public.ifieldtype.customname.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IFieldType](./kibana-plugin-plugins-data-public.ifieldtype.md) > [customLabel](./kibana-plugin-plugins-data-public.ifieldtype.customlabel.md) -## IFieldType.customName property +## IFieldType.customLabel property Signature: ```typescript -customName?: string; +customLabel?: string; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md index 6f3876ff82f04..2b3d3df1ec8d0 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md @@ -16,7 +16,7 @@ export interface IFieldType | --- | --- | --- | | [aggregatable](./kibana-plugin-plugins-data-public.ifieldtype.aggregatable.md) | boolean | | | [count](./kibana-plugin-plugins-data-public.ifieldtype.count.md) | number | | -| [customName](./kibana-plugin-plugins-data-public.ifieldtype.customname.md) | string | | +| [customLabel](./kibana-plugin-plugins-data-public.ifieldtype.customlabel.md) | string | | | [displayName](./kibana-plugin-plugins-data-public.ifieldtype.displayname.md) | string | | | [esTypes](./kibana-plugin-plugins-data-public.ifieldtype.estypes.md) | string[] | | | [filterable](./kibana-plugin-plugins-data-public.ifieldtype.filterable.md) | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md index f81edf4b94b42..0c1fbe7d0d1b6 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md @@ -9,7 +9,7 @@ ```typescript getFieldAttrs: () => { [x: string]: { - customName: string; + customLabel: string; }; }; ``` 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 1228bf7adc2ef..3383116f404b2 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,7 +27,7 @@ export declare class IndexPattern implements IIndexPattern | [flattenHit](./kibana-plugin-plugins-data-public.indexpattern.flattenhit.md) | | (hit: Record<string, any>, deep?: boolean) => Record<string, any> | | | [formatField](./kibana-plugin-plugins-data-public.indexpattern.formatfield.md) | | FormatFieldFn | | | [formatHit](./kibana-plugin-plugins-data-public.indexpattern.formathit.md) | | {
(hit: Record<string, any>, type?: string): any;
formatField: FormatFieldFn;
} | | -| [getFieldAttrs](./kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md) | | () => {
[x: string]: {
customName: string;
};
} | | +| [getFieldAttrs](./kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md) | | () => {
[x: string]: {
customLabel: string;
};
} | | | [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md) | | () => {
fieldAttrs?: string | undefined;
title?: string | undefined;
timeFieldName?: string | undefined;
intervalName?: string | undefined;
fields?: string | undefined;
sourceFilters?: string | undefined;
fieldFormatMap?: string | undefined;
typeMeta?: string | undefined;
type?: string | undefined;
} | Get last saved saved object fields | | [id](./kibana-plugin-plugins-data-public.indexpattern.id.md) | | string | | | [intervalName](./kibana-plugin-plugins-data-public.indexpattern.intervalname.md) | | string | undefined | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.customname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.customlabel.md similarity index 59% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.customname.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.customlabel.md index ef8f9f1d31e4f..8d9c1b7a1161e 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.customname.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.customlabel.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [customName](./kibana-plugin-plugins-data-public.indexpatternfield.customname.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [customLabel](./kibana-plugin-plugins-data-public.indexpatternfield.customlabel.md) -## IndexPatternField.customName property +## IndexPatternField.customLabel property Signature: ```typescript -get customName(): string | undefined; +get customLabel(): string | undefined; -set customName(label: string | undefined); +set customLabel(customLabel: string | undefined); ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md index ef99b4353a70b..caf7d374161dd 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md @@ -23,7 +23,7 @@ export declare class IndexPatternField implements IFieldType | [aggregatable](./kibana-plugin-plugins-data-public.indexpatternfield.aggregatable.md) | | boolean | | | [conflictDescriptions](./kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md) | | Record<string, string[]> | undefined | Description of field type conflicts across different indices in the same index pattern | | [count](./kibana-plugin-plugins-data-public.indexpatternfield.count.md) | | number | Count is used for field popularity | -| [customName](./kibana-plugin-plugins-data-public.indexpatternfield.customname.md) | | string | undefined | | +| [customLabel](./kibana-plugin-plugins-data-public.indexpatternfield.customlabel.md) | | string | undefined | | | [displayName](./kibana-plugin-plugins-data-public.indexpatternfield.displayname.md) | | string | | | [esTypes](./kibana-plugin-plugins-data-public.indexpatternfield.estypes.md) | | string[] | undefined | | | [filterable](./kibana-plugin-plugins-data-public.indexpatternfield.filterable.md) | | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md index c7237701ae49d..f0600dd20658a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md @@ -20,7 +20,7 @@ toJSON(): { aggregatable: boolean; readFromDocValues: boolean; subType: import("../types").IFieldSubType | undefined; - customName: string | undefined; + customLabel: string | undefined; }; ``` Returns: @@ -38,6 +38,6 @@ toJSON(): { aggregatable: boolean; readFromDocValues: boolean; subType: import("../types").IFieldSubType | undefined; - customName: string | undefined; + customLabel: string | undefined; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.customname.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.customlabel.md similarity index 58% rename from docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.customname.md rename to docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.customlabel.md index f5fbc084237f2..8d4868cb8e9ab 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.customname.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.customlabel.md @@ -1,11 +1,11 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IFieldType](./kibana-plugin-plugins-data-server.ifieldtype.md) > [customName](./kibana-plugin-plugins-data-server.ifieldtype.customname.md) +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IFieldType](./kibana-plugin-plugins-data-server.ifieldtype.md) > [customLabel](./kibana-plugin-plugins-data-server.ifieldtype.customlabel.md) -## IFieldType.customName property +## IFieldType.customLabel property Signature: ```typescript -customName?: string; +customLabel?: string; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md index 638700b1d24f8..48836a1b620b8 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md @@ -16,7 +16,7 @@ export interface IFieldType | --- | --- | --- | | [aggregatable](./kibana-plugin-plugins-data-server.ifieldtype.aggregatable.md) | boolean | | | [count](./kibana-plugin-plugins-data-server.ifieldtype.count.md) | number | | -| [customName](./kibana-plugin-plugins-data-server.ifieldtype.customname.md) | string | | +| [customLabel](./kibana-plugin-plugins-data-server.ifieldtype.customlabel.md) | string | | | [displayName](./kibana-plugin-plugins-data-server.ifieldtype.displayname.md) | string | | | [esTypes](./kibana-plugin-plugins-data-server.ifieldtype.estypes.md) | string[] | | | [filterable](./kibana-plugin-plugins-data-server.ifieldtype.filterable.md) | boolean | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md index 80dd329232ed8..b1e38258353c3 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md @@ -9,7 +9,7 @@ ```typescript getFieldAttrs: () => { [x: string]: { - customName: string; + customLabel: string; }; }; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md index 3d2b021b29515..5103af52f1b43 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md @@ -27,7 +27,7 @@ export declare class IndexPattern implements IIndexPattern | [flattenHit](./kibana-plugin-plugins-data-server.indexpattern.flattenhit.md) | | (hit: Record<string, any>, deep?: boolean) => Record<string, any> | | | [formatField](./kibana-plugin-plugins-data-server.indexpattern.formatfield.md) | | FormatFieldFn | | | [formatHit](./kibana-plugin-plugins-data-server.indexpattern.formathit.md) | | {
(hit: Record<string, any>, type?: string): any;
formatField: FormatFieldFn;
} | | -| [getFieldAttrs](./kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md) | | () => {
[x: string]: {
customName: string;
};
} | | +| [getFieldAttrs](./kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md) | | () => {
[x: string]: {
customLabel: string;
};
} | | | [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md) | | () => {
fieldAttrs?: string | undefined;
title?: string | undefined;
timeFieldName?: string | undefined;
intervalName?: string | undefined;
fields?: string | undefined;
sourceFilters?: string | undefined;
fieldFormatMap?: string | undefined;
typeMeta?: string | undefined;
type?: string | undefined;
} | Get last saved saved object fields | | [id](./kibana-plugin-plugins-data-server.indexpattern.id.md) | | string | | | [intervalName](./kibana-plugin-plugins-data-server.indexpattern.intervalname.md) | | string | undefined | | diff --git a/package.json b/package.json index 23f7a0b430654..33a509e863793 100644 --- a/package.json +++ b/package.json @@ -693,15 +693,15 @@ "is-glob": "^4.0.1", "is-path-inside": "^3.0.2", "istanbul-instrumenter-loader": "^3.0.1", - "jest": "^26.4.2", + "jest": "^26.6.3", "jest-canvas-mock": "^2.2.0", - "jest-circus": "^26.4.2", - "jest-cli": "^26.4.2", - "jest-diff": "^26.4.2", + "jest-circus": "^26.6.3", + "jest-cli": "^26.6.3", + "jest-diff": "^26.6.2", "jest-environment-jsdom-thirteen": "^1.0.1", "jest-raw-loader": "^1.0.1", "jest-silent-reporter": "^0.2.1", - "jest-snapshot": "^26.4.2", + "jest-snapshot": "^26.6.2", "jest-specific-snapshot": "2.0.0", "jest-styled-components": "^7.0.2", "jest-when": "^2.7.2", @@ -723,7 +723,7 @@ "less": "npm:@elastic/less@2.7.3-kibana", "license-checker": "^16.0.0", "listr": "^0.14.1", - "lmdb-store": "^0.8.15", + "lmdb-store": "^0.6.10", "load-grunt-config": "^3.0.1", "loader-utils": "^1.2.3", "log-symbols": "^2.2.0", diff --git a/packages/kbn-i18n/src/react/index.tsx b/packages/kbn-i18n/src/react/index.tsx index b438c44598b75..857a2b0824b55 100644 --- a/packages/kbn-i18n/src/react/index.tsx +++ b/packages/kbn-i18n/src/react/index.tsx @@ -18,9 +18,7 @@ */ import { InjectedIntl as _InjectedIntl, InjectedIntlProps as _InjectedIntlProps } from 'react-intl'; - -export type InjectedIntl = _InjectedIntl; -export type InjectedIntlProps = _InjectedIntlProps; +export type { InjectedIntl, InjectedIntlProps } from 'react-intl'; export { intlShape, diff --git a/packages/kbn-optimizer/src/node/cache.ts b/packages/kbn-optimizer/src/node/cache.ts index a73dba5b16469..e918bae86c835 100644 --- a/packages/kbn-optimizer/src/node/cache.ts +++ b/packages/kbn-optimizer/src/node/cache.ts @@ -18,11 +18,20 @@ */ import Path from 'path'; +import Fs from 'fs'; +// @ts-expect-error no types available import * as LmdbStore from 'lmdb-store'; import { REPO_ROOT, UPSTREAM_BRANCH } from '@kbn/dev-utils'; -const CACHE_DIR = Path.resolve(REPO_ROOT, 'data/node_auto_transpilation_cache', UPSTREAM_BRANCH); +const LMDB_PKG = JSON.parse( + Fs.readFileSync(Path.resolve(REPO_ROOT, 'node_modules/lmdb-store/package.json'), 'utf8') +); +const CACHE_DIR = Path.resolve( + REPO_ROOT, + `data/node_auto_transpilation_cache/lmdb-${LMDB_PKG.version}/${UPSTREAM_BRANCH}` +); + const reportError = () => { // right now I'm not sure we need to worry about errors, the cache isn't actually // necessary, and if the cache is broken it should just rebuild on the next restart @@ -36,11 +45,30 @@ const MINUTE = 1000 * 60; const HOUR = MINUTE * 60; const DAY = HOUR * 24; +interface Lmdb { + name: string; + get(key: string): T | undefined; + put(key: string, value: T, version?: number, ifVersion?: number): Promise; + remove(key: string, ifVersion?: number): Promise; + removeSync(key: string): void; + openDB(options: { + name: string; + encoding: 'msgpack' | 'string' | 'json' | 'binary'; + }): Lmdb; + getRange(options?: { + start?: T; + end?: T; + reverse?: boolean; + limit?: number; + versions?: boolean; + }): Iterable<{ key: string; value: T }>; +} + export class Cache { - private readonly codes: LmdbStore.RootDatabase; - private readonly atimes: LmdbStore.Database; - private readonly mtimes: LmdbStore.Database; - private readonly sourceMaps: LmdbStore.Database; + private readonly codes: Lmdb; + private readonly atimes: Lmdb; + private readonly mtimes: Lmdb; + private readonly sourceMaps: Lmdb; private readonly prefix: string; constructor(config: { prefix: string }) { @@ -77,7 +105,7 @@ export class Cache { } getMtime(path: string) { - return this.safeGet(this.mtimes, this.getKey(path)); + return this.safeGet(this.mtimes, this.getKey(path)); } getCode(path: string) { @@ -88,11 +116,11 @@ export class Cache { // touched in a long time (currently 30 days) this.atimes.put(key, GLOBAL_ATIME).catch(reportError); - return this.safeGet(this.codes, key); + return this.safeGet(this.codes, key); } getSourceMap(path: string) { - return this.safeGet(this.sourceMaps, this.getKey(path)); + return this.safeGet(this.sourceMaps, this.getKey(path)); } update(path: string, file: { mtime: string; code: string; map: any }) { @@ -110,11 +138,13 @@ export class Cache { return `${this.prefix}${path}`; } - private safeGet(db: LmdbStore.Database, key: string) { + private safeGet(db: Lmdb, key: string) { try { - return db.get(key) as V | undefined; + return db.get(key); } catch (error) { - // get errors indicate that a key value is corrupt in some way, so remove it + process.stderr.write( + `failed to read node transpilation [${db.name}] cache for [${key}]: ${error.stack}\n` + ); db.removeSync(key); } } @@ -124,13 +154,12 @@ export class Cache { const ATIME_LIMIT = Date.now() - 30 * DAY; const BATCH_SIZE = 1000; - const validKeys: LmdbStore.Key[] = []; - const invalidKeys: LmdbStore.Key[] = []; + const validKeys: string[] = []; + const invalidKeys: string[] = []; - // @ts-expect-error See https://github.com/DoctorEvidence/lmdb-store/pull/18 for (const { key, value } of this.atimes.getRange()) { - const atime = parseInt(`${value}`, 10); - if (Number.isNaN(atime) || atime < ATIME_LIMIT) { + const atime = parseInt(value, 10); + if (atime < ATIME_LIMIT) { invalidKeys.push(key); } else { validKeys.push(key); diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index cd8b1f674fa40..c62b3f2afc14d 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -94,7 +94,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _cli__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "run", function() { return _cli__WEBPACK_IMPORTED_MODULE_0__["run"]; }); -/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(506); +/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(511); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildProductionProjects"]; }); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(248); @@ -106,7 +106,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(251); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "transformDependencies", function() { return _utils_package_json__WEBPACK_IMPORTED_MODULE_4__["transformDependencies"]; }); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(505); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(510); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjectPaths", function() { return _config__WEBPACK_IMPORTED_MODULE_5__["getProjectPaths"]; }); /* @@ -150,7 +150,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _commands__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(128); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(499); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(504); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(246); /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -8897,9 +8897,9 @@ exports.ToolingLogCollectingWriter = ToolingLogCollectingWriter; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "commands", function() { return commands; }); /* harmony import */ var _bootstrap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(129); -/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(367); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(398); -/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(399); +/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(372); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(403); +/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(404); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -8942,10 +8942,10 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(246); /* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(247); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(248); -/* harmony import */ var _utils_project_checksums__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(359); -/* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(364); -/* harmony import */ var _utils_yarn_lock__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(361); -/* harmony import */ var _utils_validate_dependencies__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(365); +/* harmony import */ var _utils_project_checksums__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(364); +/* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(369); +/* harmony import */ var _utils_yarn_lock__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(366); +/* harmony import */ var _utils_validate_dependencies__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(370); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -22997,7 +22997,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _errors__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(249); /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(246); /* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(251); -/* harmony import */ var _scripts__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(313); +/* harmony import */ var _scripts__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(318); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -23205,7 +23205,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "transformDependencies", function() { return transformDependencies; }); /* harmony import */ var read_pkg__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(252); /* harmony import */ var read_pkg__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(read_pkg__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var write_pkg__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(300); +/* harmony import */ var write_pkg__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(305); /* harmony import */ var write_pkg__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(write_pkg__WEBPACK_IMPORTED_MODULE_1__); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } @@ -26091,7 +26091,7 @@ module.exports = normalize var fixer = __webpack_require__(275) normalize.fixer = fixer -var makeWarning = __webpack_require__(298) +var makeWarning = __webpack_require__(303) var fieldsToFix = ['name','version','description','repository','modules','scripts' ,'files','bin','man','bugs','keywords','readme','homepage','license'] @@ -26136,9 +26136,9 @@ var validateLicense = __webpack_require__(277); var hostedGitInfo = __webpack_require__(282) var isBuiltinModule = __webpack_require__(286).isCore var depTypes = ["dependencies","devDependencies","optionalDependencies"] -var extractDescription = __webpack_require__(296) +var extractDescription = __webpack_require__(301) var url = __webpack_require__(283) -var typos = __webpack_require__(297) +var typos = __webpack_require__(302) var fixer = module.exports = { // default warning function @@ -30089,9 +30089,9 @@ GitHost.prototype.toString = function (opts) { /***/ (function(module, exports, __webpack_require__) { var async = __webpack_require__(287); -async.core = __webpack_require__(293); -async.isCore = __webpack_require__(292); -async.sync = __webpack_require__(295); +async.core = __webpack_require__(297); +async.isCore = __webpack_require__(299); +async.sync = __webpack_require__(300); module.exports = async; @@ -30175,6 +30175,7 @@ module.exports = function resolve(x, options, callback) { var packageIterator = opts.packageIterator; var extensions = opts.extensions || ['.js']; + var includeCoreModules = opts.includeCoreModules !== false; var basedir = opts.basedir || path.dirname(caller()); var parent = opts.filename || basedir; @@ -30201,7 +30202,7 @@ module.exports = function resolve(x, options, callback) { if ((/\/$/).test(x) && res === basedir) { loadAsDirectory(res, opts.package, onfile); } else loadAsFile(res, opts.package, onfile); - } else if (isCore(x)) { + } else if (includeCoreModules && isCore(x)) { return cb(null, x); } else loadNodeModules(x, basedir, function (err, n, pkg) { if (err) cb(err); @@ -30582,10 +30583,75 @@ module.exports = function (x, opts) { /* 292 */ /***/ (function(module, exports, __webpack_require__) { -var core = __webpack_require__(293); +"use strict"; -module.exports = function isCore(x) { - return Object.prototype.hasOwnProperty.call(core, x); + +var has = __webpack_require__(293); + +function specifierIncluded(current, specifier) { + var nodeParts = current.split('.'); + var parts = specifier.split(' '); + var op = parts.length > 1 ? parts[0] : '='; + var versionParts = (parts.length > 1 ? parts[1] : parts[0]).split('.'); + + for (var i = 0; i < 3; ++i) { + var cur = parseInt(nodeParts[i] || 0, 10); + var ver = parseInt(versionParts[i] || 0, 10); + if (cur === ver) { + continue; // eslint-disable-line no-restricted-syntax, no-continue + } + if (op === '<') { + return cur < ver; + } + if (op === '>=') { + return cur >= ver; + } + return false; + } + return op === '>='; +} + +function matchesRange(current, range) { + var specifiers = range.split(/ ?&& ?/); + if (specifiers.length === 0) { + return false; + } + for (var i = 0; i < specifiers.length; ++i) { + if (!specifierIncluded(current, specifiers[i])) { + return false; + } + } + return true; +} + +function versionIncluded(nodeVersion, specifierValue) { + if (typeof specifierValue === 'boolean') { + return specifierValue; + } + + var current = typeof nodeVersion === 'undefined' + ? process.versions && process.versions.node && process.versions.node + : nodeVersion; + + if (typeof current !== 'string') { + throw new TypeError(typeof nodeVersion === 'undefined' ? 'Unable to determine current node version' : 'If provided, a valid node version is required'); + } + + if (specifierValue && typeof specifierValue === 'object') { + for (var i = 0; i < specifierValue.length; ++i) { + if (matchesRange(current, specifierValue[i])) { + return true; + } + } + return false; + } + return matchesRange(current, specifierValue); +} + +var data = __webpack_require__(296); + +module.exports = function isCore(x, nodeVersion) { + return has(data, x) && versionIncluded(nodeVersion, data[x]); }; @@ -30593,6 +30659,95 @@ module.exports = function isCore(x) { /* 293 */ /***/ (function(module, exports, __webpack_require__) { +"use strict"; + + +var bind = __webpack_require__(294); + +module.exports = bind.call(Function.call, Object.prototype.hasOwnProperty); + + +/***/ }), +/* 294 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var implementation = __webpack_require__(295); + +module.exports = Function.prototype.bind || implementation; + + +/***/ }), +/* 295 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +/* eslint no-invalid-this: 1 */ + +var ERROR_MESSAGE = 'Function.prototype.bind called on incompatible '; +var slice = Array.prototype.slice; +var toStr = Object.prototype.toString; +var funcType = '[object Function]'; + +module.exports = function bind(that) { + var target = this; + if (typeof target !== 'function' || toStr.call(target) !== funcType) { + throw new TypeError(ERROR_MESSAGE + target); + } + var args = slice.call(arguments, 1); + + var bound; + var binder = function () { + if (this instanceof bound) { + var result = target.apply( + this, + args.concat(slice.call(arguments)) + ); + if (Object(result) === result) { + return result; + } + return this; + } else { + return target.apply( + that, + args.concat(slice.call(arguments)) + ); + } + }; + + var boundLength = Math.max(0, target.length - args.length); + var boundArgs = []; + for (var i = 0; i < boundLength; i++) { + boundArgs.push('$' + i); + } + + bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this,arguments); }')(binder); + + if (target.prototype) { + var Empty = function Empty() {}; + Empty.prototype = target.prototype; + bound.prototype = new Empty(); + Empty.prototype = null; + } + + return bound; +}; + + +/***/ }), +/* 296 */ +/***/ (function(module) { + +module.exports = JSON.parse("{\"assert\":true,\"assert/strict\":\">= 15\",\"async_hooks\":\">= 8\",\"buffer_ieee754\":\"< 0.9.7\",\"buffer\":true,\"child_process\":true,\"cluster\":true,\"console\":true,\"constants\":true,\"crypto\":true,\"_debug_agent\":\">= 1 && < 8\",\"_debugger\":\"< 8\",\"dgram\":true,\"diagnostics_channel\":\">= 15.1\",\"dns\":true,\"dns/promises\":\">= 15\",\"domain\":\">= 0.7.12\",\"events\":true,\"freelist\":\"< 6\",\"fs\":true,\"fs/promises\":[\">= 10 && < 10.1\",\">= 14\"],\"_http_agent\":\">= 0.11.1\",\"_http_client\":\">= 0.11.1\",\"_http_common\":\">= 0.11.1\",\"_http_incoming\":\">= 0.11.1\",\"_http_outgoing\":\">= 0.11.1\",\"_http_server\":\">= 0.11.1\",\"http\":true,\"http2\":\">= 8.8\",\"https\":true,\"inspector\":\">= 8.0.0\",\"_linklist\":\"< 8\",\"module\":true,\"net\":true,\"node-inspect/lib/_inspect\":\">= 7.6.0 && < 12\",\"node-inspect/lib/internal/inspect_client\":\">= 7.6.0 && < 12\",\"node-inspect/lib/internal/inspect_repl\":\">= 7.6.0 && < 12\",\"os\":true,\"path\":true,\"perf_hooks\":\">= 8.5\",\"process\":\">= 1\",\"punycode\":true,\"querystring\":true,\"readline\":true,\"repl\":true,\"smalloc\":\">= 0.11.5 && < 3\",\"_stream_duplex\":\">= 0.9.4\",\"_stream_transform\":\">= 0.9.4\",\"_stream_wrap\":\">= 1.4.1\",\"_stream_passthrough\":\">= 0.9.4\",\"_stream_readable\":\">= 0.9.4\",\"_stream_writable\":\">= 0.9.4\",\"stream\":true,\"stream/promises\":\">= 15\",\"string_decoder\":true,\"sys\":[\">= 0.6 && < 0.7\",\">= 0.8\"],\"timers\":true,\"timers/promises\":\">= 15\",\"_tls_common\":\">= 0.11.13\",\"_tls_legacy\":\">= 0.11.3 && < 10\",\"_tls_wrap\":\">= 0.11.3\",\"tls\":true,\"trace_events\":\">= 10\",\"tty\":true,\"url\":true,\"util\":true,\"v8/tools/arguments\":\">= 10 && < 12\",\"v8/tools/codemap\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/consarray\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/csvparser\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/logreader\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/profile_view\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/splaytree\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8\":\">= 1\",\"vm\":true,\"wasi\":\">= 13.4 && < 13.5\",\"worker_threads\":\">= 11.7\",\"zlib\":true}"); + +/***/ }), +/* 297 */ +/***/ (function(module, exports, __webpack_require__) { + var current = (process.versions && process.versions.node && process.versions.node.split('.')) || []; function specifierIncluded(specifier) { @@ -30601,8 +30756,8 @@ function specifierIncluded(specifier) { var versionParts = (parts.length > 1 ? parts[1] : parts[0]).split('.'); for (var i = 0; i < 3; ++i) { - var cur = Number(current[i] || 0); - var ver = Number(versionParts[i] || 0); + var cur = parseInt(current[i] || 0, 10); + var ver = parseInt(versionParts[i] || 0, 10); if (cur === ver) { continue; // eslint-disable-line no-restricted-syntax, no-continue } @@ -30637,7 +30792,7 @@ function versionIncluded(specifierValue) { return matchesRange(specifierValue); } -var data = __webpack_require__(294); +var data = __webpack_require__(298); var core = {}; for (var mod in data) { // eslint-disable-line no-restricted-syntax @@ -30649,13 +30804,24 @@ module.exports = core; /***/ }), -/* 294 */ +/* 298 */ /***/ (function(module) { -module.exports = JSON.parse("{\"assert\":true,\"async_hooks\":\">= 8\",\"buffer_ieee754\":\"< 0.9.7\",\"buffer\":true,\"child_process\":true,\"cluster\":true,\"console\":true,\"constants\":true,\"crypto\":true,\"_debug_agent\":\">= 1 && < 8\",\"_debugger\":\"< 8\",\"dgram\":true,\"dns\":true,\"domain\":true,\"events\":true,\"freelist\":\"< 6\",\"fs\":true,\"fs/promises\":[\">= 10 && < 10.1\",\">= 14\"],\"_http_agent\":\">= 0.11.1\",\"_http_client\":\">= 0.11.1\",\"_http_common\":\">= 0.11.1\",\"_http_incoming\":\">= 0.11.1\",\"_http_outgoing\":\">= 0.11.1\",\"_http_server\":\">= 0.11.1\",\"http\":true,\"http2\":\">= 8.8\",\"https\":true,\"inspector\":\">= 8.0.0\",\"_linklist\":\"< 8\",\"module\":true,\"net\":true,\"node-inspect/lib/_inspect\":\">= 7.6.0 && < 12\",\"node-inspect/lib/internal/inspect_client\":\">= 7.6.0 && < 12\",\"node-inspect/lib/internal/inspect_repl\":\">= 7.6.0 && < 12\",\"os\":true,\"path\":true,\"perf_hooks\":\">= 8.5\",\"process\":\">= 1\",\"punycode\":true,\"querystring\":true,\"readline\":true,\"repl\":true,\"smalloc\":\">= 0.11.5 && < 3\",\"_stream_duplex\":\">= 0.9.4\",\"_stream_transform\":\">= 0.9.4\",\"_stream_wrap\":\">= 1.4.1\",\"_stream_passthrough\":\">= 0.9.4\",\"_stream_readable\":\">= 0.9.4\",\"_stream_writable\":\">= 0.9.4\",\"stream\":true,\"string_decoder\":true,\"sys\":true,\"timers\":true,\"_tls_common\":\">= 0.11.13\",\"_tls_legacy\":\">= 0.11.3 && < 10\",\"_tls_wrap\":\">= 0.11.3\",\"tls\":true,\"trace_events\":\">= 10\",\"tty\":true,\"url\":true,\"util\":true,\"v8/tools/arguments\":\">= 10 && < 12\",\"v8/tools/codemap\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/consarray\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/csvparser\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/logreader\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/profile_view\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/splaytree\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8\":\">= 1\",\"vm\":true,\"wasi\":\">= 13.4 && < 13.5\",\"worker_threads\":\">= 11.7\",\"zlib\":true}"); +module.exports = JSON.parse("{\"assert\":true,\"assert/strict\":\">= 15\",\"async_hooks\":\">= 8\",\"buffer_ieee754\":\"< 0.9.7\",\"buffer\":true,\"child_process\":true,\"cluster\":true,\"console\":true,\"constants\":true,\"crypto\":true,\"_debug_agent\":\">= 1 && < 8\",\"_debugger\":\"< 8\",\"dgram\":true,\"diagnostics_channel\":\">= 15.1\",\"dns\":true,\"dns/promises\":\">= 15\",\"domain\":\">= 0.7.12\",\"events\":true,\"freelist\":\"< 6\",\"fs\":true,\"fs/promises\":[\">= 10 && < 10.1\",\">= 14\"],\"_http_agent\":\">= 0.11.1\",\"_http_client\":\">= 0.11.1\",\"_http_common\":\">= 0.11.1\",\"_http_incoming\":\">= 0.11.1\",\"_http_outgoing\":\">= 0.11.1\",\"_http_server\":\">= 0.11.1\",\"http\":true,\"http2\":\">= 8.8\",\"https\":true,\"inspector\":\">= 8.0.0\",\"_linklist\":\"< 8\",\"module\":true,\"net\":true,\"node-inspect/lib/_inspect\":\">= 7.6.0 && < 12\",\"node-inspect/lib/internal/inspect_client\":\">= 7.6.0 && < 12\",\"node-inspect/lib/internal/inspect_repl\":\">= 7.6.0 && < 12\",\"os\":true,\"path\":true,\"perf_hooks\":\">= 8.5\",\"process\":\">= 1\",\"punycode\":true,\"querystring\":true,\"readline\":true,\"repl\":true,\"smalloc\":\">= 0.11.5 && < 3\",\"_stream_duplex\":\">= 0.9.4\",\"_stream_transform\":\">= 0.9.4\",\"_stream_wrap\":\">= 1.4.1\",\"_stream_passthrough\":\">= 0.9.4\",\"_stream_readable\":\">= 0.9.4\",\"_stream_writable\":\">= 0.9.4\",\"stream\":true,\"stream/promises\":\">= 15\",\"string_decoder\":true,\"sys\":[\">= 0.6 && < 0.7\",\">= 0.8\"],\"timers\":true,\"timers/promises\":\">= 15\",\"_tls_common\":\">= 0.11.13\",\"_tls_legacy\":\">= 0.11.3 && < 10\",\"_tls_wrap\":\">= 0.11.3\",\"tls\":true,\"trace_events\":\">= 10\",\"tty\":true,\"url\":true,\"util\":true,\"v8/tools/arguments\":\">= 10 && < 12\",\"v8/tools/codemap\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/consarray\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/csvparser\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/logreader\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/profile_view\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8/tools/splaytree\":[\">= 4.4.0 && < 5\",\">= 5.2.0 && < 12\"],\"v8\":\">= 1\",\"vm\":true,\"wasi\":\">= 13.4 && < 13.5\",\"worker_threads\":\">= 11.7\",\"zlib\":true}"); /***/ }), -/* 295 */ +/* 299 */ +/***/ (function(module, exports, __webpack_require__) { + +var isCoreModule = __webpack_require__(292); + +module.exports = function isCore(x) { + return isCoreModule(x); +}; + + +/***/ }), +/* 300 */ /***/ (function(module, exports, __webpack_require__) { var isCore = __webpack_require__(292); @@ -30726,6 +30892,7 @@ module.exports = function resolveSync(x, options) { var packageIterator = opts.packageIterator; var extensions = opts.extensions || ['.js']; + var includeCoreModules = opts.includeCoreModules !== false; var basedir = opts.basedir || path.dirname(caller()); var parent = opts.filename || basedir; @@ -30739,7 +30906,7 @@ module.exports = function resolveSync(x, options) { if (x === '.' || x === '..' || x.slice(-1) === '/') res += '/'; var m = loadAsFileSync(res) || loadAsDirectorySync(res); if (m) return maybeRealpathSync(realpathSync, m, opts); - } else if (isCore(x)) { + } else if (includeCoreModules && isCore(x)) { return x; } else { var n = loadNodeModulesSync(x, absoluteStart); @@ -30852,7 +31019,7 @@ module.exports = function resolveSync(x, options) { /***/ }), -/* 296 */ +/* 301 */ /***/ (function(module, exports) { module.exports = extractDescription @@ -30872,17 +31039,17 @@ function extractDescription (d) { /***/ }), -/* 297 */ +/* 302 */ /***/ (function(module) { module.exports = JSON.parse("{\"topLevel\":{\"dependancies\":\"dependencies\",\"dependecies\":\"dependencies\",\"depdenencies\":\"dependencies\",\"devEependencies\":\"devDependencies\",\"depends\":\"dependencies\",\"dev-dependencies\":\"devDependencies\",\"devDependences\":\"devDependencies\",\"devDepenencies\":\"devDependencies\",\"devdependencies\":\"devDependencies\",\"repostitory\":\"repository\",\"repo\":\"repository\",\"prefereGlobal\":\"preferGlobal\",\"hompage\":\"homepage\",\"hampage\":\"homepage\",\"autohr\":\"author\",\"autor\":\"author\",\"contributers\":\"contributors\",\"publicationConfig\":\"publishConfig\",\"script\":\"scripts\"},\"bugs\":{\"web\":\"url\",\"name\":\"url\"},\"script\":{\"server\":\"start\",\"tests\":\"test\"}}"); /***/ }), -/* 298 */ +/* 303 */ /***/ (function(module, exports, __webpack_require__) { var util = __webpack_require__(112) -var messages = __webpack_require__(299) +var messages = __webpack_require__(304) module.exports = function() { var args = Array.prototype.slice.call(arguments, 0) @@ -30907,20 +31074,20 @@ function makeTypoWarning (providedName, probableName, field) { /***/ }), -/* 299 */ +/* 304 */ /***/ (function(module) { module.exports = JSON.parse("{\"repositories\":\"'repositories' (plural) Not supported. Please pick one as the 'repository' field\",\"missingRepository\":\"No repository field.\",\"brokenGitUrl\":\"Probably broken git url: %s\",\"nonObjectScripts\":\"scripts must be an object\",\"nonStringScript\":\"script values must be string commands\",\"nonArrayFiles\":\"Invalid 'files' member\",\"invalidFilename\":\"Invalid filename in 'files' list: %s\",\"nonArrayBundleDependencies\":\"Invalid 'bundleDependencies' list. Must be array of package names\",\"nonStringBundleDependency\":\"Invalid bundleDependencies member: %s\",\"nonDependencyBundleDependency\":\"Non-dependency in bundleDependencies: %s\",\"nonObjectDependencies\":\"%s field must be an object\",\"nonStringDependency\":\"Invalid dependency: %s %s\",\"deprecatedArrayDependencies\":\"specifying %s as array is deprecated\",\"deprecatedModules\":\"modules field is deprecated\",\"nonArrayKeywords\":\"keywords should be an array of strings\",\"nonStringKeyword\":\"keywords should be an array of strings\",\"conflictingName\":\"%s is also the name of a node core module.\",\"nonStringDescription\":\"'description' field should be a string\",\"missingDescription\":\"No description\",\"missingReadme\":\"No README data\",\"missingLicense\":\"No license field.\",\"nonEmailUrlBugsString\":\"Bug string field must be url, email, or {email,url}\",\"nonUrlBugsUrlField\":\"bugs.url field must be a string url. Deleted.\",\"nonEmailBugsEmailField\":\"bugs.email field must be a string email. Deleted.\",\"emptyNormalizedBugs\":\"Normalized value of bugs field is an empty object. Deleted.\",\"nonUrlHomepage\":\"homepage field must be a string url. Deleted.\",\"invalidLicense\":\"license should be a valid SPDX license expression\",\"typo\":\"%s should probably be %s.\"}"); /***/ }), -/* 300 */ +/* 305 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const writeJsonFile = __webpack_require__(301); -const sortKeys = __webpack_require__(307); +const writeJsonFile = __webpack_require__(306); +const sortKeys = __webpack_require__(312); const dependencyKeys = new Set([ 'dependencies', @@ -30985,18 +31152,18 @@ module.exports.sync = (filePath, data, options) => { /***/ }), -/* 301 */ +/* 306 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); const fs = __webpack_require__(133); -const writeFileAtomic = __webpack_require__(302); -const sortKeys = __webpack_require__(307); -const makeDir = __webpack_require__(309); -const pify = __webpack_require__(310); -const detectIndent = __webpack_require__(312); +const writeFileAtomic = __webpack_require__(307); +const sortKeys = __webpack_require__(312); +const makeDir = __webpack_require__(314); +const pify = __webpack_require__(315); +const detectIndent = __webpack_require__(317); const init = (fn, filePath, data, options) => { if (!filePath) { @@ -31068,7 +31235,7 @@ module.exports.sync = (filePath, data, options) => { /***/ }), -/* 302 */ +/* 307 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -31079,8 +31246,8 @@ module.exports._getTmpname = getTmpname // for testing module.exports._cleanupOnExit = cleanupOnExit var fs = __webpack_require__(133) -var MurmurHash3 = __webpack_require__(303) -var onExit = __webpack_require__(304) +var MurmurHash3 = __webpack_require__(308) +var onExit = __webpack_require__(309) var path = __webpack_require__(4) var activeFiles = {} @@ -31088,7 +31255,7 @@ var activeFiles = {} /* istanbul ignore next */ var threadId = (function getId () { try { - var workerThreads = __webpack_require__(306) + var workerThreads = __webpack_require__(311) /// if we are in main thread, this is set to `0` return workerThreads.threadId @@ -31313,7 +31480,7 @@ function writeFileSync (filename, data, options) { /***/ }), -/* 303 */ +/* 308 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -31455,14 +31622,14 @@ function writeFileSync (filename, data, options) { /***/ }), -/* 304 */ +/* 309 */ /***/ (function(module, exports, __webpack_require__) { // Note: since nyc uses this module to output coverage, any lines // that are in the direct sync flow of nyc's outputCoverage are // ignored, since we can never get coverage for them. var assert = __webpack_require__(140) -var signals = __webpack_require__(305) +var signals = __webpack_require__(310) var EE = __webpack_require__(156) /* istanbul ignore if */ @@ -31618,7 +31785,7 @@ function processEmit (ev, arg) { /***/ }), -/* 305 */ +/* 310 */ /***/ (function(module, exports) { // This is not the set of all possible signals. @@ -31677,18 +31844,18 @@ if (process.platform === 'linux') { /***/ }), -/* 306 */ +/* 311 */ /***/ (function(module, exports) { module.exports = require(undefined); /***/ }), -/* 307 */ +/* 312 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const isPlainObj = __webpack_require__(308); +const isPlainObj = __webpack_require__(313); module.exports = (obj, opts) => { if (!isPlainObj(obj)) { @@ -31745,7 +31912,7 @@ module.exports = (obj, opts) => { /***/ }), -/* 308 */ +/* 313 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -31759,15 +31926,15 @@ module.exports = function (x) { /***/ }), -/* 309 */ +/* 314 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); const path = __webpack_require__(4); -const pify = __webpack_require__(310); -const semver = __webpack_require__(311); +const pify = __webpack_require__(315); +const semver = __webpack_require__(316); const defaults = { mode: 0o777 & (~process.umask()), @@ -31905,7 +32072,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 310 */ +/* 315 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -31980,7 +32147,7 @@ module.exports = (input, options) => { /***/ }), -/* 311 */ +/* 316 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -33469,7 +33636,7 @@ function coerce (version) { /***/ }), -/* 312 */ +/* 317 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -33598,7 +33765,7 @@ module.exports = str => { /***/ }), -/* 313 */ +/* 318 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -33606,7 +33773,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "installInDir", function() { return installInDir; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runScriptInPackage", function() { return runScriptInPackage; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runScriptInPackageStreaming", function() { return runScriptInPackageStreaming; }); -/* harmony import */ var _child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(314); +/* harmony import */ var _child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(319); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -33669,7 +33836,7 @@ function runScriptInPackageStreaming({ } /***/ }), -/* 314 */ +/* 319 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -33680,9 +33847,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var stream__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(stream__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(113); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(315); +/* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(320); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(execa__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(351); +/* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(356); /* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(246); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } @@ -33770,23 +33937,23 @@ function spawnStreaming(command, args, opts, { } /***/ }), -/* 315 */ +/* 320 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const childProcess = __webpack_require__(316); -const crossSpawn = __webpack_require__(317); -const stripFinalNewline = __webpack_require__(330); -const npmRunPath = __webpack_require__(331); -const onetime = __webpack_require__(333); -const makeError = __webpack_require__(335); -const normalizeStdio = __webpack_require__(340); -const {spawnedKill, spawnedCancel, setupTimeout, setExitHandler} = __webpack_require__(341); -const {handleInput, getSpawnedResult, makeAllStream, validateInputSync} = __webpack_require__(342); -const {mergePromise, getSpawnedPromise} = __webpack_require__(349); -const {joinCommand, parseCommand} = __webpack_require__(350); +const childProcess = __webpack_require__(321); +const crossSpawn = __webpack_require__(322); +const stripFinalNewline = __webpack_require__(335); +const npmRunPath = __webpack_require__(336); +const onetime = __webpack_require__(338); +const makeError = __webpack_require__(340); +const normalizeStdio = __webpack_require__(345); +const {spawnedKill, spawnedCancel, setupTimeout, setExitHandler} = __webpack_require__(346); +const {handleInput, getSpawnedResult, makeAllStream, validateInputSync} = __webpack_require__(347); +const {mergePromise, getSpawnedPromise} = __webpack_require__(354); +const {joinCommand, parseCommand} = __webpack_require__(355); const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100; @@ -34033,21 +34200,21 @@ module.exports.node = (scriptPath, args, options = {}) => { /***/ }), -/* 316 */ +/* 321 */ /***/ (function(module, exports) { module.exports = require("child_process"); /***/ }), -/* 317 */ +/* 322 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const cp = __webpack_require__(316); -const parse = __webpack_require__(318); -const enoent = __webpack_require__(329); +const cp = __webpack_require__(321); +const parse = __webpack_require__(323); +const enoent = __webpack_require__(334); function spawn(command, args, options) { // Parse the arguments @@ -34085,16 +34252,16 @@ module.exports._enoent = enoent; /***/ }), -/* 318 */ +/* 323 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const resolveCommand = __webpack_require__(319); -const escape = __webpack_require__(325); -const readShebang = __webpack_require__(326); +const resolveCommand = __webpack_require__(324); +const escape = __webpack_require__(330); +const readShebang = __webpack_require__(331); const isWin = process.platform === 'win32'; const isExecutableRegExp = /\.(?:com|exe)$/i; @@ -34183,15 +34350,15 @@ module.exports = parse; /***/ }), -/* 319 */ +/* 324 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const which = __webpack_require__(320); -const pathKey = __webpack_require__(324)(); +const which = __webpack_require__(325); +const pathKey = __webpack_require__(329)(); function resolveCommandAttempt(parsed, withoutPathExt) { const cwd = process.cwd(); @@ -34241,7 +34408,7 @@ module.exports = resolveCommand; /***/ }), -/* 320 */ +/* 325 */ /***/ (function(module, exports, __webpack_require__) { const isWindows = process.platform === 'win32' || @@ -34250,7 +34417,7 @@ const isWindows = process.platform === 'win32' || const path = __webpack_require__(4) const COLON = isWindows ? ';' : ':' -const isexe = __webpack_require__(321) +const isexe = __webpack_require__(326) const getNotFoundError = (cmd) => Object.assign(new Error(`not found: ${cmd}`), { code: 'ENOENT' }) @@ -34372,15 +34539,15 @@ which.sync = whichSync /***/ }), -/* 321 */ +/* 326 */ /***/ (function(module, exports, __webpack_require__) { var fs = __webpack_require__(134) var core if (process.platform === 'win32' || global.TESTING_WINDOWS) { - core = __webpack_require__(322) + core = __webpack_require__(327) } else { - core = __webpack_require__(323) + core = __webpack_require__(328) } module.exports = isexe @@ -34435,7 +34602,7 @@ function sync (path, options) { /***/ }), -/* 322 */ +/* 327 */ /***/ (function(module, exports, __webpack_require__) { module.exports = isexe @@ -34483,7 +34650,7 @@ function sync (path, options) { /***/ }), -/* 323 */ +/* 328 */ /***/ (function(module, exports, __webpack_require__) { module.exports = isexe @@ -34530,7 +34697,7 @@ function checkMode (stat, options) { /***/ }), -/* 324 */ +/* 329 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34553,7 +34720,7 @@ module.exports.default = pathKey; /***/ }), -/* 325 */ +/* 330 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34605,14 +34772,14 @@ module.exports.argument = escapeArgument; /***/ }), -/* 326 */ +/* 331 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const shebangCommand = __webpack_require__(327); +const shebangCommand = __webpack_require__(332); function readShebang(command) { // Read the first 150 bytes from the file @@ -34635,12 +34802,12 @@ module.exports = readShebang; /***/ }), -/* 327 */ +/* 332 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const shebangRegex = __webpack_require__(328); +const shebangRegex = __webpack_require__(333); module.exports = (string = '') => { const match = string.match(shebangRegex); @@ -34661,7 +34828,7 @@ module.exports = (string = '') => { /***/ }), -/* 328 */ +/* 333 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34670,7 +34837,7 @@ module.exports = /^#!(.*)/; /***/ }), -/* 329 */ +/* 334 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34736,7 +34903,7 @@ module.exports = { /***/ }), -/* 330 */ +/* 335 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34759,13 +34926,13 @@ module.exports = input => { /***/ }), -/* 331 */ +/* 336 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathKey = __webpack_require__(332); +const pathKey = __webpack_require__(337); const npmRunPath = options => { options = { @@ -34813,7 +34980,7 @@ module.exports.env = options => { /***/ }), -/* 332 */ +/* 337 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34836,12 +35003,12 @@ module.exports.default = pathKey; /***/ }), -/* 333 */ +/* 338 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const mimicFn = __webpack_require__(334); +const mimicFn = __webpack_require__(339); const calledFunctions = new WeakMap(); @@ -34893,7 +35060,7 @@ module.exports.callCount = fn => { /***/ }), -/* 334 */ +/* 339 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -34913,12 +35080,12 @@ module.exports.default = mimicFn; /***/ }), -/* 335 */ +/* 340 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const {signalsByName} = __webpack_require__(336); +const {signalsByName} = __webpack_require__(341); const getErrorPrefix = ({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}) => { if (timedOut) { @@ -35006,14 +35173,14 @@ module.exports = makeError; /***/ }), -/* 336 */ +/* 341 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports,"__esModule",{value:true});exports.signalsByNumber=exports.signalsByName=void 0;var _os=__webpack_require__(121); -var _signals=__webpack_require__(337); -var _realtime=__webpack_require__(339); +var _signals=__webpack_require__(342); +var _realtime=__webpack_require__(344); @@ -35083,14 +35250,14 @@ const signalsByNumber=getSignalsByNumber();exports.signalsByNumber=signalsByNumb //# sourceMappingURL=main.js.map /***/ }), -/* 337 */ +/* 342 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports,"__esModule",{value:true});exports.getSignals=void 0;var _os=__webpack_require__(121); -var _core=__webpack_require__(338); -var _realtime=__webpack_require__(339); +var _core=__webpack_require__(343); +var _realtime=__webpack_require__(344); @@ -35124,7 +35291,7 @@ return{name,number,description,supported,action,forced,standard}; //# sourceMappingURL=signals.js.map /***/ }), -/* 338 */ +/* 343 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -35403,7 +35570,7 @@ standard:"other"}];exports.SIGNALS=SIGNALS; //# sourceMappingURL=core.js.map /***/ }), -/* 339 */ +/* 344 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -35428,7 +35595,7 @@ const SIGRTMAX=64;exports.SIGRTMAX=SIGRTMAX; //# sourceMappingURL=realtime.js.map /***/ }), -/* 340 */ +/* 345 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -35487,13 +35654,13 @@ module.exports.node = opts => { /***/ }), -/* 341 */ +/* 346 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const os = __webpack_require__(121); -const onExit = __webpack_require__(304); +const onExit = __webpack_require__(309); const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5; @@ -35606,14 +35773,14 @@ module.exports = { /***/ }), -/* 342 */ +/* 347 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const isStream = __webpack_require__(343); -const getStream = __webpack_require__(344); -const mergeStream = __webpack_require__(348); +const isStream = __webpack_require__(348); +const getStream = __webpack_require__(349); +const mergeStream = __webpack_require__(353); // `input` option const handleInput = (spawned, input) => { @@ -35710,7 +35877,7 @@ module.exports = { /***/ }), -/* 343 */ +/* 348 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -35746,13 +35913,13 @@ module.exports = isStream; /***/ }), -/* 344 */ +/* 349 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pump = __webpack_require__(345); -const bufferStream = __webpack_require__(347); +const pump = __webpack_require__(350); +const bufferStream = __webpack_require__(352); class MaxBufferError extends Error { constructor() { @@ -35811,11 +35978,11 @@ module.exports.MaxBufferError = MaxBufferError; /***/ }), -/* 345 */ +/* 350 */ /***/ (function(module, exports, __webpack_require__) { var once = __webpack_require__(162) -var eos = __webpack_require__(346) +var eos = __webpack_require__(351) var fs = __webpack_require__(134) // we only need fs to get the ReadStream and WriteStream prototypes var noop = function () {} @@ -35899,7 +36066,7 @@ module.exports = pump /***/ }), -/* 346 */ +/* 351 */ /***/ (function(module, exports, __webpack_require__) { var once = __webpack_require__(162); @@ -35999,7 +36166,7 @@ module.exports = eos; /***/ }), -/* 347 */ +/* 352 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36058,7 +36225,7 @@ module.exports = options => { /***/ }), -/* 348 */ +/* 353 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36106,7 +36273,7 @@ module.exports = function (/*streams...*/) { /***/ }), -/* 349 */ +/* 354 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36159,7 +36326,7 @@ module.exports = { /***/ }), -/* 350 */ +/* 355 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36204,7 +36371,7 @@ module.exports = { /***/ }), -/* 351 */ +/* 356 */ /***/ (function(module, exports, __webpack_require__) { // Copyright IBM Corp. 2014,2018. All Rights Reserved. @@ -36212,12 +36379,12 @@ module.exports = { // This file is licensed under the Apache License 2.0. // License text available at https://opensource.org/licenses/Apache-2.0 -module.exports = __webpack_require__(352); -module.exports.cli = __webpack_require__(356); +module.exports = __webpack_require__(357); +module.exports.cli = __webpack_require__(361); /***/ }), -/* 352 */ +/* 357 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36232,9 +36399,9 @@ var stream = __webpack_require__(138); var util = __webpack_require__(112); var fs = __webpack_require__(134); -var through = __webpack_require__(353); -var duplexer = __webpack_require__(354); -var StringDecoder = __webpack_require__(355).StringDecoder; +var through = __webpack_require__(358); +var duplexer = __webpack_require__(359); +var StringDecoder = __webpack_require__(360).StringDecoder; module.exports = Logger; @@ -36423,7 +36590,7 @@ function lineMerger(host) { /***/ }), -/* 353 */ +/* 358 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(138) @@ -36537,7 +36704,7 @@ function through (write, end, opts) { /***/ }), -/* 354 */ +/* 359 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(138) @@ -36630,13 +36797,13 @@ function duplex(writer, reader) { /***/ }), -/* 355 */ +/* 360 */ /***/ (function(module, exports) { module.exports = require("string_decoder"); /***/ }), -/* 356 */ +/* 361 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36647,11 +36814,11 @@ module.exports = require("string_decoder"); -var minimist = __webpack_require__(357); +var minimist = __webpack_require__(362); var path = __webpack_require__(4); -var Logger = __webpack_require__(352); -var pkg = __webpack_require__(358); +var Logger = __webpack_require__(357); +var pkg = __webpack_require__(363); module.exports = cli; @@ -36705,7 +36872,7 @@ function usage($0, p) { /***/ }), -/* 357 */ +/* 362 */ /***/ (function(module, exports) { module.exports = function (args, opts) { @@ -36956,13 +37123,13 @@ function isNumber (x) { /***/ }), -/* 358 */ +/* 363 */ /***/ (function(module) { module.exports = JSON.parse("{\"name\":\"strong-log-transformer\",\"version\":\"2.1.0\",\"description\":\"Stream transformer that prefixes lines with timestamps and other things.\",\"author\":\"Ryan Graham \",\"license\":\"Apache-2.0\",\"repository\":{\"type\":\"git\",\"url\":\"git://github.com/strongloop/strong-log-transformer\"},\"keywords\":[\"logging\",\"streams\"],\"bugs\":{\"url\":\"https://github.com/strongloop/strong-log-transformer/issues\"},\"homepage\":\"https://github.com/strongloop/strong-log-transformer\",\"directories\":{\"test\":\"test\"},\"bin\":{\"sl-log-transformer\":\"bin/sl-log-transformer.js\"},\"main\":\"index.js\",\"scripts\":{\"test\":\"tap --100 test/test-*\"},\"dependencies\":{\"duplexer\":\"^0.1.1\",\"minimist\":\"^1.2.0\",\"through\":\"^2.3.4\"},\"devDependencies\":{\"tap\":\"^12.0.1\"},\"engines\":{\"node\":\">=4\"}}"); /***/ }), -/* 359 */ +/* 364 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -36970,13 +37137,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getAllChecksums", function() { return getAllChecksums; }); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(134); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(360); +/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(365); /* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(crypto__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(112); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(315); +/* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(320); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(execa__WEBPACK_IMPORTED_MODULE_3__); -/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(361); +/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(366); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -37175,20 +37342,20 @@ async function getAllChecksums(kbn, log, yarnLock) { } /***/ }), -/* 360 */ +/* 365 */ /***/ (function(module, exports) { module.exports = require("crypto"); /***/ }), -/* 361 */ +/* 366 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "readYarnLock", function() { return readYarnLock; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "resolveDepsForProject", function() { return resolveDepsForProject; }); -/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(362); +/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(367); /* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(131); /* @@ -37301,7 +37468,7 @@ function resolveDepsForProject({ } /***/ }), -/* 362 */ +/* 367 */ /***/ (function(module, exports, __webpack_require__) { module.exports = @@ -38860,7 +39027,7 @@ module.exports = invariant; /* 9 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(360); +module.exports = __webpack_require__(365); /***/ }), /* 10 */, @@ -41184,7 +41351,7 @@ function onceStrict (fn) { /* 63 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(363); +module.exports = __webpack_require__(368); /***/ }), /* 64 */, @@ -47579,13 +47746,13 @@ module.exports = process && support(supportLevel); /******/ ]); /***/ }), -/* 363 */ +/* 368 */ /***/ (function(module, exports) { module.exports = require("buffer"); /***/ }), -/* 364 */ +/* 369 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -47682,13 +47849,13 @@ class BootstrapCacheFile { } /***/ }), -/* 365 */ +/* 370 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "validateDependencies", function() { return validateDependencies; }); -/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(362); +/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(367); /* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var dedent__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2); /* harmony import */ var dedent__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(dedent__WEBPACK_IMPORTED_MODULE_1__); @@ -47699,7 +47866,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(131); /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(246); /* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(251); -/* harmony import */ var _projects_tree__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(366); +/* harmony import */ var _projects_tree__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(371); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -47891,7 +48058,7 @@ function getDevOnlyProductionDepsTree(kbn, projectName) { } /***/ }), -/* 366 */ +/* 371 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -48044,7 +48211,7 @@ function addProjectToTree(tree, pathParts, project) { } /***/ }), -/* 367 */ +/* 372 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -48052,7 +48219,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CleanCommand", function() { return CleanCommand; }); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(143); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(368); +/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(373); /* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(ora__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); @@ -48152,20 +48319,20 @@ const CleanCommand = { }; /***/ }), -/* 368 */ +/* 373 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readline = __webpack_require__(369); -const chalk = __webpack_require__(370); -const cliCursor = __webpack_require__(377); -const cliSpinners = __webpack_require__(379); -const logSymbols = __webpack_require__(381); -const stripAnsi = __webpack_require__(390); -const wcwidth = __webpack_require__(392); -const isInteractive = __webpack_require__(396); -const MuteStream = __webpack_require__(397); +const readline = __webpack_require__(374); +const chalk = __webpack_require__(375); +const cliCursor = __webpack_require__(382); +const cliSpinners = __webpack_require__(384); +const logSymbols = __webpack_require__(386); +const stripAnsi = __webpack_require__(395); +const wcwidth = __webpack_require__(397); +const isInteractive = __webpack_require__(401); +const MuteStream = __webpack_require__(402); const TEXT = Symbol('text'); const PREFIX_TEXT = Symbol('prefixText'); @@ -48518,23 +48685,23 @@ module.exports.promise = (action, options) => { /***/ }), -/* 369 */ +/* 374 */ /***/ (function(module, exports) { module.exports = require("readline"); /***/ }), -/* 370 */ +/* 375 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiStyles = __webpack_require__(371); +const ansiStyles = __webpack_require__(376); const {stdout: stdoutColor, stderr: stderrColor} = __webpack_require__(120); const { stringReplaceAll, stringEncaseCRLFWithFirstIndex -} = __webpack_require__(375); +} = __webpack_require__(380); // `supportsColor.level` → `ansiStyles.color[name]` mapping const levelMapping = [ @@ -48735,7 +48902,7 @@ const chalkTag = (chalk, ...strings) => { } if (template === undefined) { - template = __webpack_require__(376); + template = __webpack_require__(381); } return template(chalk, parts.join('')); @@ -48764,7 +48931,7 @@ module.exports = chalk; /***/ }), -/* 371 */ +/* 376 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -48810,7 +48977,7 @@ const setLazyProperty = (object, property, get) => { let colorConvert; const makeDynamicStyles = (wrap, targetSpace, identity, isBackground) => { if (colorConvert === undefined) { - colorConvert = __webpack_require__(372); + colorConvert = __webpack_require__(377); } const offset = isBackground ? 10 : 0; @@ -48935,11 +49102,11 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(115)(module))) /***/ }), -/* 372 */ +/* 377 */ /***/ (function(module, exports, __webpack_require__) { -const conversions = __webpack_require__(373); -const route = __webpack_require__(374); +const conversions = __webpack_require__(378); +const route = __webpack_require__(379); const convert = {}; @@ -49022,7 +49189,7 @@ module.exports = convert; /***/ }), -/* 373 */ +/* 378 */ /***/ (function(module, exports, __webpack_require__) { /* MIT license */ @@ -49867,10 +50034,10 @@ convert.rgb.gray = function (rgb) { /***/ }), -/* 374 */ +/* 379 */ /***/ (function(module, exports, __webpack_require__) { -const conversions = __webpack_require__(373); +const conversions = __webpack_require__(378); /* This function routes a model to all other models. @@ -49970,7 +50137,7 @@ module.exports = function (fromModel) { /***/ }), -/* 375 */ +/* 380 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50016,7 +50183,7 @@ module.exports = { /***/ }), -/* 376 */ +/* 381 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50157,12 +50324,12 @@ module.exports = (chalk, temporary) => { /***/ }), -/* 377 */ +/* 382 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const restoreCursor = __webpack_require__(378); +const restoreCursor = __webpack_require__(383); let isHidden = false; @@ -50199,13 +50366,13 @@ exports.toggle = (force, writableStream) => { /***/ }), -/* 378 */ +/* 383 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const onetime = __webpack_require__(333); -const signalExit = __webpack_require__(304); +const onetime = __webpack_require__(338); +const signalExit = __webpack_require__(309); module.exports = onetime(() => { signalExit(() => { @@ -50215,13 +50382,13 @@ module.exports = onetime(() => { /***/ }), -/* 379 */ +/* 384 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const spinners = Object.assign({}, __webpack_require__(380)); +const spinners = Object.assign({}, __webpack_require__(385)); const spinnersList = Object.keys(spinners); @@ -50239,18 +50406,18 @@ module.exports.default = spinners; /***/ }), -/* 380 */ +/* 385 */ /***/ (function(module) { module.exports = JSON.parse("{\"dots\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠹\",\"⠸\",\"⠼\",\"⠴\",\"⠦\",\"⠧\",\"⠇\",\"⠏\"]},\"dots2\":{\"interval\":80,\"frames\":[\"⣾\",\"⣽\",\"⣻\",\"⢿\",\"⡿\",\"⣟\",\"⣯\",\"⣷\"]},\"dots3\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠞\",\"⠖\",\"⠦\",\"⠴\",\"⠲\",\"⠳\",\"⠓\"]},\"dots4\":{\"interval\":80,\"frames\":[\"⠄\",\"⠆\",\"⠇\",\"⠋\",\"⠙\",\"⠸\",\"⠰\",\"⠠\",\"⠰\",\"⠸\",\"⠙\",\"⠋\",\"⠇\",\"⠆\"]},\"dots5\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\"]},\"dots6\":{\"interval\":80,\"frames\":[\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠴\",\"⠲\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠚\",\"⠙\",\"⠉\",\"⠁\"]},\"dots7\":{\"interval\":80,\"frames\":[\"⠈\",\"⠉\",\"⠋\",\"⠓\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠖\",\"⠦\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\"]},\"dots8\":{\"interval\":80,\"frames\":[\"⠁\",\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\",\"⠈\"]},\"dots9\":{\"interval\":80,\"frames\":[\"⢹\",\"⢺\",\"⢼\",\"⣸\",\"⣇\",\"⡧\",\"⡗\",\"⡏\"]},\"dots10\":{\"interval\":80,\"frames\":[\"⢄\",\"⢂\",\"⢁\",\"⡁\",\"⡈\",\"⡐\",\"⡠\"]},\"dots11\":{\"interval\":100,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⡀\",\"⢀\",\"⠠\",\"⠐\",\"⠈\"]},\"dots12\":{\"interval\":80,\"frames\":[\"⢀⠀\",\"⡀⠀\",\"⠄⠀\",\"⢂⠀\",\"⡂⠀\",\"⠅⠀\",\"⢃⠀\",\"⡃⠀\",\"⠍⠀\",\"⢋⠀\",\"⡋⠀\",\"⠍⠁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⢈⠩\",\"⡀⢙\",\"⠄⡙\",\"⢂⠩\",\"⡂⢘\",\"⠅⡘\",\"⢃⠨\",\"⡃⢐\",\"⠍⡐\",\"⢋⠠\",\"⡋⢀\",\"⠍⡁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⠈⠩\",\"⠀⢙\",\"⠀⡙\",\"⠀⠩\",\"⠀⢘\",\"⠀⡘\",\"⠀⠨\",\"⠀⢐\",\"⠀⡐\",\"⠀⠠\",\"⠀⢀\",\"⠀⡀\"]},\"dots8Bit\":{\"interval\":80,\"frames\":[\"⠀\",\"⠁\",\"⠂\",\"⠃\",\"⠄\",\"⠅\",\"⠆\",\"⠇\",\"⡀\",\"⡁\",\"⡂\",\"⡃\",\"⡄\",\"⡅\",\"⡆\",\"⡇\",\"⠈\",\"⠉\",\"⠊\",\"⠋\",\"⠌\",\"⠍\",\"⠎\",\"⠏\",\"⡈\",\"⡉\",\"⡊\",\"⡋\",\"⡌\",\"⡍\",\"⡎\",\"⡏\",\"⠐\",\"⠑\",\"⠒\",\"⠓\",\"⠔\",\"⠕\",\"⠖\",\"⠗\",\"⡐\",\"⡑\",\"⡒\",\"⡓\",\"⡔\",\"⡕\",\"⡖\",\"⡗\",\"⠘\",\"⠙\",\"⠚\",\"⠛\",\"⠜\",\"⠝\",\"⠞\",\"⠟\",\"⡘\",\"⡙\",\"⡚\",\"⡛\",\"⡜\",\"⡝\",\"⡞\",\"⡟\",\"⠠\",\"⠡\",\"⠢\",\"⠣\",\"⠤\",\"⠥\",\"⠦\",\"⠧\",\"⡠\",\"⡡\",\"⡢\",\"⡣\",\"⡤\",\"⡥\",\"⡦\",\"⡧\",\"⠨\",\"⠩\",\"⠪\",\"⠫\",\"⠬\",\"⠭\",\"⠮\",\"⠯\",\"⡨\",\"⡩\",\"⡪\",\"⡫\",\"⡬\",\"⡭\",\"⡮\",\"⡯\",\"⠰\",\"⠱\",\"⠲\",\"⠳\",\"⠴\",\"⠵\",\"⠶\",\"⠷\",\"⡰\",\"⡱\",\"⡲\",\"⡳\",\"⡴\",\"⡵\",\"⡶\",\"⡷\",\"⠸\",\"⠹\",\"⠺\",\"⠻\",\"⠼\",\"⠽\",\"⠾\",\"⠿\",\"⡸\",\"⡹\",\"⡺\",\"⡻\",\"⡼\",\"⡽\",\"⡾\",\"⡿\",\"⢀\",\"⢁\",\"⢂\",\"⢃\",\"⢄\",\"⢅\",\"⢆\",\"⢇\",\"⣀\",\"⣁\",\"⣂\",\"⣃\",\"⣄\",\"⣅\",\"⣆\",\"⣇\",\"⢈\",\"⢉\",\"⢊\",\"⢋\",\"⢌\",\"⢍\",\"⢎\",\"⢏\",\"⣈\",\"⣉\",\"⣊\",\"⣋\",\"⣌\",\"⣍\",\"⣎\",\"⣏\",\"⢐\",\"⢑\",\"⢒\",\"⢓\",\"⢔\",\"⢕\",\"⢖\",\"⢗\",\"⣐\",\"⣑\",\"⣒\",\"⣓\",\"⣔\",\"⣕\",\"⣖\",\"⣗\",\"⢘\",\"⢙\",\"⢚\",\"⢛\",\"⢜\",\"⢝\",\"⢞\",\"⢟\",\"⣘\",\"⣙\",\"⣚\",\"⣛\",\"⣜\",\"⣝\",\"⣞\",\"⣟\",\"⢠\",\"⢡\",\"⢢\",\"⢣\",\"⢤\",\"⢥\",\"⢦\",\"⢧\",\"⣠\",\"⣡\",\"⣢\",\"⣣\",\"⣤\",\"⣥\",\"⣦\",\"⣧\",\"⢨\",\"⢩\",\"⢪\",\"⢫\",\"⢬\",\"⢭\",\"⢮\",\"⢯\",\"⣨\",\"⣩\",\"⣪\",\"⣫\",\"⣬\",\"⣭\",\"⣮\",\"⣯\",\"⢰\",\"⢱\",\"⢲\",\"⢳\",\"⢴\",\"⢵\",\"⢶\",\"⢷\",\"⣰\",\"⣱\",\"⣲\",\"⣳\",\"⣴\",\"⣵\",\"⣶\",\"⣷\",\"⢸\",\"⢹\",\"⢺\",\"⢻\",\"⢼\",\"⢽\",\"⢾\",\"⢿\",\"⣸\",\"⣹\",\"⣺\",\"⣻\",\"⣼\",\"⣽\",\"⣾\",\"⣿\"]},\"line\":{\"interval\":130,\"frames\":[\"-\",\"\\\\\",\"|\",\"/\"]},\"line2\":{\"interval\":100,\"frames\":[\"⠂\",\"-\",\"–\",\"—\",\"–\",\"-\"]},\"pipe\":{\"interval\":100,\"frames\":[\"┤\",\"┘\",\"┴\",\"└\",\"├\",\"┌\",\"┬\",\"┐\"]},\"simpleDots\":{\"interval\":400,\"frames\":[\". \",\".. \",\"...\",\" \"]},\"simpleDotsScrolling\":{\"interval\":200,\"frames\":[\". \",\".. \",\"...\",\" ..\",\" .\",\" \"]},\"star\":{\"interval\":70,\"frames\":[\"✶\",\"✸\",\"✹\",\"✺\",\"✹\",\"✷\"]},\"star2\":{\"interval\":80,\"frames\":[\"+\",\"x\",\"*\"]},\"flip\":{\"interval\":70,\"frames\":[\"_\",\"_\",\"_\",\"-\",\"`\",\"`\",\"'\",\"´\",\"-\",\"_\",\"_\",\"_\"]},\"hamburger\":{\"interval\":100,\"frames\":[\"☱\",\"☲\",\"☴\"]},\"growVertical\":{\"interval\":120,\"frames\":[\"▁\",\"▃\",\"▄\",\"▅\",\"▆\",\"▇\",\"▆\",\"▅\",\"▄\",\"▃\"]},\"growHorizontal\":{\"interval\":120,\"frames\":[\"▏\",\"▎\",\"▍\",\"▌\",\"▋\",\"▊\",\"▉\",\"▊\",\"▋\",\"▌\",\"▍\",\"▎\"]},\"balloon\":{\"interval\":140,\"frames\":[\" \",\".\",\"o\",\"O\",\"@\",\"*\",\" \"]},\"balloon2\":{\"interval\":120,\"frames\":[\".\",\"o\",\"O\",\"°\",\"O\",\"o\",\".\"]},\"noise\":{\"interval\":100,\"frames\":[\"▓\",\"▒\",\"░\"]},\"bounce\":{\"interval\":120,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⠂\"]},\"boxBounce\":{\"interval\":120,\"frames\":[\"▖\",\"▘\",\"▝\",\"▗\"]},\"boxBounce2\":{\"interval\":100,\"frames\":[\"▌\",\"▀\",\"▐\",\"▄\"]},\"triangle\":{\"interval\":50,\"frames\":[\"◢\",\"◣\",\"◤\",\"◥\"]},\"arc\":{\"interval\":100,\"frames\":[\"◜\",\"◠\",\"◝\",\"◞\",\"◡\",\"◟\"]},\"circle\":{\"interval\":120,\"frames\":[\"◡\",\"⊙\",\"◠\"]},\"squareCorners\":{\"interval\":180,\"frames\":[\"◰\",\"◳\",\"◲\",\"◱\"]},\"circleQuarters\":{\"interval\":120,\"frames\":[\"◴\",\"◷\",\"◶\",\"◵\"]},\"circleHalves\":{\"interval\":50,\"frames\":[\"◐\",\"◓\",\"◑\",\"◒\"]},\"squish\":{\"interval\":100,\"frames\":[\"╫\",\"╪\"]},\"toggle\":{\"interval\":250,\"frames\":[\"⊶\",\"⊷\"]},\"toggle2\":{\"interval\":80,\"frames\":[\"▫\",\"▪\"]},\"toggle3\":{\"interval\":120,\"frames\":[\"□\",\"■\"]},\"toggle4\":{\"interval\":100,\"frames\":[\"■\",\"□\",\"▪\",\"▫\"]},\"toggle5\":{\"interval\":100,\"frames\":[\"▮\",\"▯\"]},\"toggle6\":{\"interval\":300,\"frames\":[\"ဝ\",\"၀\"]},\"toggle7\":{\"interval\":80,\"frames\":[\"⦾\",\"⦿\"]},\"toggle8\":{\"interval\":100,\"frames\":[\"◍\",\"◌\"]},\"toggle9\":{\"interval\":100,\"frames\":[\"◉\",\"◎\"]},\"toggle10\":{\"interval\":100,\"frames\":[\"㊂\",\"㊀\",\"㊁\"]},\"toggle11\":{\"interval\":50,\"frames\":[\"⧇\",\"⧆\"]},\"toggle12\":{\"interval\":120,\"frames\":[\"☗\",\"☖\"]},\"toggle13\":{\"interval\":80,\"frames\":[\"=\",\"*\",\"-\"]},\"arrow\":{\"interval\":100,\"frames\":[\"←\",\"↖\",\"↑\",\"↗\",\"→\",\"↘\",\"↓\",\"↙\"]},\"arrow2\":{\"interval\":80,\"frames\":[\"⬆️ \",\"↗️ \",\"➡️ \",\"↘️ \",\"⬇️ \",\"↙️ \",\"⬅️ \",\"↖️ \"]},\"arrow3\":{\"interval\":120,\"frames\":[\"▹▹▹▹▹\",\"▸▹▹▹▹\",\"▹▸▹▹▹\",\"▹▹▸▹▹\",\"▹▹▹▸▹\",\"▹▹▹▹▸\"]},\"bouncingBar\":{\"interval\":80,\"frames\":[\"[ ]\",\"[= ]\",\"[== ]\",\"[=== ]\",\"[ ===]\",\"[ ==]\",\"[ =]\",\"[ ]\",\"[ =]\",\"[ ==]\",\"[ ===]\",\"[====]\",\"[=== ]\",\"[== ]\",\"[= ]\"]},\"bouncingBall\":{\"interval\":80,\"frames\":[\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ●)\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"(● )\"]},\"smiley\":{\"interval\":200,\"frames\":[\"😄 \",\"😝 \"]},\"monkey\":{\"interval\":300,\"frames\":[\"🙈 \",\"🙈 \",\"🙉 \",\"🙊 \"]},\"hearts\":{\"interval\":100,\"frames\":[\"💛 \",\"💙 \",\"💜 \",\"💚 \",\"❤️ \"]},\"clock\":{\"interval\":100,\"frames\":[\"🕛 \",\"🕐 \",\"🕑 \",\"🕒 \",\"🕓 \",\"🕔 \",\"🕕 \",\"🕖 \",\"🕗 \",\"🕘 \",\"🕙 \",\"🕚 \"]},\"earth\":{\"interval\":180,\"frames\":[\"🌍 \",\"🌎 \",\"🌏 \"]},\"material\":{\"interval\":17,\"frames\":[\"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"███████▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"████████▁▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"██████████▁▁▁▁▁▁▁▁▁▁\",\"███████████▁▁▁▁▁▁▁▁▁\",\"█████████████▁▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁▁██████████████▁▁▁▁\",\"▁▁▁██████████████▁▁▁\",\"▁▁▁▁█████████████▁▁▁\",\"▁▁▁▁██████████████▁▁\",\"▁▁▁▁██████████████▁▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁▁██████████████\",\"▁▁▁▁▁▁██████████████\",\"▁▁▁▁▁▁▁█████████████\",\"▁▁▁▁▁▁▁█████████████\",\"▁▁▁▁▁▁▁▁████████████\",\"▁▁▁▁▁▁▁▁████████████\",\"▁▁▁▁▁▁▁▁▁███████████\",\"▁▁▁▁▁▁▁▁▁███████████\",\"▁▁▁▁▁▁▁▁▁▁██████████\",\"▁▁▁▁▁▁▁▁▁▁██████████\",\"▁▁▁▁▁▁▁▁▁▁▁▁████████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁██████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"████████▁▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"███████████▁▁▁▁▁▁▁▁▁\",\"████████████▁▁▁▁▁▁▁▁\",\"████████████▁▁▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁▁▁█████████████▁▁▁▁\",\"▁▁▁▁▁████████████▁▁▁\",\"▁▁▁▁▁████████████▁▁▁\",\"▁▁▁▁▁▁███████████▁▁▁\",\"▁▁▁▁▁▁▁▁█████████▁▁▁\",\"▁▁▁▁▁▁▁▁█████████▁▁▁\",\"▁▁▁▁▁▁▁▁▁█████████▁▁\",\"▁▁▁▁▁▁▁▁▁█████████▁▁\",\"▁▁▁▁▁▁▁▁▁▁█████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁███████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁███████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\"]},\"moon\":{\"interval\":80,\"frames\":[\"🌑 \",\"🌒 \",\"🌓 \",\"🌔 \",\"🌕 \",\"🌖 \",\"🌗 \",\"🌘 \"]},\"runner\":{\"interval\":140,\"frames\":[\"🚶 \",\"🏃 \"]},\"pong\":{\"interval\":80,\"frames\":[\"▐⠂ ▌\",\"▐⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂▌\",\"▐ ⠠▌\",\"▐ ⡀▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐⠠ ▌\"]},\"shark\":{\"interval\":120,\"frames\":[\"▐|\\\\____________▌\",\"▐_|\\\\___________▌\",\"▐__|\\\\__________▌\",\"▐___|\\\\_________▌\",\"▐____|\\\\________▌\",\"▐_____|\\\\_______▌\",\"▐______|\\\\______▌\",\"▐_______|\\\\_____▌\",\"▐________|\\\\____▌\",\"▐_________|\\\\___▌\",\"▐__________|\\\\__▌\",\"▐___________|\\\\_▌\",\"▐____________|\\\\▌\",\"▐____________/|▌\",\"▐___________/|_▌\",\"▐__________/|__▌\",\"▐_________/|___▌\",\"▐________/|____▌\",\"▐_______/|_____▌\",\"▐______/|______▌\",\"▐_____/|_______▌\",\"▐____/|________▌\",\"▐___/|_________▌\",\"▐__/|__________▌\",\"▐_/|___________▌\",\"▐/|____________▌\"]},\"dqpb\":{\"interval\":100,\"frames\":[\"d\",\"q\",\"p\",\"b\"]},\"weather\":{\"interval\":100,\"frames\":[\"☀️ \",\"☀️ \",\"☀️ \",\"🌤 \",\"⛅️ \",\"🌥 \",\"☁️ \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"⛈ \",\"🌨 \",\"🌧 \",\"🌨 \",\"☁️ \",\"🌥 \",\"⛅️ \",\"🌤 \",\"☀️ \",\"☀️ \"]},\"christmas\":{\"interval\":400,\"frames\":[\"🌲\",\"🎄\"]},\"grenade\":{\"interval\":80,\"frames\":[\"، \",\"′ \",\" ´ \",\" ‾ \",\" ⸌\",\" ⸊\",\" |\",\" ⁎\",\" ⁕\",\" ෴ \",\" ⁓\",\" \",\" \",\" \"]},\"point\":{\"interval\":125,\"frames\":[\"∙∙∙\",\"●∙∙\",\"∙●∙\",\"∙∙●\",\"∙∙∙\"]},\"layer\":{\"interval\":150,\"frames\":[\"-\",\"=\",\"≡\"]},\"betaWave\":{\"interval\":80,\"frames\":[\"ρββββββ\",\"βρβββββ\",\"ββρββββ\",\"βββρβββ\",\"ββββρββ\",\"βββββρβ\",\"ββββββρ\"]}}"); /***/ }), -/* 381 */ +/* 386 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const chalk = __webpack_require__(382); +const chalk = __webpack_require__(387); const isSupported = process.platform !== 'win32' || process.env.CI || process.env.TERM === 'xterm-256color'; @@ -50272,16 +50439,16 @@ module.exports = isSupported ? main : fallbacks; /***/ }), -/* 382 */ +/* 387 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(265); -const ansiStyles = __webpack_require__(383); -const stdoutColor = __webpack_require__(388).stdout; +const ansiStyles = __webpack_require__(388); +const stdoutColor = __webpack_require__(393).stdout; -const template = __webpack_require__(389); +const template = __webpack_require__(394); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -50507,12 +50674,12 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 383 */ +/* 388 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /* WEBPACK VAR INJECTION */(function(module) { -const colorConvert = __webpack_require__(384); +const colorConvert = __webpack_require__(389); const wrapAnsi16 = (fn, offset) => function () { const code = fn.apply(colorConvert, arguments); @@ -50680,11 +50847,11 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(115)(module))) /***/ }), -/* 384 */ +/* 389 */ /***/ (function(module, exports, __webpack_require__) { -var conversions = __webpack_require__(385); -var route = __webpack_require__(387); +var conversions = __webpack_require__(390); +var route = __webpack_require__(392); var convert = {}; @@ -50764,11 +50931,11 @@ module.exports = convert; /***/ }), -/* 385 */ +/* 390 */ /***/ (function(module, exports, __webpack_require__) { /* MIT license */ -var cssKeywords = __webpack_require__(386); +var cssKeywords = __webpack_require__(391); // NOTE: conversions should only return primitive values (i.e. arrays, or // values that give correct `typeof` results). @@ -51638,7 +51805,7 @@ convert.rgb.gray = function (rgb) { /***/ }), -/* 386 */ +/* 391 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -51797,10 +51964,10 @@ module.exports = { /***/ }), -/* 387 */ +/* 392 */ /***/ (function(module, exports, __webpack_require__) { -var conversions = __webpack_require__(385); +var conversions = __webpack_require__(390); /* this function routes a model to all other models. @@ -51900,7 +52067,7 @@ module.exports = function (fromModel) { /***/ }), -/* 388 */ +/* 393 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52038,7 +52205,7 @@ module.exports = { /***/ }), -/* 389 */ +/* 394 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52173,18 +52340,18 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 390 */ +/* 395 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiRegex = __webpack_require__(391); +const ansiRegex = __webpack_require__(396); module.exports = string => typeof string === 'string' ? string.replace(ansiRegex(), '') : string; /***/ }), -/* 391 */ +/* 396 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52201,14 +52368,14 @@ module.exports = ({onlyFirst = false} = {}) => { /***/ }), -/* 392 */ +/* 397 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var defaults = __webpack_require__(393) -var combining = __webpack_require__(395) +var defaults = __webpack_require__(398) +var combining = __webpack_require__(400) var DEFAULTS = { nul: 0, @@ -52307,10 +52474,10 @@ function bisearch(ucs) { /***/ }), -/* 393 */ +/* 398 */ /***/ (function(module, exports, __webpack_require__) { -var clone = __webpack_require__(394); +var clone = __webpack_require__(399); module.exports = function(options, defaults) { options = options || {}; @@ -52325,7 +52492,7 @@ module.exports = function(options, defaults) { }; /***/ }), -/* 394 */ +/* 399 */ /***/ (function(module, exports, __webpack_require__) { var clone = (function() { @@ -52497,7 +52664,7 @@ if ( true && module.exports) { /***/ }), -/* 395 */ +/* 400 */ /***/ (function(module, exports) { module.exports = [ @@ -52553,7 +52720,7 @@ module.exports = [ /***/ }), -/* 396 */ +/* 401 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52569,7 +52736,7 @@ module.exports = ({stream = process.stdout} = {}) => { /***/ }), -/* 397 */ +/* 402 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(138) @@ -52720,7 +52887,7 @@ MuteStream.prototype.close = proxy('close') /***/ }), -/* 398 */ +/* 403 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52781,7 +52948,7 @@ const RunCommand = { }; /***/ }), -/* 399 */ +/* 404 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52791,7 +52958,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(246); /* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(247); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(248); -/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(400); +/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(405); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -52877,14 +53044,14 @@ const WatchCommand = { }; /***/ }), -/* 400 */ +/* 405 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "waitUntilWatchIsReady", function() { return waitUntilWatchIsReady; }); /* harmony import */ var rxjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(8); -/* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(401); +/* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(406); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -52951,141 +53118,141 @@ function waitUntilWatchIsReady(stream, opts = {}) { } /***/ }), -/* 401 */ +/* 406 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(402); +/* harmony import */ var _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(407); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "audit", function() { return _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__["audit"]; }); -/* harmony import */ var _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(403); +/* harmony import */ var _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(408); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__["auditTime"]; }); -/* harmony import */ var _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(404); +/* harmony import */ var _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(409); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buffer", function() { return _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__["buffer"]; }); -/* harmony import */ var _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(405); +/* harmony import */ var _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(410); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferCount", function() { return _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__["bufferCount"]; }); -/* harmony import */ var _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(406); +/* harmony import */ var _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(411); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferTime", function() { return _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__["bufferTime"]; }); -/* harmony import */ var _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(407); +/* harmony import */ var _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(412); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferToggle", function() { return _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__["bufferToggle"]; }); -/* harmony import */ var _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(408); +/* harmony import */ var _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(413); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferWhen", function() { return _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__["bufferWhen"]; }); -/* harmony import */ var _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(409); +/* harmony import */ var _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(414); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "catchError", function() { return _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__["catchError"]; }); -/* harmony import */ var _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(410); +/* harmony import */ var _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(415); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineAll", function() { return _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__["combineAll"]; }); -/* harmony import */ var _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(411); +/* harmony import */ var _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(416); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineLatest", function() { return _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__["combineLatest"]; }); -/* harmony import */ var _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(412); +/* harmony import */ var _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(417); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concat", function() { return _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__["concat"]; }); /* harmony import */ var _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(80); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatAll", function() { return _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__["concatAll"]; }); -/* harmony import */ var _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(413); +/* harmony import */ var _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(418); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMap", function() { return _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__["concatMap"]; }); -/* harmony import */ var _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(414); +/* harmony import */ var _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(419); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__["concatMapTo"]; }); -/* harmony import */ var _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(415); +/* harmony import */ var _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(420); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "count", function() { return _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__["count"]; }); -/* harmony import */ var _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(416); +/* harmony import */ var _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(421); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounce", function() { return _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__["debounce"]; }); -/* harmony import */ var _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(417); +/* harmony import */ var _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(422); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounceTime", function() { return _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__["debounceTime"]; }); -/* harmony import */ var _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(418); +/* harmony import */ var _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(423); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "defaultIfEmpty", function() { return _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__["defaultIfEmpty"]; }); -/* harmony import */ var _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(419); +/* harmony import */ var _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(424); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__["delay"]; }); -/* harmony import */ var _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(421); +/* harmony import */ var _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(426); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delayWhen", function() { return _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__["delayWhen"]; }); -/* harmony import */ var _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(422); +/* harmony import */ var _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(427); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "dematerialize", function() { return _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__["dematerialize"]; }); -/* harmony import */ var _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(423); +/* harmony import */ var _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(428); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinct", function() { return _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__["distinct"]; }); -/* harmony import */ var _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(424); +/* harmony import */ var _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(429); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilChanged", function() { return _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__["distinctUntilChanged"]; }); -/* harmony import */ var _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(425); +/* harmony import */ var _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(430); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__["distinctUntilKeyChanged"]; }); -/* harmony import */ var _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(426); +/* harmony import */ var _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(431); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__["elementAt"]; }); -/* harmony import */ var _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(429); +/* harmony import */ var _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(434); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "endWith", function() { return _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__["endWith"]; }); -/* harmony import */ var _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(430); +/* harmony import */ var _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(435); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "every", function() { return _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__["every"]; }); -/* harmony import */ var _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(431); +/* harmony import */ var _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(436); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaust", function() { return _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__["exhaust"]; }); -/* harmony import */ var _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(432); +/* harmony import */ var _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(437); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaustMap", function() { return _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__["exhaustMap"]; }); -/* harmony import */ var _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(433); +/* harmony import */ var _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(438); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "expand", function() { return _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__["expand"]; }); /* harmony import */ var _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(105); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "filter", function() { return _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__["filter"]; }); -/* harmony import */ var _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(434); +/* harmony import */ var _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(439); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "finalize", function() { return _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__["finalize"]; }); -/* harmony import */ var _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(435); +/* harmony import */ var _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(440); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "find", function() { return _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__["find"]; }); -/* harmony import */ var _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(436); +/* harmony import */ var _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(441); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__["findIndex"]; }); -/* harmony import */ var _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(437); +/* harmony import */ var _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(442); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "first", function() { return _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__["first"]; }); /* harmony import */ var _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(31); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "groupBy", function() { return _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__["groupBy"]; }); -/* harmony import */ var _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(438); +/* harmony import */ var _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(443); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ignoreElements", function() { return _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__["ignoreElements"]; }); -/* harmony import */ var _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(439); +/* harmony import */ var _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(444); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "isEmpty", function() { return _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__["isEmpty"]; }); -/* harmony import */ var _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(440); +/* harmony import */ var _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(445); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "last", function() { return _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__["last"]; }); /* harmony import */ var _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(66); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "map", function() { return _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__["map"]; }); -/* harmony import */ var _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(442); +/* harmony import */ var _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(447); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mapTo", function() { return _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__["mapTo"]; }); -/* harmony import */ var _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(443); +/* harmony import */ var _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(448); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "materialize", function() { return _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__["materialize"]; }); -/* harmony import */ var _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(444); +/* harmony import */ var _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(449); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "max", function() { return _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__["max"]; }); -/* harmony import */ var _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(447); +/* harmony import */ var _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(452); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__["merge"]; }); /* harmony import */ var _internal_operators_mergeAll__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(81); @@ -53096,175 +53263,175 @@ __webpack_require__.r(__webpack_exports__); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "flatMap", function() { return _internal_operators_mergeMap__WEBPACK_IMPORTED_MODULE_45__["flatMap"]; }); -/* harmony import */ var _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(448); +/* harmony import */ var _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(453); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeMapTo", function() { return _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__["mergeMapTo"]; }); -/* harmony import */ var _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(449); +/* harmony import */ var _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(454); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeScan", function() { return _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__["mergeScan"]; }); -/* harmony import */ var _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(450); +/* harmony import */ var _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(455); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "min", function() { return _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__["min"]; }); -/* harmony import */ var _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(451); +/* harmony import */ var _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(456); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "multicast", function() { return _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__["multicast"]; }); /* harmony import */ var _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(41); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "observeOn", function() { return _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__["observeOn"]; }); -/* harmony import */ var _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(452); +/* harmony import */ var _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(457); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__["onErrorResumeNext"]; }); -/* harmony import */ var _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(453); +/* harmony import */ var _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(458); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pairwise", function() { return _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__["pairwise"]; }); -/* harmony import */ var _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(454); +/* harmony import */ var _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(459); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "partition", function() { return _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__["partition"]; }); -/* harmony import */ var _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(455); +/* harmony import */ var _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(460); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pluck", function() { return _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__["pluck"]; }); -/* harmony import */ var _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(456); +/* harmony import */ var _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(461); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__["publish"]; }); -/* harmony import */ var _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(457); +/* harmony import */ var _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(462); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__["publishBehavior"]; }); -/* harmony import */ var _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(458); +/* harmony import */ var _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(463); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__["publishLast"]; }); -/* harmony import */ var _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(459); +/* harmony import */ var _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(464); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__["publishReplay"]; }); -/* harmony import */ var _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__ = __webpack_require__(460); +/* harmony import */ var _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__ = __webpack_require__(465); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "race", function() { return _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__["race"]; }); -/* harmony import */ var _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__ = __webpack_require__(445); +/* harmony import */ var _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__ = __webpack_require__(450); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__["reduce"]; }); -/* harmony import */ var _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__ = __webpack_require__(461); +/* harmony import */ var _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__ = __webpack_require__(466); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeat", function() { return _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__["repeat"]; }); -/* harmony import */ var _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__ = __webpack_require__(462); +/* harmony import */ var _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__ = __webpack_require__(467); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeatWhen", function() { return _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__["repeatWhen"]; }); -/* harmony import */ var _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__ = __webpack_require__(463); +/* harmony import */ var _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__ = __webpack_require__(468); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retry", function() { return _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__["retry"]; }); -/* harmony import */ var _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__ = __webpack_require__(464); +/* harmony import */ var _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__ = __webpack_require__(469); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retryWhen", function() { return _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__["retryWhen"]; }); /* harmony import */ var _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__ = __webpack_require__(30); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "refCount", function() { return _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__["refCount"]; }); -/* harmony import */ var _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__ = __webpack_require__(465); +/* harmony import */ var _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__ = __webpack_require__(470); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sample", function() { return _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__["sample"]; }); -/* harmony import */ var _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__ = __webpack_require__(466); +/* harmony import */ var _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__ = __webpack_require__(471); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sampleTime", function() { return _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__["sampleTime"]; }); -/* harmony import */ var _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__ = __webpack_require__(446); +/* harmony import */ var _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__ = __webpack_require__(451); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "scan", function() { return _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__["scan"]; }); -/* harmony import */ var _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__ = __webpack_require__(467); +/* harmony import */ var _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__ = __webpack_require__(472); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sequenceEqual", function() { return _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__["sequenceEqual"]; }); -/* harmony import */ var _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__ = __webpack_require__(468); +/* harmony import */ var _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__ = __webpack_require__(473); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "share", function() { return _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__["share"]; }); -/* harmony import */ var _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__ = __webpack_require__(469); +/* harmony import */ var _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__ = __webpack_require__(474); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "shareReplay", function() { return _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__["shareReplay"]; }); -/* harmony import */ var _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__ = __webpack_require__(470); +/* harmony import */ var _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__ = __webpack_require__(475); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "single", function() { return _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__["single"]; }); -/* harmony import */ var _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__ = __webpack_require__(471); +/* harmony import */ var _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__ = __webpack_require__(476); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skip", function() { return _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__["skip"]; }); -/* harmony import */ var _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__ = __webpack_require__(472); +/* harmony import */ var _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__ = __webpack_require__(477); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipLast", function() { return _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__["skipLast"]; }); -/* harmony import */ var _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__ = __webpack_require__(473); +/* harmony import */ var _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__ = __webpack_require__(478); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipUntil", function() { return _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__["skipUntil"]; }); -/* harmony import */ var _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__ = __webpack_require__(474); +/* harmony import */ var _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__ = __webpack_require__(479); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipWhile", function() { return _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__["skipWhile"]; }); -/* harmony import */ var _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__ = __webpack_require__(475); +/* harmony import */ var _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__ = __webpack_require__(480); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "startWith", function() { return _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__["startWith"]; }); -/* harmony import */ var _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__ = __webpack_require__(476); +/* harmony import */ var _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__ = __webpack_require__(481); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__["subscribeOn"]; }); -/* harmony import */ var _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__ = __webpack_require__(478); +/* harmony import */ var _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__ = __webpack_require__(483); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__["switchAll"]; }); -/* harmony import */ var _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__ = __webpack_require__(479); +/* harmony import */ var _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__ = __webpack_require__(484); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMap", function() { return _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__["switchMap"]; }); -/* harmony import */ var _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__ = __webpack_require__(480); +/* harmony import */ var _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__ = __webpack_require__(485); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__["switchMapTo"]; }); -/* harmony import */ var _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__ = __webpack_require__(428); +/* harmony import */ var _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__ = __webpack_require__(433); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "take", function() { return _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__["take"]; }); -/* harmony import */ var _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__ = __webpack_require__(441); +/* harmony import */ var _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__ = __webpack_require__(446); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeLast", function() { return _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__["takeLast"]; }); -/* harmony import */ var _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__ = __webpack_require__(481); +/* harmony import */ var _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__ = __webpack_require__(486); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeUntil", function() { return _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__["takeUntil"]; }); -/* harmony import */ var _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__ = __webpack_require__(482); +/* harmony import */ var _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__ = __webpack_require__(487); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeWhile", function() { return _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__["takeWhile"]; }); -/* harmony import */ var _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__ = __webpack_require__(483); +/* harmony import */ var _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__ = __webpack_require__(488); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "tap", function() { return _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__["tap"]; }); -/* harmony import */ var _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__ = __webpack_require__(484); +/* harmony import */ var _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__ = __webpack_require__(489); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttle", function() { return _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__["throttle"]; }); -/* harmony import */ var _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__ = __webpack_require__(485); +/* harmony import */ var _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__ = __webpack_require__(490); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttleTime", function() { return _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__["throttleTime"]; }); -/* harmony import */ var _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__ = __webpack_require__(427); +/* harmony import */ var _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__ = __webpack_require__(432); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throwIfEmpty", function() { return _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__["throwIfEmpty"]; }); -/* harmony import */ var _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__ = __webpack_require__(486); +/* harmony import */ var _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__ = __webpack_require__(491); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__["timeInterval"]; }); -/* harmony import */ var _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__ = __webpack_require__(487); +/* harmony import */ var _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__ = __webpack_require__(492); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__["timeout"]; }); -/* harmony import */ var _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__ = __webpack_require__(488); +/* harmony import */ var _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__ = __webpack_require__(493); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__["timeoutWith"]; }); -/* harmony import */ var _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__ = __webpack_require__(489); +/* harmony import */ var _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__ = __webpack_require__(494); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timestamp", function() { return _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__["timestamp"]; }); -/* harmony import */ var _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__ = __webpack_require__(490); +/* harmony import */ var _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__ = __webpack_require__(495); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__["toArray"]; }); -/* harmony import */ var _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__ = __webpack_require__(491); +/* harmony import */ var _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__ = __webpack_require__(496); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "window", function() { return _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__["window"]; }); -/* harmony import */ var _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__ = __webpack_require__(492); +/* harmony import */ var _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__ = __webpack_require__(497); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowCount", function() { return _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__["windowCount"]; }); -/* harmony import */ var _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__ = __webpack_require__(493); +/* harmony import */ var _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__ = __webpack_require__(498); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowTime", function() { return _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__["windowTime"]; }); -/* harmony import */ var _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__ = __webpack_require__(494); +/* harmony import */ var _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__ = __webpack_require__(499); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowToggle", function() { return _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__["windowToggle"]; }); -/* harmony import */ var _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__ = __webpack_require__(495); +/* harmony import */ var _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__ = __webpack_require__(500); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowWhen", function() { return _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__["windowWhen"]; }); -/* harmony import */ var _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__ = __webpack_require__(496); +/* harmony import */ var _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__ = __webpack_require__(501); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "withLatestFrom", function() { return _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__["withLatestFrom"]; }); -/* harmony import */ var _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__ = __webpack_require__(497); +/* harmony import */ var _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__ = __webpack_require__(502); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zip", function() { return _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__["zip"]; }); -/* harmony import */ var _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__ = __webpack_require__(498); +/* harmony import */ var _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__ = __webpack_require__(503); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zipAll", function() { return _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__["zipAll"]; }); /** PURE_IMPORTS_START PURE_IMPORTS_END */ @@ -53375,7 +53542,7 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 402 */ +/* 407 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53454,14 +53621,14 @@ var AuditSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 403 */ +/* 408 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return auditTime; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); -/* harmony import */ var _audit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(402); +/* harmony import */ var _audit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(407); /* harmony import */ var _observable_timer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(108); /** PURE_IMPORTS_START _scheduler_async,_audit,_observable_timer PURE_IMPORTS_END */ @@ -53477,7 +53644,7 @@ function auditTime(duration, scheduler) { /***/ }), -/* 404 */ +/* 409 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53524,7 +53691,7 @@ var BufferSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 405 */ +/* 410 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53625,7 +53792,7 @@ var BufferSkipCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 406 */ +/* 411 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53786,7 +53953,7 @@ function dispatchBufferClose(arg) { /***/ }), -/* 407 */ +/* 412 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53905,7 +54072,7 @@ var BufferToggleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 408 */ +/* 413 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53998,7 +54165,7 @@ var BufferWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 409 */ +/* 414 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54058,7 +54225,7 @@ var CatchSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 410 */ +/* 415 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54074,7 +54241,7 @@ function combineAll(project) { /***/ }), -/* 411 */ +/* 416 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54106,7 +54273,7 @@ function combineLatest() { /***/ }), -/* 412 */ +/* 417 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54126,7 +54293,7 @@ function concat() { /***/ }), -/* 413 */ +/* 418 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54142,13 +54309,13 @@ function concatMap(project, resultSelector) { /***/ }), -/* 414 */ +/* 419 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return concatMapTo; }); -/* harmony import */ var _concatMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(413); +/* harmony import */ var _concatMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(418); /** PURE_IMPORTS_START _concatMap PURE_IMPORTS_END */ function concatMapTo(innerObservable, resultSelector) { @@ -54158,7 +54325,7 @@ function concatMapTo(innerObservable, resultSelector) { /***/ }), -/* 415 */ +/* 420 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54223,7 +54390,7 @@ var CountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 416 */ +/* 421 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54308,7 +54475,7 @@ var DebounceSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 417 */ +/* 422 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54384,7 +54551,7 @@ function dispatchNext(subscriber) { /***/ }), -/* 418 */ +/* 423 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54434,7 +54601,7 @@ var DefaultIfEmptySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 419 */ +/* 424 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54442,7 +54609,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return delay; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(55); -/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(420); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(425); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(11); /* harmony import */ var _Notification__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(42); /** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_Subscriber,_Notification PURE_IMPORTS_END */ @@ -54541,7 +54708,7 @@ var DelayMessage = /*@__PURE__*/ (function () { /***/ }), -/* 420 */ +/* 425 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54555,7 +54722,7 @@ function isDate(value) { /***/ }), -/* 421 */ +/* 426 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54701,7 +54868,7 @@ var SubscriptionDelaySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 422 */ +/* 427 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54739,7 +54906,7 @@ var DeMaterializeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 423 */ +/* 428 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54815,7 +54982,7 @@ var DistinctSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 424 */ +/* 429 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54886,13 +55053,13 @@ var DistinctUntilChangedSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 425 */ +/* 430 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return distinctUntilKeyChanged; }); -/* harmony import */ var _distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(424); +/* harmony import */ var _distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(429); /** PURE_IMPORTS_START _distinctUntilChanged PURE_IMPORTS_END */ function distinctUntilKeyChanged(key, compare) { @@ -54902,7 +55069,7 @@ function distinctUntilKeyChanged(key, compare) { /***/ }), -/* 426 */ +/* 431 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54910,9 +55077,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return elementAt; }); /* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(62); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(105); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(427); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(418); -/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(428); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(432); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(423); +/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(433); /** PURE_IMPORTS_START _util_ArgumentOutOfRangeError,_filter,_throwIfEmpty,_defaultIfEmpty,_take PURE_IMPORTS_END */ @@ -54934,7 +55101,7 @@ function elementAt(index, defaultValue) { /***/ }), -/* 427 */ +/* 432 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55000,7 +55167,7 @@ function defaultErrorFactory() { /***/ }), -/* 428 */ +/* 433 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55062,7 +55229,7 @@ var TakeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 429 */ +/* 434 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55084,7 +55251,7 @@ function endWith() { /***/ }), -/* 430 */ +/* 435 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55146,7 +55313,7 @@ var EverySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 431 */ +/* 436 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55200,7 +55367,7 @@ var SwitchFirstSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 432 */ +/* 437 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55294,7 +55461,7 @@ var ExhaustMapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 433 */ +/* 438 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55406,7 +55573,7 @@ var ExpandSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 434 */ +/* 439 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55444,7 +55611,7 @@ var FinallySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 435 */ +/* 440 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55516,13 +55683,13 @@ var FindValueSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 436 */ +/* 441 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return findIndex; }); -/* harmony import */ var _operators_find__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(435); +/* harmony import */ var _operators_find__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(440); /** PURE_IMPORTS_START _operators_find PURE_IMPORTS_END */ function findIndex(predicate, thisArg) { @@ -55532,7 +55699,7 @@ function findIndex(predicate, thisArg) { /***/ }), -/* 437 */ +/* 442 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55540,9 +55707,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "first", function() { return first; }); /* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(63); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(105); -/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(428); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(418); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(427); +/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(433); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(423); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(432); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25); /** PURE_IMPORTS_START _util_EmptyError,_filter,_take,_defaultIfEmpty,_throwIfEmpty,_util_identity PURE_IMPORTS_END */ @@ -55559,7 +55726,7 @@ function first(predicate, defaultValue) { /***/ }), -/* 438 */ +/* 443 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55596,7 +55763,7 @@ var IgnoreElementsSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 439 */ +/* 444 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55640,7 +55807,7 @@ var IsEmptySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 440 */ +/* 445 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55648,9 +55815,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "last", function() { return last; }); /* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(63); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(105); -/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(441); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(427); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(418); +/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(446); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(432); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(423); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25); /** PURE_IMPORTS_START _util_EmptyError,_filter,_takeLast,_throwIfEmpty,_defaultIfEmpty,_util_identity PURE_IMPORTS_END */ @@ -55667,7 +55834,7 @@ function last(predicate, defaultValue) { /***/ }), -/* 441 */ +/* 446 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55744,7 +55911,7 @@ var TakeLastSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 442 */ +/* 447 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55783,7 +55950,7 @@ var MapToSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 443 */ +/* 448 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55833,13 +56000,13 @@ var MaterializeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 444 */ +/* 449 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "max", function() { return max; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(445); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(450); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function max(comparer) { @@ -55852,15 +56019,15 @@ function max(comparer) { /***/ }), -/* 445 */ +/* 450 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return reduce; }); -/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(446); -/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(441); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(418); +/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(451); +/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(446); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(423); /* harmony import */ var _util_pipe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(24); /** PURE_IMPORTS_START _scan,_takeLast,_defaultIfEmpty,_util_pipe PURE_IMPORTS_END */ @@ -55881,7 +56048,7 @@ function reduce(accumulator, seed) { /***/ }), -/* 446 */ +/* 451 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55963,7 +56130,7 @@ var ScanSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 447 */ +/* 452 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55983,7 +56150,7 @@ function merge() { /***/ }), -/* 448 */ +/* 453 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56008,7 +56175,7 @@ function mergeMapTo(innerObservable, resultSelector, concurrent) { /***/ }), -/* 449 */ +/* 454 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56117,13 +56284,13 @@ var MergeScanSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 450 */ +/* 455 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "min", function() { return min; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(445); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(450); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function min(comparer) { @@ -56136,7 +56303,7 @@ function min(comparer) { /***/ }), -/* 451 */ +/* 456 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56185,7 +56352,7 @@ var MulticastOperator = /*@__PURE__*/ (function () { /***/ }), -/* 452 */ +/* 457 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56275,7 +56442,7 @@ var OnErrorResumeNextSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 453 */ +/* 458 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56323,7 +56490,7 @@ var PairwiseSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 454 */ +/* 459 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56346,7 +56513,7 @@ function partition(predicate, thisArg) { /***/ }), -/* 455 */ +/* 460 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56386,14 +56553,14 @@ function plucker(props, length) { /***/ }), -/* 456 */ +/* 461 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return publish; }); /* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(27); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(451); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(456); /** PURE_IMPORTS_START _Subject,_multicast PURE_IMPORTS_END */ @@ -56406,14 +56573,14 @@ function publish(selector) { /***/ }), -/* 457 */ +/* 462 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return publishBehavior; }); /* harmony import */ var _BehaviorSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(32); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(451); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(456); /** PURE_IMPORTS_START _BehaviorSubject,_multicast PURE_IMPORTS_END */ @@ -56424,14 +56591,14 @@ function publishBehavior(value) { /***/ }), -/* 458 */ +/* 463 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return publishLast; }); /* harmony import */ var _AsyncSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(50); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(451); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(456); /** PURE_IMPORTS_START _AsyncSubject,_multicast PURE_IMPORTS_END */ @@ -56442,14 +56609,14 @@ function publishLast() { /***/ }), -/* 459 */ +/* 464 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return publishReplay; }); /* harmony import */ var _ReplaySubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(33); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(451); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(456); /** PURE_IMPORTS_START _ReplaySubject,_multicast PURE_IMPORTS_END */ @@ -56465,7 +56632,7 @@ function publishReplay(bufferSize, windowTime, selectorOrScheduler, scheduler) { /***/ }), -/* 460 */ +/* 465 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56492,7 +56659,7 @@ function race() { /***/ }), -/* 461 */ +/* 466 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56557,7 +56724,7 @@ var RepeatSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 462 */ +/* 467 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56651,7 +56818,7 @@ var RepeatWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 463 */ +/* 468 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56704,7 +56871,7 @@ var RetrySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 464 */ +/* 469 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56790,7 +56957,7 @@ var RetryWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 465 */ +/* 470 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56845,7 +57012,7 @@ var SampleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 466 */ +/* 471 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56905,7 +57072,7 @@ function dispatchNotification(state) { /***/ }), -/* 467 */ +/* 472 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57028,13 +57195,13 @@ var SequenceEqualCompareToSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 468 */ +/* 473 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "share", function() { return share; }); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(451); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(456); /* harmony import */ var _refCount__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(30); /* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(27); /** PURE_IMPORTS_START _multicast,_refCount,_Subject PURE_IMPORTS_END */ @@ -57051,7 +57218,7 @@ function share() { /***/ }), -/* 469 */ +/* 474 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57120,7 +57287,7 @@ function shareReplayOperator(_a) { /***/ }), -/* 470 */ +/* 475 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57200,7 +57367,7 @@ var SingleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 471 */ +/* 476 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57242,7 +57409,7 @@ var SkipSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 472 */ +/* 477 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57304,7 +57471,7 @@ var SkipLastSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 473 */ +/* 478 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57361,7 +57528,7 @@ var SkipUntilSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 474 */ +/* 479 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57417,7 +57584,7 @@ var SkipWhileSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 475 */ +/* 480 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57446,13 +57613,13 @@ function startWith() { /***/ }), -/* 476 */ +/* 481 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return subscribeOn; }); -/* harmony import */ var _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(477); +/* harmony import */ var _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(482); /** PURE_IMPORTS_START _observable_SubscribeOnObservable PURE_IMPORTS_END */ function subscribeOn(scheduler, delay) { @@ -57477,7 +57644,7 @@ var SubscribeOnOperator = /*@__PURE__*/ (function () { /***/ }), -/* 477 */ +/* 482 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57541,13 +57708,13 @@ var SubscribeOnObservable = /*@__PURE__*/ (function (_super) { /***/ }), -/* 478 */ +/* 483 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return switchAll; }); -/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(479); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(484); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(25); /** PURE_IMPORTS_START _switchMap,_util_identity PURE_IMPORTS_END */ @@ -57559,7 +57726,7 @@ function switchAll() { /***/ }), -/* 479 */ +/* 484 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57647,13 +57814,13 @@ var SwitchMapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 480 */ +/* 485 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return switchMapTo; }); -/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(479); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(484); /** PURE_IMPORTS_START _switchMap PURE_IMPORTS_END */ function switchMapTo(innerObservable, resultSelector) { @@ -57663,7 +57830,7 @@ function switchMapTo(innerObservable, resultSelector) { /***/ }), -/* 481 */ +/* 486 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57711,7 +57878,7 @@ var TakeUntilSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 482 */ +/* 487 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57779,7 +57946,7 @@ var TakeWhileSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 483 */ +/* 488 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57867,7 +58034,7 @@ var TapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 484 */ +/* 489 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57969,7 +58136,7 @@ var ThrottleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 485 */ +/* 490 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57978,7 +58145,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(11); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(55); -/* harmony import */ var _throttle__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(484); +/* harmony import */ var _throttle__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(489); /** PURE_IMPORTS_START tslib,_Subscriber,_scheduler_async,_throttle PURE_IMPORTS_END */ @@ -58067,7 +58234,7 @@ function dispatchNext(arg) { /***/ }), -/* 486 */ +/* 491 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58075,7 +58242,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return timeInterval; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TimeInterval", function() { return TimeInterval; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); -/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(446); +/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(451); /* harmony import */ var _observable_defer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(91); /* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(66); /** PURE_IMPORTS_START _scheduler_async,_scan,_observable_defer,_map PURE_IMPORTS_END */ @@ -58111,7 +58278,7 @@ var TimeInterval = /*@__PURE__*/ (function () { /***/ }), -/* 487 */ +/* 492 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58119,7 +58286,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return timeout; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); /* harmony import */ var _util_TimeoutError__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(64); -/* harmony import */ var _timeoutWith__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(488); +/* harmony import */ var _timeoutWith__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(493); /* harmony import */ var _observable_throwError__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(49); /** PURE_IMPORTS_START _scheduler_async,_util_TimeoutError,_timeoutWith,_observable_throwError PURE_IMPORTS_END */ @@ -58136,7 +58303,7 @@ function timeout(due, scheduler) { /***/ }), -/* 488 */ +/* 493 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58144,7 +58311,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return timeoutWith; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(55); -/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(420); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(425); /* harmony import */ var _innerSubscribe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(90); /** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_innerSubscribe PURE_IMPORTS_END */ @@ -58215,7 +58382,7 @@ var TimeoutWithSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 489 */ +/* 494 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58245,13 +58412,13 @@ var Timestamp = /*@__PURE__*/ (function () { /***/ }), -/* 490 */ +/* 495 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return toArray; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(445); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(450); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function toArrayReducer(arr, item, index) { @@ -58268,7 +58435,7 @@ function toArray() { /***/ }), -/* 491 */ +/* 496 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58346,7 +58513,7 @@ var WindowSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 492 */ +/* 497 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58436,7 +58603,7 @@ var WindowCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 493 */ +/* 498 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58606,7 +58773,7 @@ function dispatchWindowClose(state) { /***/ }), -/* 494 */ +/* 499 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58749,7 +58916,7 @@ var WindowToggleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 495 */ +/* 500 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58846,7 +59013,7 @@ var WindowSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 496 */ +/* 501 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58941,7 +59108,7 @@ var WithLatestFromSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 497 */ +/* 502 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58963,7 +59130,7 @@ function zip() { /***/ }), -/* 498 */ +/* 503 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58979,7 +59146,7 @@ function zipAll(project) { /***/ }), -/* 499 */ +/* 504 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58988,8 +59155,8 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(249); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(246); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(248); -/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(366); -/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(500); +/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(371); +/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(505); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -59071,7 +59238,7 @@ function toArray(value) { } /***/ }), -/* 500 */ +/* 505 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -59079,13 +59246,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Kibana", function() { return Kibana; }); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(501); +/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(506); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(239); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(361); +/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(366); /* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(248); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(505); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(510); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -59247,15 +59414,15 @@ class Kibana { } /***/ }), -/* 501 */ +/* 506 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const minimatch = __webpack_require__(150); -const arrayUnion = __webpack_require__(502); -const arrayDiffer = __webpack_require__(503); -const arrify = __webpack_require__(504); +const arrayUnion = __webpack_require__(507); +const arrayDiffer = __webpack_require__(508); +const arrify = __webpack_require__(509); module.exports = (list, patterns, options = {}) => { list = arrify(list); @@ -59279,7 +59446,7 @@ module.exports = (list, patterns, options = {}) => { /***/ }), -/* 502 */ +/* 507 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59291,7 +59458,7 @@ module.exports = (...arguments_) => { /***/ }), -/* 503 */ +/* 508 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59306,7 +59473,7 @@ module.exports = arrayDiffer; /***/ }), -/* 504 */ +/* 509 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59336,7 +59503,7 @@ module.exports = arrify; /***/ }), -/* 505 */ +/* 510 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -59406,12 +59573,12 @@ function getProjectPaths({ } /***/ }), -/* 506 */ +/* 511 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(507); +/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(512); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); /* @@ -59435,19 +59602,19 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 507 */ +/* 512 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return buildProductionProjects; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(508); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(513); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(143); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(505); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(510); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(131); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(246); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(251); @@ -59584,7 +59751,7 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { } /***/ }), -/* 508 */ +/* 513 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59592,14 +59759,14 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { const EventEmitter = __webpack_require__(156); const path = __webpack_require__(4); const os = __webpack_require__(121); -const pMap = __webpack_require__(509); -const arrify = __webpack_require__(504); -const globby = __webpack_require__(510); -const hasGlob = __webpack_require__(706); -const cpFile = __webpack_require__(708); -const junk = __webpack_require__(718); -const pFilter = __webpack_require__(719); -const CpyError = __webpack_require__(721); +const pMap = __webpack_require__(514); +const arrify = __webpack_require__(509); +const globby = __webpack_require__(515); +const hasGlob = __webpack_require__(711); +const cpFile = __webpack_require__(713); +const junk = __webpack_require__(723); +const pFilter = __webpack_require__(724); +const CpyError = __webpack_require__(726); const defaultOptions = { ignoreJunk: true @@ -59750,7 +59917,7 @@ module.exports = (source, destination, { /***/ }), -/* 509 */ +/* 514 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59838,17 +60005,17 @@ module.exports = async ( /***/ }), -/* 510 */ +/* 515 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const arrayUnion = __webpack_require__(511); +const arrayUnion = __webpack_require__(516); const glob = __webpack_require__(147); -const fastGlob = __webpack_require__(513); -const dirGlob = __webpack_require__(699); -const gitignore = __webpack_require__(702); +const fastGlob = __webpack_require__(518); +const dirGlob = __webpack_require__(704); +const gitignore = __webpack_require__(707); const DEFAULT_FILTER = () => false; @@ -59993,12 +60160,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 511 */ +/* 516 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(512); +var arrayUniq = __webpack_require__(517); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -60006,7 +60173,7 @@ module.exports = function () { /***/ }), -/* 512 */ +/* 517 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60075,10 +60242,10 @@ if ('Set' in global) { /***/ }), -/* 513 */ +/* 518 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(514); +const pkg = __webpack_require__(519); module.exports = pkg.async; module.exports.default = pkg.async; @@ -60091,19 +60258,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 514 */ +/* 519 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var optionsManager = __webpack_require__(515); -var taskManager = __webpack_require__(516); -var reader_async_1 = __webpack_require__(670); -var reader_stream_1 = __webpack_require__(694); -var reader_sync_1 = __webpack_require__(695); -var arrayUtils = __webpack_require__(697); -var streamUtils = __webpack_require__(698); +var optionsManager = __webpack_require__(520); +var taskManager = __webpack_require__(521); +var reader_async_1 = __webpack_require__(675); +var reader_stream_1 = __webpack_require__(699); +var reader_sync_1 = __webpack_require__(700); +var arrayUtils = __webpack_require__(702); +var streamUtils = __webpack_require__(703); /** * Synchronous API. */ @@ -60169,7 +60336,7 @@ function isString(source) { /***/ }), -/* 515 */ +/* 520 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60207,13 +60374,13 @@ exports.prepare = prepare; /***/ }), -/* 516 */ +/* 521 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(517); +var patternUtils = __webpack_require__(522); /** * Generate tasks based on parent directory of each pattern. */ @@ -60304,16 +60471,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 517 */ +/* 522 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var globParent = __webpack_require__(518); +var globParent = __webpack_require__(523); var isGlob = __webpack_require__(172); -var micromatch = __webpack_require__(521); +var micromatch = __webpack_require__(526); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -60459,15 +60626,15 @@ exports.matchAny = matchAny; /***/ }), -/* 518 */ +/* 523 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(4); -var isglob = __webpack_require__(519); -var pathDirname = __webpack_require__(520); +var isglob = __webpack_require__(524); +var pathDirname = __webpack_require__(525); var isWin32 = __webpack_require__(121).platform() === 'win32'; module.exports = function globParent(str) { @@ -60490,7 +60657,7 @@ module.exports = function globParent(str) { /***/ }), -/* 519 */ +/* 524 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -60521,7 +60688,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 520 */ +/* 525 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60671,7 +60838,7 @@ module.exports.win32 = win32; /***/ }), -/* 521 */ +/* 526 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60682,18 +60849,18 @@ module.exports.win32 = win32; */ var util = __webpack_require__(112); -var braces = __webpack_require__(522); -var toRegex = __webpack_require__(523); -var extend = __webpack_require__(636); +var braces = __webpack_require__(527); +var toRegex = __webpack_require__(528); +var extend = __webpack_require__(641); /** * Local dependencies */ -var compilers = __webpack_require__(638); -var parsers = __webpack_require__(665); -var cache = __webpack_require__(666); -var utils = __webpack_require__(667); +var compilers = __webpack_require__(643); +var parsers = __webpack_require__(670); +var cache = __webpack_require__(671); +var utils = __webpack_require__(672); var MAX_LENGTH = 1024 * 64; /** @@ -61555,7 +61722,7 @@ module.exports = micromatch; /***/ }), -/* 522 */ +/* 527 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -61565,18 +61732,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(523); -var unique = __webpack_require__(545); -var extend = __webpack_require__(546); +var toRegex = __webpack_require__(528); +var unique = __webpack_require__(550); +var extend = __webpack_require__(551); /** * Local dependencies */ -var compilers = __webpack_require__(548); -var parsers = __webpack_require__(561); -var Braces = __webpack_require__(565); -var utils = __webpack_require__(549); +var compilers = __webpack_require__(553); +var parsers = __webpack_require__(566); +var Braces = __webpack_require__(570); +var utils = __webpack_require__(554); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -61880,16 +62047,16 @@ module.exports = braces; /***/ }), -/* 523 */ +/* 528 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(524); -var define = __webpack_require__(530); -var extend = __webpack_require__(538); -var not = __webpack_require__(542); +var safe = __webpack_require__(529); +var define = __webpack_require__(535); +var extend = __webpack_require__(543); +var not = __webpack_require__(547); var MAX_LENGTH = 1024 * 64; /** @@ -62042,10 +62209,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 524 */ +/* 529 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(525); +var parse = __webpack_require__(530); var types = parse.types; module.exports = function (re, opts) { @@ -62091,13 +62258,13 @@ function isRegExp (x) { /***/ }), -/* 525 */ +/* 530 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(526); -var types = __webpack_require__(527); -var sets = __webpack_require__(528); -var positions = __webpack_require__(529); +var util = __webpack_require__(531); +var types = __webpack_require__(532); +var sets = __webpack_require__(533); +var positions = __webpack_require__(534); module.exports = function(regexpStr) { @@ -62379,11 +62546,11 @@ module.exports.types = types; /***/ }), -/* 526 */ +/* 531 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(527); -var sets = __webpack_require__(528); +var types = __webpack_require__(532); +var sets = __webpack_require__(533); // All of these are private and only used by randexp. @@ -62496,7 +62663,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 527 */ +/* 532 */ /***/ (function(module, exports) { module.exports = { @@ -62512,10 +62679,10 @@ module.exports = { /***/ }), -/* 528 */ +/* 533 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(527); +var types = __webpack_require__(532); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -62600,10 +62767,10 @@ exports.anyChar = function() { /***/ }), -/* 529 */ +/* 534 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(527); +var types = __webpack_require__(532); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -62623,7 +62790,7 @@ exports.end = function() { /***/ }), -/* 530 */ +/* 535 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62636,8 +62803,8 @@ exports.end = function() { -var isobject = __webpack_require__(531); -var isDescriptor = __webpack_require__(532); +var isobject = __webpack_require__(536); +var isDescriptor = __webpack_require__(537); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -62668,7 +62835,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 531 */ +/* 536 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62687,7 +62854,7 @@ module.exports = function isObject(val) { /***/ }), -/* 532 */ +/* 537 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62700,9 +62867,9 @@ module.exports = function isObject(val) { -var typeOf = __webpack_require__(533); -var isAccessor = __webpack_require__(534); -var isData = __webpack_require__(536); +var typeOf = __webpack_require__(538); +var isAccessor = __webpack_require__(539); +var isData = __webpack_require__(541); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -62716,7 +62883,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 533 */ +/* 538 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -62851,7 +63018,7 @@ function isBuffer(val) { /***/ }), -/* 534 */ +/* 539 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62864,7 +63031,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(535); +var typeOf = __webpack_require__(540); // accessor descriptor properties var accessor = { @@ -62927,7 +63094,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 535 */ +/* 540 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -63062,7 +63229,7 @@ function isBuffer(val) { /***/ }), -/* 536 */ +/* 541 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63075,7 +63242,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(537); +var typeOf = __webpack_require__(542); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -63118,7 +63285,7 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 537 */ +/* 542 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -63253,14 +63420,14 @@ function isBuffer(val) { /***/ }), -/* 538 */ +/* 543 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(539); -var assignSymbols = __webpack_require__(541); +var isExtendable = __webpack_require__(544); +var assignSymbols = __webpack_require__(546); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -63320,7 +63487,7 @@ function isEnum(obj, key) { /***/ }), -/* 539 */ +/* 544 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63333,7 +63500,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(540); +var isPlainObject = __webpack_require__(545); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -63341,7 +63508,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 540 */ +/* 545 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63354,7 +63521,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(531); +var isObject = __webpack_require__(536); function isObjectObject(o) { return isObject(o) === true @@ -63385,7 +63552,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 541 */ +/* 546 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63432,14 +63599,14 @@ module.exports = function(receiver, objects) { /***/ }), -/* 542 */ +/* 547 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(543); -var safe = __webpack_require__(524); +var extend = __webpack_require__(548); +var safe = __webpack_require__(529); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -63511,14 +63678,14 @@ module.exports = toRegex; /***/ }), -/* 543 */ +/* 548 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(544); -var assignSymbols = __webpack_require__(541); +var isExtendable = __webpack_require__(549); +var assignSymbols = __webpack_require__(546); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -63578,7 +63745,7 @@ function isEnum(obj, key) { /***/ }), -/* 544 */ +/* 549 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63591,7 +63758,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(540); +var isPlainObject = __webpack_require__(545); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -63599,7 +63766,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 545 */ +/* 550 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63649,13 +63816,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 546 */ +/* 551 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(547); +var isObject = __webpack_require__(552); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -63689,7 +63856,7 @@ function hasOwn(obj, key) { /***/ }), -/* 547 */ +/* 552 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63709,13 +63876,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 548 */ +/* 553 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(549); +var utils = __webpack_require__(554); module.exports = function(braces, options) { braces.compiler @@ -63998,25 +64165,25 @@ function hasQueue(node) { /***/ }), -/* 549 */ +/* 554 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(550); +var splitString = __webpack_require__(555); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(546); -utils.flatten = __webpack_require__(553); -utils.isObject = __webpack_require__(531); -utils.fillRange = __webpack_require__(554); -utils.repeat = __webpack_require__(560); -utils.unique = __webpack_require__(545); +utils.extend = __webpack_require__(551); +utils.flatten = __webpack_require__(558); +utils.isObject = __webpack_require__(536); +utils.fillRange = __webpack_require__(559); +utils.repeat = __webpack_require__(565); +utils.unique = __webpack_require__(550); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -64348,7 +64515,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 550 */ +/* 555 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64361,7 +64528,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(551); +var extend = __webpack_require__(556); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -64526,14 +64693,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 551 */ +/* 556 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(552); -var assignSymbols = __webpack_require__(541); +var isExtendable = __webpack_require__(557); +var assignSymbols = __webpack_require__(546); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -64593,7 +64760,7 @@ function isEnum(obj, key) { /***/ }), -/* 552 */ +/* 557 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64606,7 +64773,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(540); +var isPlainObject = __webpack_require__(545); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -64614,7 +64781,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 553 */ +/* 558 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64643,7 +64810,7 @@ function flat(arr, res) { /***/ }), -/* 554 */ +/* 559 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64657,10 +64824,10 @@ function flat(arr, res) { var util = __webpack_require__(112); -var isNumber = __webpack_require__(555); -var extend = __webpack_require__(546); -var repeat = __webpack_require__(558); -var toRegex = __webpack_require__(559); +var isNumber = __webpack_require__(560); +var extend = __webpack_require__(551); +var repeat = __webpack_require__(563); +var toRegex = __webpack_require__(564); /** * Return a range of numbers or letters. @@ -64858,7 +65025,7 @@ module.exports = fillRange; /***/ }), -/* 555 */ +/* 560 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64871,7 +65038,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(556); +var typeOf = __webpack_require__(561); module.exports = function isNumber(num) { var type = typeOf(num); @@ -64887,10 +65054,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 556 */ +/* 561 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(557); +var isBuffer = __webpack_require__(562); var toString = Object.prototype.toString; /** @@ -65009,7 +65176,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 557 */ +/* 562 */ /***/ (function(module, exports) { /*! @@ -65036,7 +65203,7 @@ function isSlowBuffer (obj) { /***/ }), -/* 558 */ +/* 563 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65113,7 +65280,7 @@ function repeat(str, num) { /***/ }), -/* 559 */ +/* 564 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65126,8 +65293,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(558); -var isNumber = __webpack_require__(555); +var repeat = __webpack_require__(563); +var isNumber = __webpack_require__(560); var cache = {}; function toRegexRange(min, max, options) { @@ -65414,7 +65581,7 @@ module.exports = toRegexRange; /***/ }), -/* 560 */ +/* 565 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65439,14 +65606,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 561 */ +/* 566 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(562); -var utils = __webpack_require__(549); +var Node = __webpack_require__(567); +var utils = __webpack_require__(554); /** * Braces parsers @@ -65806,15 +65973,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 562 */ +/* 567 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(531); -var define = __webpack_require__(563); -var utils = __webpack_require__(564); +var isObject = __webpack_require__(536); +var define = __webpack_require__(568); +var utils = __webpack_require__(569); var ownNames; /** @@ -66305,7 +66472,7 @@ exports = module.exports = Node; /***/ }), -/* 563 */ +/* 568 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66318,7 +66485,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(532); +var isDescriptor = __webpack_require__(537); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -66343,13 +66510,13 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 564 */ +/* 569 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(556); +var typeOf = __webpack_require__(561); var utils = module.exports; /** @@ -67369,17 +67536,17 @@ function assert(val, message) { /***/ }), -/* 565 */ +/* 570 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(546); -var Snapdragon = __webpack_require__(566); -var compilers = __webpack_require__(548); -var parsers = __webpack_require__(561); -var utils = __webpack_require__(549); +var extend = __webpack_require__(551); +var Snapdragon = __webpack_require__(571); +var compilers = __webpack_require__(553); +var parsers = __webpack_require__(566); +var utils = __webpack_require__(554); /** * Customize Snapdragon parser and renderer @@ -67480,17 +67647,17 @@ module.exports = Braces; /***/ }), -/* 566 */ +/* 571 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(567); -var define = __webpack_require__(594); -var Compiler = __webpack_require__(604); -var Parser = __webpack_require__(633); -var utils = __webpack_require__(613); +var Base = __webpack_require__(572); +var define = __webpack_require__(599); +var Compiler = __webpack_require__(609); +var Parser = __webpack_require__(638); +var utils = __webpack_require__(618); var regexCache = {}; var cache = {}; @@ -67661,20 +67828,20 @@ module.exports.Parser = Parser; /***/ }), -/* 567 */ +/* 572 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var define = __webpack_require__(568); -var CacheBase = __webpack_require__(569); -var Emitter = __webpack_require__(570); -var isObject = __webpack_require__(531); -var merge = __webpack_require__(588); -var pascal = __webpack_require__(591); -var cu = __webpack_require__(592); +var define = __webpack_require__(573); +var CacheBase = __webpack_require__(574); +var Emitter = __webpack_require__(575); +var isObject = __webpack_require__(536); +var merge = __webpack_require__(593); +var pascal = __webpack_require__(596); +var cu = __webpack_require__(597); /** * Optionally define a custom `cache` namespace to use. @@ -68103,7 +68270,7 @@ module.exports.namespace = namespace; /***/ }), -/* 568 */ +/* 573 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68116,7 +68283,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(532); +var isDescriptor = __webpack_require__(537); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -68141,21 +68308,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 569 */ +/* 574 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(531); -var Emitter = __webpack_require__(570); -var visit = __webpack_require__(571); -var toPath = __webpack_require__(574); -var union = __webpack_require__(575); -var del = __webpack_require__(579); -var get = __webpack_require__(577); -var has = __webpack_require__(584); -var set = __webpack_require__(587); +var isObject = __webpack_require__(536); +var Emitter = __webpack_require__(575); +var visit = __webpack_require__(576); +var toPath = __webpack_require__(579); +var union = __webpack_require__(580); +var del = __webpack_require__(584); +var get = __webpack_require__(582); +var has = __webpack_require__(589); +var set = __webpack_require__(592); /** * Create a `Cache` constructor that when instantiated will @@ -68409,7 +68576,7 @@ module.exports.namespace = namespace; /***/ }), -/* 570 */ +/* 575 */ /***/ (function(module, exports, __webpack_require__) { @@ -68578,7 +68745,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 571 */ +/* 576 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68591,8 +68758,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(572); -var mapVisit = __webpack_require__(573); +var visit = __webpack_require__(577); +var mapVisit = __webpack_require__(578); module.exports = function(collection, method, val) { var result; @@ -68615,7 +68782,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 572 */ +/* 577 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68628,7 +68795,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(531); +var isObject = __webpack_require__(536); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -68655,14 +68822,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 573 */ +/* 578 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var visit = __webpack_require__(572); +var visit = __webpack_require__(577); /** * Map `visit` over an array of objects. @@ -68699,7 +68866,7 @@ function isObject(val) { /***/ }), -/* 574 */ +/* 579 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68712,7 +68879,7 @@ function isObject(val) { -var typeOf = __webpack_require__(556); +var typeOf = __webpack_require__(561); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -68739,16 +68906,16 @@ function filter(arr) { /***/ }), -/* 575 */ +/* 580 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(547); -var union = __webpack_require__(576); -var get = __webpack_require__(577); -var set = __webpack_require__(578); +var isObject = __webpack_require__(552); +var union = __webpack_require__(581); +var get = __webpack_require__(582); +var set = __webpack_require__(583); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -68776,7 +68943,7 @@ function arrayify(val) { /***/ }), -/* 576 */ +/* 581 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68812,7 +68979,7 @@ module.exports = function union(init) { /***/ }), -/* 577 */ +/* 582 */ /***/ (function(module, exports) { /*! @@ -68868,7 +69035,7 @@ function toString(val) { /***/ }), -/* 578 */ +/* 583 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68881,10 +69048,10 @@ function toString(val) { -var split = __webpack_require__(550); -var extend = __webpack_require__(546); -var isPlainObject = __webpack_require__(540); -var isObject = __webpack_require__(547); +var split = __webpack_require__(555); +var extend = __webpack_require__(551); +var isPlainObject = __webpack_require__(545); +var isObject = __webpack_require__(552); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -68930,7 +69097,7 @@ function isValidKey(key) { /***/ }), -/* 579 */ +/* 584 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68943,8 +69110,8 @@ function isValidKey(key) { -var isObject = __webpack_require__(531); -var has = __webpack_require__(580); +var isObject = __webpack_require__(536); +var has = __webpack_require__(585); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -68969,7 +69136,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 580 */ +/* 585 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68982,9 +69149,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(581); -var hasValues = __webpack_require__(583); -var get = __webpack_require__(577); +var isObject = __webpack_require__(586); +var hasValues = __webpack_require__(588); +var get = __webpack_require__(582); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -68995,7 +69162,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 581 */ +/* 586 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69008,7 +69175,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(582); +var isArray = __webpack_require__(587); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -69016,7 +69183,7 @@ module.exports = function isObject(val) { /***/ }), -/* 582 */ +/* 587 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -69027,7 +69194,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 583 */ +/* 588 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69070,7 +69237,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 584 */ +/* 589 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69083,9 +69250,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(531); -var hasValues = __webpack_require__(585); -var get = __webpack_require__(577); +var isObject = __webpack_require__(536); +var hasValues = __webpack_require__(590); +var get = __webpack_require__(582); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -69093,7 +69260,7 @@ module.exports = function(val, prop) { /***/ }), -/* 585 */ +/* 590 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69106,8 +69273,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(586); -var isNumber = __webpack_require__(555); +var typeOf = __webpack_require__(591); +var isNumber = __webpack_require__(560); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -69160,10 +69327,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 586 */ +/* 591 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(557); +var isBuffer = __webpack_require__(562); var toString = Object.prototype.toString; /** @@ -69285,7 +69452,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 587 */ +/* 592 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69298,10 +69465,10 @@ module.exports = function kindOf(val) { -var split = __webpack_require__(550); -var extend = __webpack_require__(546); -var isPlainObject = __webpack_require__(540); -var isObject = __webpack_require__(547); +var split = __webpack_require__(555); +var extend = __webpack_require__(551); +var isPlainObject = __webpack_require__(545); +var isObject = __webpack_require__(552); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -69347,14 +69514,14 @@ function isValidKey(key) { /***/ }), -/* 588 */ +/* 593 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(589); -var forIn = __webpack_require__(590); +var isExtendable = __webpack_require__(594); +var forIn = __webpack_require__(595); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -69418,7 +69585,7 @@ module.exports = mixinDeep; /***/ }), -/* 589 */ +/* 594 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69431,7 +69598,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(540); +var isPlainObject = __webpack_require__(545); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -69439,7 +69606,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 590 */ +/* 595 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69462,7 +69629,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 591 */ +/* 596 */ /***/ (function(module, exports) { /*! @@ -69489,14 +69656,14 @@ module.exports = pascalcase; /***/ }), -/* 592 */ +/* 597 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var utils = __webpack_require__(593); +var utils = __webpack_require__(598); /** * Expose class utils @@ -69861,7 +70028,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 593 */ +/* 598 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69875,10 +70042,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(576); -utils.define = __webpack_require__(594); -utils.isObj = __webpack_require__(531); -utils.staticExtend = __webpack_require__(601); +utils.union = __webpack_require__(581); +utils.define = __webpack_require__(599); +utils.isObj = __webpack_require__(536); +utils.staticExtend = __webpack_require__(606); /** @@ -69889,7 +70056,7 @@ module.exports = utils; /***/ }), -/* 594 */ +/* 599 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69902,7 +70069,7 @@ module.exports = utils; -var isDescriptor = __webpack_require__(595); +var isDescriptor = __webpack_require__(600); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -69927,7 +70094,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 595 */ +/* 600 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69940,9 +70107,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(596); -var isAccessor = __webpack_require__(597); -var isData = __webpack_require__(599); +var typeOf = __webpack_require__(601); +var isAccessor = __webpack_require__(602); +var isData = __webpack_require__(604); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -69956,7 +70123,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 596 */ +/* 601 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -70109,7 +70276,7 @@ function isBuffer(val) { /***/ }), -/* 597 */ +/* 602 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70122,7 +70289,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(598); +var typeOf = __webpack_require__(603); // accessor descriptor properties var accessor = { @@ -70185,10 +70352,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 598 */ +/* 603 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(557); +var isBuffer = __webpack_require__(562); var toString = Object.prototype.toString; /** @@ -70307,7 +70474,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 599 */ +/* 604 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70320,7 +70487,7 @@ module.exports = function kindOf(val) { -var typeOf = __webpack_require__(600); +var typeOf = __webpack_require__(605); // data descriptor properties var data = { @@ -70369,10 +70536,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 600 */ +/* 605 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(557); +var isBuffer = __webpack_require__(562); var toString = Object.prototype.toString; /** @@ -70491,7 +70658,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 601 */ +/* 606 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70504,8 +70671,8 @@ module.exports = function kindOf(val) { -var copy = __webpack_require__(602); -var define = __webpack_require__(594); +var copy = __webpack_require__(607); +var define = __webpack_require__(599); var util = __webpack_require__(112); /** @@ -70588,15 +70755,15 @@ module.exports = extend; /***/ }), -/* 602 */ +/* 607 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(556); -var copyDescriptor = __webpack_require__(603); -var define = __webpack_require__(594); +var typeOf = __webpack_require__(561); +var copyDescriptor = __webpack_require__(608); +var define = __webpack_require__(599); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -70769,7 +70936,7 @@ module.exports.has = has; /***/ }), -/* 603 */ +/* 608 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70857,16 +71024,16 @@ function isObject(val) { /***/ }), -/* 604 */ +/* 609 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(605); -var define = __webpack_require__(594); -var debug = __webpack_require__(607)('snapdragon:compiler'); -var utils = __webpack_require__(613); +var use = __webpack_require__(610); +var define = __webpack_require__(599); +var debug = __webpack_require__(612)('snapdragon:compiler'); +var utils = __webpack_require__(618); /** * Create a new `Compiler` with the given `options`. @@ -71020,7 +71187,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(632); + var sourcemaps = __webpack_require__(637); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -71041,7 +71208,7 @@ module.exports = Compiler; /***/ }), -/* 605 */ +/* 610 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71054,7 +71221,7 @@ module.exports = Compiler; -var utils = __webpack_require__(606); +var utils = __webpack_require__(611); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -71169,7 +71336,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 606 */ +/* 611 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71183,8 +71350,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(594); -utils.isObject = __webpack_require__(531); +utils.define = __webpack_require__(599); +utils.isObject = __webpack_require__(536); utils.isString = function(val) { @@ -71199,7 +71366,7 @@ module.exports = utils; /***/ }), -/* 607 */ +/* 612 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71208,14 +71375,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(608); + module.exports = __webpack_require__(613); } else { - module.exports = __webpack_require__(611); + module.exports = __webpack_require__(616); } /***/ }), -/* 608 */ +/* 613 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71224,7 +71391,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(609); +exports = module.exports = __webpack_require__(614); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -71406,7 +71573,7 @@ function localstorage() { /***/ }), -/* 609 */ +/* 614 */ /***/ (function(module, exports, __webpack_require__) { @@ -71422,7 +71589,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(610); +exports.humanize = __webpack_require__(615); /** * The currently active debug mode names, and names to skip. @@ -71614,7 +71781,7 @@ function coerce(val) { /***/ }), -/* 610 */ +/* 615 */ /***/ (function(module, exports) { /** @@ -71772,7 +71939,7 @@ function plural(ms, n, name) { /***/ }), -/* 611 */ +/* 616 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71788,7 +71955,7 @@ var util = __webpack_require__(112); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(609); +exports = module.exports = __webpack_require__(614); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -71967,7 +72134,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(612); + var net = __webpack_require__(617); stream = new net.Socket({ fd: fd, readable: false, @@ -72026,13 +72193,13 @@ exports.enable(load()); /***/ }), -/* 612 */ +/* 617 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 613 */ +/* 618 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72042,9 +72209,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(546); -exports.SourceMap = __webpack_require__(614); -exports.sourceMapResolve = __webpack_require__(625); +exports.extend = __webpack_require__(551); +exports.SourceMap = __webpack_require__(619); +exports.sourceMapResolve = __webpack_require__(630); /** * Convert backslash in the given string to forward slashes @@ -72087,7 +72254,7 @@ exports.last = function(arr, n) { /***/ }), -/* 614 */ +/* 619 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -72095,13 +72262,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(615).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(621).SourceMapConsumer; -exports.SourceNode = __webpack_require__(624).SourceNode; +exports.SourceMapGenerator = __webpack_require__(620).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(626).SourceMapConsumer; +exports.SourceNode = __webpack_require__(629).SourceNode; /***/ }), -/* 615 */ +/* 620 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72111,10 +72278,10 @@ exports.SourceNode = __webpack_require__(624).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(616); -var util = __webpack_require__(618); -var ArraySet = __webpack_require__(619).ArraySet; -var MappingList = __webpack_require__(620).MappingList; +var base64VLQ = __webpack_require__(621); +var util = __webpack_require__(623); +var ArraySet = __webpack_require__(624).ArraySet; +var MappingList = __webpack_require__(625).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -72523,7 +72690,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 616 */ +/* 621 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72563,7 +72730,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(617); +var base64 = __webpack_require__(622); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -72669,7 +72836,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 617 */ +/* 622 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72742,7 +72909,7 @@ exports.decode = function (charCode) { /***/ }), -/* 618 */ +/* 623 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73165,7 +73332,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 619 */ +/* 624 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73175,7 +73342,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(618); +var util = __webpack_require__(623); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -73292,7 +73459,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 620 */ +/* 625 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73302,7 +73469,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(618); +var util = __webpack_require__(623); /** * Determine whether mappingB is after mappingA with respect to generated @@ -73377,7 +73544,7 @@ exports.MappingList = MappingList; /***/ }), -/* 621 */ +/* 626 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73387,11 +73554,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(618); -var binarySearch = __webpack_require__(622); -var ArraySet = __webpack_require__(619).ArraySet; -var base64VLQ = __webpack_require__(616); -var quickSort = __webpack_require__(623).quickSort; +var util = __webpack_require__(623); +var binarySearch = __webpack_require__(627); +var ArraySet = __webpack_require__(624).ArraySet; +var base64VLQ = __webpack_require__(621); +var quickSort = __webpack_require__(628).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -74465,7 +74632,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 622 */ +/* 627 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74582,7 +74749,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 623 */ +/* 628 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74702,7 +74869,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 624 */ +/* 629 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74712,8 +74879,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(615).SourceMapGenerator; -var util = __webpack_require__(618); +var SourceMapGenerator = __webpack_require__(620).SourceMapGenerator; +var util = __webpack_require__(623); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -75121,17 +75288,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 625 */ +/* 630 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(626) -var resolveUrl = __webpack_require__(627) -var decodeUriComponent = __webpack_require__(628) -var urix = __webpack_require__(630) -var atob = __webpack_require__(631) +var sourceMappingURL = __webpack_require__(631) +var resolveUrl = __webpack_require__(632) +var decodeUriComponent = __webpack_require__(633) +var urix = __webpack_require__(635) +var atob = __webpack_require__(636) @@ -75429,7 +75596,7 @@ module.exports = { /***/ }), -/* 626 */ +/* 631 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -75492,7 +75659,7 @@ void (function(root, factory) { /***/ }), -/* 627 */ +/* 632 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -75510,13 +75677,13 @@ module.exports = resolveUrl /***/ }), -/* 628 */ +/* 633 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(629) +var decodeUriComponent = __webpack_require__(634) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -75527,7 +75694,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 629 */ +/* 634 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75628,7 +75795,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 630 */ +/* 635 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -75651,7 +75818,7 @@ module.exports = urix /***/ }), -/* 631 */ +/* 636 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75665,7 +75832,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 632 */ +/* 637 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75673,8 +75840,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(134); var path = __webpack_require__(4); -var define = __webpack_require__(594); -var utils = __webpack_require__(613); +var define = __webpack_require__(599); +var utils = __webpack_require__(618); /** * Expose `mixin()`. @@ -75817,19 +75984,19 @@ exports.comment = function(node) { /***/ }), -/* 633 */ +/* 638 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(605); +var use = __webpack_require__(610); var util = __webpack_require__(112); -var Cache = __webpack_require__(634); -var define = __webpack_require__(594); -var debug = __webpack_require__(607)('snapdragon:parser'); -var Position = __webpack_require__(635); -var utils = __webpack_require__(613); +var Cache = __webpack_require__(639); +var define = __webpack_require__(599); +var debug = __webpack_require__(612)('snapdragon:parser'); +var Position = __webpack_require__(640); +var utils = __webpack_require__(618); /** * Create a new `Parser` with the given `input` and `options`. @@ -76357,7 +76524,7 @@ module.exports = Parser; /***/ }), -/* 634 */ +/* 639 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76464,13 +76631,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 635 */ +/* 640 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(594); +var define = __webpack_require__(599); /** * Store position for a node @@ -76485,14 +76652,14 @@ module.exports = function Position(start, parser) { /***/ }), -/* 636 */ +/* 641 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(637); -var assignSymbols = __webpack_require__(541); +var isExtendable = __webpack_require__(642); +var assignSymbols = __webpack_require__(546); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -76552,7 +76719,7 @@ function isEnum(obj, key) { /***/ }), -/* 637 */ +/* 642 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76565,7 +76732,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(540); +var isPlainObject = __webpack_require__(545); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -76573,14 +76740,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 638 */ +/* 643 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(639); -var extglob = __webpack_require__(654); +var nanomatch = __webpack_require__(644); +var extglob = __webpack_require__(659); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -76657,7 +76824,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 639 */ +/* 644 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76668,17 +76835,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(112); -var toRegex = __webpack_require__(523); -var extend = __webpack_require__(640); +var toRegex = __webpack_require__(528); +var extend = __webpack_require__(645); /** * Local dependencies */ -var compilers = __webpack_require__(642); -var parsers = __webpack_require__(643); -var cache = __webpack_require__(646); -var utils = __webpack_require__(648); +var compilers = __webpack_require__(647); +var parsers = __webpack_require__(648); +var cache = __webpack_require__(651); +var utils = __webpack_require__(653); var MAX_LENGTH = 1024 * 64; /** @@ -77502,14 +77669,14 @@ module.exports = nanomatch; /***/ }), -/* 640 */ +/* 645 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(641); -var assignSymbols = __webpack_require__(541); +var isExtendable = __webpack_require__(646); +var assignSymbols = __webpack_require__(546); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -77569,7 +77736,7 @@ function isEnum(obj, key) { /***/ }), -/* 641 */ +/* 646 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77582,7 +77749,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(540); +var isPlainObject = __webpack_require__(545); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -77590,7 +77757,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 642 */ +/* 647 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77936,15 +78103,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 643 */ +/* 648 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(542); -var toRegex = __webpack_require__(523); -var isOdd = __webpack_require__(644); +var regexNot = __webpack_require__(547); +var toRegex = __webpack_require__(528); +var isOdd = __webpack_require__(649); /** * Characters to use in negation regex (we want to "not" match @@ -78330,7 +78497,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 644 */ +/* 649 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78343,7 +78510,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(645); +var isNumber = __webpack_require__(650); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -78357,7 +78524,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 645 */ +/* 650 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78385,14 +78552,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 646 */ +/* 651 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(647))(); +module.exports = new (__webpack_require__(652))(); /***/ }), -/* 647 */ +/* 652 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78405,7 +78572,7 @@ module.exports = new (__webpack_require__(647))(); -var MapCache = __webpack_require__(634); +var MapCache = __webpack_require__(639); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -78527,7 +78694,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 648 */ +/* 653 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78540,14 +78707,14 @@ var path = __webpack_require__(4); * Module dependencies */ -var isWindows = __webpack_require__(649)(); -var Snapdragon = __webpack_require__(566); -utils.define = __webpack_require__(650); -utils.diff = __webpack_require__(651); -utils.extend = __webpack_require__(640); -utils.pick = __webpack_require__(652); -utils.typeOf = __webpack_require__(653); -utils.unique = __webpack_require__(545); +var isWindows = __webpack_require__(654)(); +var Snapdragon = __webpack_require__(571); +utils.define = __webpack_require__(655); +utils.diff = __webpack_require__(656); +utils.extend = __webpack_require__(645); +utils.pick = __webpack_require__(657); +utils.typeOf = __webpack_require__(658); +utils.unique = __webpack_require__(550); /** * Returns true if the given value is effectively an empty string @@ -78913,7 +79080,7 @@ utils.unixify = function(options) { /***/ }), -/* 649 */ +/* 654 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -78941,7 +79108,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 650 */ +/* 655 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78954,8 +79121,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(531); -var isDescriptor = __webpack_require__(532); +var isobject = __webpack_require__(536); +var isDescriptor = __webpack_require__(537); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -78986,7 +79153,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 651 */ +/* 656 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79040,7 +79207,7 @@ function diffArray(one, two) { /***/ }), -/* 652 */ +/* 657 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79053,7 +79220,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(531); +var isObject = __webpack_require__(536); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -79082,7 +79249,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 653 */ +/* 658 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -79217,7 +79384,7 @@ function isBuffer(val) { /***/ }), -/* 654 */ +/* 659 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79227,18 +79394,18 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(546); -var unique = __webpack_require__(545); -var toRegex = __webpack_require__(523); +var extend = __webpack_require__(551); +var unique = __webpack_require__(550); +var toRegex = __webpack_require__(528); /** * Local dependencies */ -var compilers = __webpack_require__(655); -var parsers = __webpack_require__(661); -var Extglob = __webpack_require__(664); -var utils = __webpack_require__(663); +var compilers = __webpack_require__(660); +var parsers = __webpack_require__(666); +var Extglob = __webpack_require__(669); +var utils = __webpack_require__(668); var MAX_LENGTH = 1024 * 64; /** @@ -79555,13 +79722,13 @@ module.exports = extglob; /***/ }), -/* 655 */ +/* 660 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(656); +var brackets = __webpack_require__(661); /** * Extglob compilers @@ -79731,7 +79898,7 @@ module.exports = function(extglob) { /***/ }), -/* 656 */ +/* 661 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79741,17 +79908,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(657); -var parsers = __webpack_require__(659); +var compilers = __webpack_require__(662); +var parsers = __webpack_require__(664); /** * Module dependencies */ -var debug = __webpack_require__(607)('expand-brackets'); -var extend = __webpack_require__(546); -var Snapdragon = __webpack_require__(566); -var toRegex = __webpack_require__(523); +var debug = __webpack_require__(612)('expand-brackets'); +var extend = __webpack_require__(551); +var Snapdragon = __webpack_require__(571); +var toRegex = __webpack_require__(528); /** * Parses the given POSIX character class `pattern` and returns a @@ -79949,13 +80116,13 @@ module.exports = brackets; /***/ }), -/* 657 */ +/* 662 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(658); +var posix = __webpack_require__(663); module.exports = function(brackets) { brackets.compiler @@ -80043,7 +80210,7 @@ module.exports = function(brackets) { /***/ }), -/* 658 */ +/* 663 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80072,14 +80239,14 @@ module.exports = { /***/ }), -/* 659 */ +/* 664 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(660); -var define = __webpack_require__(594); +var utils = __webpack_require__(665); +var define = __webpack_require__(599); /** * Text regex @@ -80298,14 +80465,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 660 */ +/* 665 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(523); -var regexNot = __webpack_require__(542); +var toRegex = __webpack_require__(528); +var regexNot = __webpack_require__(547); var cached; /** @@ -80339,15 +80506,15 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 661 */ +/* 666 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(656); -var define = __webpack_require__(662); -var utils = __webpack_require__(663); +var brackets = __webpack_require__(661); +var define = __webpack_require__(667); +var utils = __webpack_require__(668); /** * Characters to use in text regex (we want to "not" match @@ -80502,7 +80669,7 @@ module.exports = parsers; /***/ }), -/* 662 */ +/* 667 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80515,7 +80682,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(532); +var isDescriptor = __webpack_require__(537); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -80540,14 +80707,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 663 */ +/* 668 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(542); -var Cache = __webpack_require__(647); +var regex = __webpack_require__(547); +var Cache = __webpack_require__(652); /** * Utils @@ -80616,7 +80783,7 @@ utils.createRegex = function(str) { /***/ }), -/* 664 */ +/* 669 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80626,16 +80793,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(566); -var define = __webpack_require__(662); -var extend = __webpack_require__(546); +var Snapdragon = __webpack_require__(571); +var define = __webpack_require__(667); +var extend = __webpack_require__(551); /** * Local dependencies */ -var compilers = __webpack_require__(655); -var parsers = __webpack_require__(661); +var compilers = __webpack_require__(660); +var parsers = __webpack_require__(666); /** * Customize Snapdragon parser and renderer @@ -80701,16 +80868,16 @@ module.exports = Extglob; /***/ }), -/* 665 */ +/* 670 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(654); -var nanomatch = __webpack_require__(639); -var regexNot = __webpack_require__(542); -var toRegex = __webpack_require__(523); +var extglob = __webpack_require__(659); +var nanomatch = __webpack_require__(644); +var regexNot = __webpack_require__(547); +var toRegex = __webpack_require__(528); var not; /** @@ -80791,14 +80958,14 @@ function textRegex(pattern) { /***/ }), -/* 666 */ +/* 671 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(647))(); +module.exports = new (__webpack_require__(652))(); /***/ }), -/* 667 */ +/* 672 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80811,13 +80978,13 @@ var path = __webpack_require__(4); * Module dependencies */ -var Snapdragon = __webpack_require__(566); -utils.define = __webpack_require__(668); -utils.diff = __webpack_require__(651); -utils.extend = __webpack_require__(636); -utils.pick = __webpack_require__(652); -utils.typeOf = __webpack_require__(669); -utils.unique = __webpack_require__(545); +var Snapdragon = __webpack_require__(571); +utils.define = __webpack_require__(673); +utils.diff = __webpack_require__(656); +utils.extend = __webpack_require__(641); +utils.pick = __webpack_require__(657); +utils.typeOf = __webpack_require__(674); +utils.unique = __webpack_require__(550); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -81114,7 +81281,7 @@ utils.unixify = function(options) { /***/ }), -/* 668 */ +/* 673 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81127,8 +81294,8 @@ utils.unixify = function(options) { -var isobject = __webpack_require__(531); -var isDescriptor = __webpack_require__(532); +var isobject = __webpack_require__(536); +var isDescriptor = __webpack_require__(537); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -81159,7 +81326,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 669 */ +/* 674 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -81294,7 +81461,7 @@ function isBuffer(val) { /***/ }), -/* 670 */ +/* 675 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81313,9 +81480,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(671); -var reader_1 = __webpack_require__(684); -var fs_stream_1 = __webpack_require__(688); +var readdir = __webpack_require__(676); +var reader_1 = __webpack_require__(689); +var fs_stream_1 = __webpack_require__(693); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -81376,15 +81543,15 @@ exports.default = ReaderAsync; /***/ }), -/* 671 */ +/* 676 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(672); -const readdirAsync = __webpack_require__(680); -const readdirStream = __webpack_require__(683); +const readdirSync = __webpack_require__(677); +const readdirAsync = __webpack_require__(685); +const readdirStream = __webpack_require__(688); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -81468,7 +81635,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 672 */ +/* 677 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81476,11 +81643,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(673); +const DirectoryReader = __webpack_require__(678); let syncFacade = { - fs: __webpack_require__(678), - forEach: __webpack_require__(679), + fs: __webpack_require__(683), + forEach: __webpack_require__(684), sync: true }; @@ -81509,7 +81676,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 673 */ +/* 678 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81518,9 +81685,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(138).Readable; const EventEmitter = __webpack_require__(156).EventEmitter; const path = __webpack_require__(4); -const normalizeOptions = __webpack_require__(674); -const stat = __webpack_require__(676); -const call = __webpack_require__(677); +const normalizeOptions = __webpack_require__(679); +const stat = __webpack_require__(681); +const call = __webpack_require__(682); /** * Asynchronously reads the contents of a directory and streams the results @@ -81896,14 +82063,14 @@ module.exports = DirectoryReader; /***/ }), -/* 674 */ +/* 679 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const globToRegExp = __webpack_require__(675); +const globToRegExp = __webpack_require__(680); module.exports = normalizeOptions; @@ -82080,7 +82247,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 675 */ +/* 680 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -82217,13 +82384,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 676 */ +/* 681 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(677); +const call = __webpack_require__(682); module.exports = stat; @@ -82298,7 +82465,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 677 */ +/* 682 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82359,14 +82526,14 @@ function callOnce (fn) { /***/ }), -/* 678 */ +/* 683 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const call = __webpack_require__(677); +const call = __webpack_require__(682); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -82430,7 +82597,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 679 */ +/* 684 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82459,7 +82626,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 680 */ +/* 685 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82467,12 +82634,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(681); -const DirectoryReader = __webpack_require__(673); +const maybe = __webpack_require__(686); +const DirectoryReader = __webpack_require__(678); let asyncFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(682), + forEach: __webpack_require__(687), async: true }; @@ -82514,7 +82681,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 681 */ +/* 686 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82541,7 +82708,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 682 */ +/* 687 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82577,7 +82744,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 683 */ +/* 688 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82585,11 +82752,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(673); +const DirectoryReader = __webpack_require__(678); let streamFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(682), + forEach: __webpack_require__(687), async: true }; @@ -82609,16 +82776,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 684 */ +/* 689 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var deep_1 = __webpack_require__(685); -var entry_1 = __webpack_require__(687); -var pathUtil = __webpack_require__(686); +var deep_1 = __webpack_require__(690); +var entry_1 = __webpack_require__(692); +var pathUtil = __webpack_require__(691); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -82684,14 +82851,14 @@ exports.default = Reader; /***/ }), -/* 685 */ +/* 690 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(686); -var patternUtils = __webpack_require__(517); +var pathUtils = __webpack_require__(691); +var patternUtils = __webpack_require__(522); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -82774,7 +82941,7 @@ exports.default = DeepFilter; /***/ }), -/* 686 */ +/* 691 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82805,14 +82972,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 687 */ +/* 692 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(686); -var patternUtils = __webpack_require__(517); +var pathUtils = __webpack_require__(691); +var patternUtils = __webpack_require__(522); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -82897,7 +83064,7 @@ exports.default = EntryFilter; /***/ }), -/* 688 */ +/* 693 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82917,8 +83084,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var fsStat = __webpack_require__(689); -var fs_1 = __webpack_require__(693); +var fsStat = __webpack_require__(694); +var fs_1 = __webpack_require__(698); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -82968,14 +83135,14 @@ exports.default = FileSystemStream; /***/ }), -/* 689 */ +/* 694 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(690); -const statProvider = __webpack_require__(692); +const optionsManager = __webpack_require__(695); +const statProvider = __webpack_require__(697); /** * Asynchronous API. */ @@ -83006,13 +83173,13 @@ exports.statSync = statSync; /***/ }), -/* 690 */ +/* 695 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(691); +const fsAdapter = __webpack_require__(696); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -83025,7 +83192,7 @@ exports.prepare = prepare; /***/ }), -/* 691 */ +/* 696 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83048,7 +83215,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 692 */ +/* 697 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83100,7 +83267,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 693 */ +/* 698 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83131,7 +83298,7 @@ exports.default = FileSystem; /***/ }), -/* 694 */ +/* 699 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83151,9 +83318,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var readdir = __webpack_require__(671); -var reader_1 = __webpack_require__(684); -var fs_stream_1 = __webpack_require__(688); +var readdir = __webpack_require__(676); +var reader_1 = __webpack_require__(689); +var fs_stream_1 = __webpack_require__(693); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -83221,7 +83388,7 @@ exports.default = ReaderStream; /***/ }), -/* 695 */ +/* 700 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83240,9 +83407,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(671); -var reader_1 = __webpack_require__(684); -var fs_sync_1 = __webpack_require__(696); +var readdir = __webpack_require__(676); +var reader_1 = __webpack_require__(689); +var fs_sync_1 = __webpack_require__(701); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -83302,7 +83469,7 @@ exports.default = ReaderSync; /***/ }), -/* 696 */ +/* 701 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83321,8 +83488,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(689); -var fs_1 = __webpack_require__(693); +var fsStat = __webpack_require__(694); +var fs_1 = __webpack_require__(698); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -83368,7 +83535,7 @@ exports.default = FileSystemSync; /***/ }), -/* 697 */ +/* 702 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83384,7 +83551,7 @@ exports.flatten = flatten; /***/ }), -/* 698 */ +/* 703 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83405,13 +83572,13 @@ exports.merge = merge; /***/ }), -/* 699 */ +/* 704 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathType = __webpack_require__(700); +const pathType = __webpack_require__(705); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -83477,13 +83644,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 700 */ +/* 705 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const pify = __webpack_require__(701); +const pify = __webpack_require__(706); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -83526,7 +83693,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 701 */ +/* 706 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83617,17 +83784,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 702 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(513); -const gitIgnore = __webpack_require__(703); -const pify = __webpack_require__(704); -const slash = __webpack_require__(705); +const fastGlob = __webpack_require__(518); +const gitIgnore = __webpack_require__(708); +const pify = __webpack_require__(709); +const slash = __webpack_require__(710); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -83725,7 +83892,7 @@ module.exports.sync = options => { /***/ }), -/* 703 */ +/* 708 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -84194,7 +84361,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 704 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84269,7 +84436,7 @@ module.exports = (input, options) => { /***/ }), -/* 705 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84287,7 +84454,7 @@ module.exports = input => { /***/ }), -/* 706 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84300,7 +84467,7 @@ module.exports = input => { -var isGlob = __webpack_require__(707); +var isGlob = __webpack_require__(712); module.exports = function hasGlob(val) { if (val == null) return false; @@ -84320,7 +84487,7 @@ module.exports = function hasGlob(val) { /***/ }), -/* 707 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -84351,17 +84518,17 @@ module.exports = function isGlob(str) { /***/ }), -/* 708 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); const {constants: fsConstants} = __webpack_require__(134); -const pEvent = __webpack_require__(709); -const CpFileError = __webpack_require__(712); -const fs = __webpack_require__(714); -const ProgressEmitter = __webpack_require__(717); +const pEvent = __webpack_require__(714); +const CpFileError = __webpack_require__(717); +const fs = __webpack_require__(719); +const ProgressEmitter = __webpack_require__(722); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -84475,12 +84642,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 709 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(710); +const pTimeout = __webpack_require__(715); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -84771,12 +84938,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 710 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(711); +const pFinally = __webpack_require__(716); class TimeoutError extends Error { constructor(message) { @@ -84822,7 +84989,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 711 */ +/* 716 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84844,12 +85011,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 712 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(713); +const NestedError = __webpack_require__(718); class CpFileError extends NestedError { constructor(message, nested) { @@ -84863,7 +85030,7 @@ module.exports = CpFileError; /***/ }), -/* 713 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(112).inherits; @@ -84919,16 +85086,16 @@ module.exports = NestedError; /***/ }), -/* 714 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(112); const fs = __webpack_require__(133); -const makeDir = __webpack_require__(715); -const pEvent = __webpack_require__(709); -const CpFileError = __webpack_require__(712); +const makeDir = __webpack_require__(720); +const pEvent = __webpack_require__(714); +const CpFileError = __webpack_require__(717); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -85025,7 +85192,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 715 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85033,7 +85200,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(134); const path = __webpack_require__(4); const {promisify} = __webpack_require__(112); -const semver = __webpack_require__(716); +const semver = __webpack_require__(721); const useNativeRecursiveOption = semver.satisfies(process.version, '>=10.12.0'); @@ -85188,7 +85355,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 716 */ +/* 721 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -86790,7 +86957,7 @@ function coerce (version, options) { /***/ }), -/* 717 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86831,7 +86998,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 718 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86877,12 +87044,12 @@ exports.default = module.exports; /***/ }), -/* 719 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(720); +const pMap = __webpack_require__(725); const pFilter = async (iterable, filterer, options) => { const values = await pMap( @@ -86899,7 +87066,7 @@ module.exports.default = pFilter; /***/ }), -/* 720 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86978,12 +87145,12 @@ module.exports.default = pMap; /***/ }), -/* 721 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(713); +const NestedError = __webpack_require__(718); class CpyError extends NestedError { constructor(message, nested) { diff --git a/scripts/kibana_encryption_keys.js b/scripts/kibana_encryption_keys.js new file mode 100644 index 0000000000000..a51f7e975c972 --- /dev/null +++ b/scripts/kibana_encryption_keys.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/cli_encryption_keys/dev'); diff --git a/src/cli_encryption_keys/__snapshots__/interactive.test.js.snap b/src/cli_encryption_keys/__snapshots__/interactive.test.js.snap new file mode 100644 index 0000000000000..14c15513d4000 --- /dev/null +++ b/src/cli_encryption_keys/__snapshots__/interactive.test.js.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`encryption key generation interactive should write to disk partial keys 1`] = ` +Array [ + Array [ + "/foo/bar", + "#xpack.encryptedSavedObjects.encryptionKey + #Used to encrypt stored objects such as dashboards and visualizations + #https://www.elastic.co/guide/en/kibana/current/xpack-security-secure-saved-objects.html#xpack-security-secure-saved-objects + +#xpack.reporting.encryptionKey + #Used to encrypt saved reports + #https://www.elastic.co/guide/en/kibana/current/reporting-settings-kb.html#general-reporting-settings + +#xpack.security.encryptionKey + #Used to encrypt session information + #https://www.elastic.co/guide/en/kibana/current/security-settings-kb.html#security-session-and-cookie-settings + +xpack.encryptedSavedObjects.encryptionKey: random-key +", + ], +] +`; diff --git a/src/cli_encryption_keys/cli_encryption_keys.js b/src/cli_encryption_keys/cli_encryption_keys.js new file mode 100644 index 0000000000000..30114f533aa30 --- /dev/null +++ b/src/cli_encryption_keys/cli_encryption_keys.js @@ -0,0 +1,56 @@ +/* + * 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 { pkg } from '../core/server/utils'; +import Command from '../cli/command'; +import { EncryptionConfig } from './encryption_config'; + +import { generateCli } from './generate'; + +const argv = process.env.kbnWorkerArgv + ? JSON.parse(process.env.kbnWorkerArgv) + : process.argv.slice(); +const program = new Command('bin/kibana-encryption-keys'); + +program.version(pkg.version).description('A tool for managing encryption keys'); + +const encryptionConfig = new EncryptionConfig(); + +generateCli(program, encryptionConfig); + +program + .command('help ') + .description('Get the help for a specific command') + .action(function (cmdName) { + const cmd = Object.values(program.commands).find((command) => command._name === cmdName); + if (!cmd) return program.error(`unknown command ${cmdName}`); + cmd.help(); + }); + +program.command('*', null, { noHelp: true }).action(function (cmd) { + program.error(`unknown command ${cmd}`); +}); + +// check for no command name +const subCommand = argv[2] && !String(argv[2][0]).match(/^-|^\.|\//); +if (!subCommand) { + program.defaultHelp(); +} + +program.parse(process.argv); diff --git a/src/cli_encryption_keys/dev.js b/src/cli_encryption_keys/dev.js new file mode 100644 index 0000000000000..544374f6107a8 --- /dev/null +++ b/src/cli_encryption_keys/dev.js @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('../setup_node_env'); +require('./cli_encryption_keys'); diff --git a/src/cli_encryption_keys/dist.js b/src/cli_encryption_keys/dist.js new file mode 100644 index 0000000000000..1c0ed01e65506 --- /dev/null +++ b/src/cli_encryption_keys/dist.js @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('../setup_node_env/dist'); +require('./cli_encryption_keys'); diff --git a/src/cli_encryption_keys/encryption_config.js b/src/cli_encryption_keys/encryption_config.js new file mode 100644 index 0000000000000..f5cf4ba0b037e --- /dev/null +++ b/src/cli_encryption_keys/encryption_config.js @@ -0,0 +1,86 @@ +/* + * 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 crypto from 'crypto'; +import { join } from 'path'; +import { get } from 'lodash'; +import { readFileSync } from 'fs'; +import { safeLoad } from 'js-yaml'; + +import { getConfigDirectory } from '@kbn/utils'; + +export class EncryptionConfig { + #config = safeLoad(readFileSync(join(getConfigDirectory(), 'kibana.yml'))); + #encryptionKeyPaths = [ + 'xpack.encryptedSavedObjects.encryptionKey', + 'xpack.reporting.encryptionKey', + 'xpack.security.encryptionKey', + ]; + #encryptionMeta = { + 'xpack.encryptedSavedObjects.encryptionKey': { + docs: + 'https://www.elastic.co/guide/en/kibana/current/xpack-security-secure-saved-objects.html#xpack-security-secure-saved-objects', + description: 'Used to encrypt stored objects such as dashboards and visualizations', + }, + 'xpack.reporting.encryptionKey': { + docs: + 'https://www.elastic.co/guide/en/kibana/current/reporting-settings-kb.html#general-reporting-settings', + description: 'Used to encrypt saved reports', + }, + 'xpack.security.encryptionKey': { + docs: + 'https://www.elastic.co/guide/en/kibana/current/security-settings-kb.html#security-session-and-cookie-settings', + description: 'Used to encrypt session information', + }, + }; + + _getEncryptionKey(key) { + return get(this.#config, key); + } + + _hasEncryptionKey(key) { + return !!get(this.#config, key); + } + + _generateEncryptionKey() { + return crypto.randomBytes(16).toString('hex'); + } + + docs({ comment } = {}) { + const commentString = comment ? '#' : ''; + let docs = ''; + this.#encryptionKeyPaths.forEach((key) => { + docs += `${commentString}${key} + ${commentString}${this.#encryptionMeta[key].description} + ${commentString}${this.#encryptionMeta[key].docs} +\n`; + }); + return docs; + } + + generate({ force = false }) { + const output = {}; + this.#encryptionKeyPaths.forEach((key) => { + if (force || !this._hasEncryptionKey(key)) { + output[key] = this._generateEncryptionKey(); + } + }); + return output; + } +} diff --git a/src/cli_encryption_keys/encryption_config.test.js b/src/cli_encryption_keys/encryption_config.test.js new file mode 100644 index 0000000000000..60220d0270b4e --- /dev/null +++ b/src/cli_encryption_keys/encryption_config.test.js @@ -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. + */ +/* + * 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 { EncryptionConfig } from './encryption_config'; +import crypto from 'crypto'; +import fs from 'fs'; + +describe('encryption key configuration', () => { + let encryptionConfig = null; + + beforeEach(() => { + jest.spyOn(fs, 'readFileSync').mockReturnValue('xpack.security.encryptionKey: foo'); + jest.spyOn(crypto, 'randomBytes').mockReturnValue('random-key'); + encryptionConfig = new EncryptionConfig(); + }); + it('should be able to check for encryption keys', () => { + expect(encryptionConfig._hasEncryptionKey('xpack.reporting.encryptionKey')).toEqual(false); + expect(encryptionConfig._hasEncryptionKey('xpack.security.encryptionKey')).toEqual(true); + }); + + it('should be able to get encryption keys', () => { + expect(encryptionConfig._getEncryptionKey('xpack.reporting.encryptionKey')).toBeUndefined(); + expect(encryptionConfig._getEncryptionKey('xpack.security.encryptionKey')).toEqual('foo'); + }); + + it('should generate a key', () => { + expect(encryptionConfig._generateEncryptionKey()).toEqual('random-key'); + }); + + it('should only generate unset keys', () => { + const output = encryptionConfig.generate({ force: false }); + expect(output['xpack.security.encryptionKey']).toEqual(undefined); + expect(output['xpack.reporting.encryptionKey']).toEqual('random-key'); + }); + + it('should regenerate all keys if the force flag is set', () => { + const output = encryptionConfig.generate({ force: true }); + expect(output['xpack.security.encryptionKey']).toEqual('random-key'); + expect(output['xpack.reporting.encryptionKey']).toEqual('random-key'); + expect(output['xpack.encryptedSavedObjects.encryptionKey']).toEqual('random-key'); + }); + + it('should set encryptedObjects and reporting with a default configuration', () => { + const output = encryptionConfig.generate({}); + expect(output['xpack.security.encryptionKey']).toBeUndefined(); + expect(output['xpack.encryptedSavedObjects.encryptionKey']).toEqual('random-key'); + expect(output['xpack.reporting.encryptionKey']).toEqual('random-key'); + }); +}); diff --git a/src/cli_encryption_keys/generate.js b/src/cli_encryption_keys/generate.js new file mode 100644 index 0000000000000..a47fa6add6e3b --- /dev/null +++ b/src/cli_encryption_keys/generate.js @@ -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 { safeDump } from 'js-yaml'; +import { isEmpty } from 'lodash'; +import { interactive } from './interactive'; +import { Logger } from '../cli_plugin/lib/logger'; + +export async function generate(encryptionConfig, command) { + const logger = new Logger(); + const keys = encryptionConfig.generate({ force: command.force }); + if (isEmpty(keys)) { + logger.log('No keys to write. Use the --force flag to generate new keys.'); + } else { + if (!command.quiet) { + logger.log('## Kibana Encryption Key Generation Utility\n'); + logger.log( + `The 'generate' command guides you through the process of setting encryption keys for:\n` + ); + logger.log(encryptionConfig.docs()); + logger.log( + 'Already defined settings are ignored and can be regenerated using the --force flag. Check the documentation links for instructions on how to rotate encryption keys.' + ); + logger.log('Definitions should be set in the kibana.yml used configure Kibana.\n'); + } + if (command.interactive) { + await interactive(keys, encryptionConfig.docs({ comment: true }), logger); + } else { + if (!command.quiet) logger.log('Settings:'); + logger.log(safeDump(keys)); + } + } +} + +export function generateCli(program, encryptionConfig) { + program + .command('generate') + .description('Generates encryption keys') + .option('-i, --interactive', 'interactive output') + .option('-q, --quiet', 'do not include instructions') + .option('-f, --force', 'generate new keys for all settings') + .action(generate.bind(null, encryptionConfig)); +} diff --git a/src/cli_encryption_keys/generate.test.js b/src/cli_encryption_keys/generate.test.js new file mode 100644 index 0000000000000..65fb8ebc028f1 --- /dev/null +++ b/src/cli_encryption_keys/generate.test.js @@ -0,0 +1,56 @@ +/* + * 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 { EncryptionConfig } from './encryption_config'; +import { generate } from './generate'; + +import { Logger } from '../cli_plugin/lib/logger'; + +describe('encryption key generation', () => { + const encryptionConfig = new EncryptionConfig(); + beforeEach(() => { + Logger.prototype.log = jest.fn(); + }); + + it('should generate a new encryption config', () => { + const command = { + force: false, + interactive: false, + quiet: false, + }; + generate(encryptionConfig, command); + const keys = Logger.prototype.log.mock.calls[6][0]; + expect(keys.search('xpack.encryptedSavedObjects.encryptionKey')).toBeGreaterThanOrEqual(0); + expect(keys.search('xpack.reporting.encryptionKey')).toBeGreaterThanOrEqual(0); + expect(keys.search('xpack.security.encryptionKey')).toBeGreaterThanOrEqual(0); + expect(keys.search('foo.bar')).toEqual(-1); + }); + + it('should only output keys if the quiet flag is set', () => { + generate(encryptionConfig, { quiet: true }); + const keys = Logger.prototype.log.mock.calls[0][0]; + const nextLog = Logger.prototype.log.mock.calls[1]; + expect(keys.search('xpack.encryptedSavedObjects.encryptionKey')).toBeGreaterThanOrEqual(0); + expect(nextLog).toEqual(undefined); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); +}); diff --git a/src/cli_encryption_keys/interactive.js b/src/cli_encryption_keys/interactive.js new file mode 100644 index 0000000000000..c5d716077672d --- /dev/null +++ b/src/cli_encryption_keys/interactive.js @@ -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 { writeFileSync } from 'fs'; +import { join } from 'path'; +import { confirm, question } from '../cli_keystore/utils'; +import { getConfigDirectory } from '@kbn/utils'; +import { safeDump } from 'js-yaml'; + +export async function interactive(keys, docs, logger) { + const settings = Object.keys(keys); + logger.log( + 'This tool will ask you a number of questions in order to generate the right set of keys for your needs.\n' + ); + const setKeys = {}; + for (const setting of settings) { + const include = await confirm(`Set ${setting}?`); + if (include) setKeys[setting] = keys[setting]; + } + const count = Object.keys(setKeys).length; + const plural = count > 1 ? 's were' : ' was'; + logger.log(''); + if (!count) return logger.log('No keys were generated'); + logger.log(`The following key${plural} generated:`); + logger.log(Object.keys(setKeys).join('\n')); + logger.log(''); + const write = await confirm('Save generated keys to a sample Kibana configuration file?'); + if (write) { + const defaultSaveLocation = join(getConfigDirectory(), 'kibana.sample.yml'); + const promptedSaveLocation = await question( + `What filename should be used for the sample Kibana config file? [${defaultSaveLocation}])` + ); + const saveLocation = promptedSaveLocation || defaultSaveLocation; + writeFileSync(saveLocation, docs + safeDump(setKeys)); + logger.log(`Wrote configuration to ${saveLocation}`); + } else { + logger.log('\nSettings:'); + logger.log(safeDump(setKeys)); + } +} diff --git a/src/cli_encryption_keys/interactive.test.js b/src/cli_encryption_keys/interactive.test.js new file mode 100644 index 0000000000000..cba722d85c545 --- /dev/null +++ b/src/cli_encryption_keys/interactive.test.js @@ -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 { EncryptionConfig } from './encryption_config'; +import { generate } from './generate'; + +import { Logger } from '../cli_plugin/lib/logger'; +import * as prompt from '../cli_keystore/utils/prompt'; +import fs from 'fs'; +import crypto from 'crypto'; + +describe('encryption key generation interactive', () => { + const encryptionConfig = new EncryptionConfig(); + beforeEach(() => { + Logger.prototype.log = jest.fn(); + }); + + it('should prompt the user to write keys if the interactive flag is set', async () => { + jest + .spyOn(prompt, 'confirm') + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(false); + jest.spyOn(prompt, 'question'); + + await generate(encryptionConfig, { interactive: true }); + expect(prompt.confirm.mock.calls).toEqual([ + ['Set xpack.encryptedSavedObjects.encryptionKey?'], + ['Set xpack.reporting.encryptionKey?'], + ['Set xpack.security.encryptionKey?'], + ['Save generated keys to a sample Kibana configuration file?'], + ]); + expect(prompt.question).not.toHaveBeenCalled(); + }); + + it('should write to disk partial keys', async () => { + jest + .spyOn(prompt, 'confirm') + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(true); + jest.spyOn(prompt, 'question').mockResolvedValue('/foo/bar'); + jest.spyOn(crypto, 'randomBytes').mockReturnValue('random-key'); + fs.writeFileSync = jest.fn(); + await generate(encryptionConfig, { interactive: true }); + expect(fs.writeFileSync.mock.calls).toMatchSnapshot(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); +}); diff --git a/src/dev/build/tasks/bin/scripts/kibana-encryption-keys b/src/dev/build/tasks/bin/scripts/kibana-encryption-keys new file mode 100755 index 0000000000000..5df19558214d3 --- /dev/null +++ b/src/dev/build/tasks/bin/scripts/kibana-encryption-keys @@ -0,0 +1,29 @@ +#!/bin/sh +SCRIPT=$0 + +# SCRIPT may be an arbitrarily deep series of symlinks. Loop until we have the concrete path. +while [ -h "$SCRIPT" ] ; do + ls=$(ls -ld "$SCRIPT") + # Drop everything prior to -> + link=$(expr "$ls" : '.*-> \(.*\)$') + if expr "$link" : '/.*' > /dev/null; then + SCRIPT="$link" + else + SCRIPT=$(dirname "$SCRIPT")/"$link" + fi +done + +DIR="$(dirname "${SCRIPT}")/.." +CONFIG_DIR=${KBN_PATH_CONF:-"$DIR/config"} +NODE="${DIR}/node/bin/node" +test -x "$NODE" +if [ ! -x "$NODE" ]; then + echo "unable to find usable node.js executable." + exit 1 +fi + +if [ -f "${CONFIG_DIR}/node.options" ]; then + KBN_NODE_OPTS="$(grep -v ^# < ${CONFIG_DIR}/node.options | xargs)" +fi + +NODE_OPTIONS="$KBN_NODE_OPTS $NODE_OPTIONS" "${NODE}" "${DIR}/src/cli_encryption_keys/dist" "$@" diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 9f445b0c05be9..654c3f9948a18 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -27,6 +27,7 @@ export default { '/src/legacy/server', '/src/cli', '/src/cli_keystore', + '/src/cli_encryption_keys', '/src/cli_plugin', '/packages/kbn-test/target/functional_test_runner', '/src/dev', diff --git a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap index afaa2d00d8cfd..3e09fa449a1aa 100644 --- a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap +++ b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap @@ -47,7 +47,7 @@ Object { ], }, "count": 1, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "text", ], diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index 850c5a312fda1..4dd2d29f38e9f 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -68,12 +68,12 @@ export class IndexPatternField implements IFieldType { this.spec.lang = lang; } - public get customName() { - return this.spec.customName; + public get customLabel() { + return this.spec.customLabel; } - public set customName(label) { - this.spec.customName = label; + public set customLabel(customLabel) { + this.spec.customLabel = customLabel; } /** @@ -93,8 +93,8 @@ export class IndexPatternField implements IFieldType { } public get displayName(): string { - return this.spec.customName - ? this.spec.customName + return this.spec.customLabel + ? this.spec.customLabel : this.spec.shortDotsEnable ? shortenDottedString(this.spec.name) : this.spec.name; @@ -163,7 +163,7 @@ export class IndexPatternField implements IFieldType { aggregatable: this.aggregatable, readFromDocValues: this.readFromDocValues, subType: this.subType, - customName: this.customName, + customLabel: this.customLabel, }; } @@ -186,7 +186,7 @@ export class IndexPatternField implements IFieldType { readFromDocValues: this.readFromDocValues, subType: this.subType, format: getFormatterForField ? getFormatterForField(this).toJSON() : undefined, - customName: this.customName, + customLabel: this.customLabel, shortDotsEnable: this.spec.shortDotsEnable, }; } diff --git a/src/plugins/data/common/index_patterns/fields/types.ts b/src/plugins/data/common/index_patterns/fields/types.ts index 86c22b0116ead..1c70a2e884025 100644 --- a/src/plugins/data/common/index_patterns/fields/types.ts +++ b/src/plugins/data/common/index_patterns/fields/types.ts @@ -37,7 +37,7 @@ export interface IFieldType { scripted?: boolean; subType?: IFieldSubType; displayName?: string; - customName?: string; + customLabel?: string; format?: any; toSpec?: (options?: { getFormatterForField?: IndexPattern['getFormatterForField'] }) => FieldSpec; } diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap index 2741322acec0f..e2bdb0009c20a 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap @@ -9,7 +9,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "keyword", ], @@ -33,7 +33,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 30, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "date", ], @@ -57,7 +57,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "_id", ], @@ -81,7 +81,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "_source", ], @@ -105,7 +105,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "_type", ], @@ -129,7 +129,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "geo_shape", ], @@ -153,7 +153,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 10, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "long", ], @@ -177,7 +177,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "conflict", ], @@ -201,7 +201,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "text", ], @@ -225,7 +225,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "keyword", ], @@ -253,7 +253,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "geo_point", ], @@ -277,7 +277,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "keyword", ], @@ -301,7 +301,7 @@ Object { "aggregatable": false, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "murmur3", ], @@ -325,7 +325,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "ip", ], @@ -349,7 +349,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "text", ], @@ -373,7 +373,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "keyword", ], @@ -401,7 +401,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "text", ], @@ -425,7 +425,7 @@ Object { "aggregatable": false, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "text", ], @@ -449,7 +449,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "integer", ], @@ -473,7 +473,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "geo_point", ], @@ -497,7 +497,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "attachment", ], @@ -521,7 +521,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "date", ], @@ -545,7 +545,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "murmur3", ], @@ -569,7 +569,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "long", ], @@ -593,7 +593,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "text", ], @@ -617,7 +617,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 20, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "boolean", ], @@ -641,7 +641,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 30, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "date", ], @@ -665,7 +665,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, - "customName": undefined, + "customLabel": undefined, "esTypes": Array [ "date", ], 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 c3a0c98745e21..47ad5860801bc 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 @@ -135,8 +135,8 @@ export class IndexPattern implements IIndexPattern { const newFieldAttrs = { ...this.fieldAttrs }; this.fields.forEach((field) => { - if (field.customName) { - newFieldAttrs[field.name] = { customName: field.customName }; + if (field.customLabel) { + newFieldAttrs[field.name] = { customLabel: field.customLabel }; } else { delete newFieldAttrs[field.name]; } 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 d51de220111e3..82c8cf4abc5ac 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 @@ -309,7 +309,7 @@ export class IndexPatternsService { */ fieldArrayToMap = (fields: FieldSpec[], fieldAttrs?: FieldAttrs) => fields.reduce((collector, field) => { - collector[field.name] = { ...field, customName: fieldAttrs?.[field.name]?.customName }; + collector[field.name] = { ...field, customLabel: fieldAttrs?.[field.name]?.customLabel }; return collector; }, {}); diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 22c400562f6d4..28b077f4bfdf3 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -52,7 +52,7 @@ export interface IndexPatternAttributes { } export interface FieldAttrs { - [key: string]: { customName: string }; + [key: string]: { customLabel: string }; } export type OnNotification = (toastInputFields: ToastInputFields) => void; @@ -169,7 +169,7 @@ export interface FieldSpec { readFromDocValues?: boolean; subType?: IFieldSubType; indexed?: boolean; - customName?: string; + customLabel?: string; // not persisted shortDotsEnable?: boolean; } diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 6c4609e5506c2..fc9b8d4839ea3 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -978,7 +978,7 @@ export interface IFieldType { // (undocumented) count?: number; // (undocumented) - customName?: string; + customLabel?: string; // (undocumented) displayName?: string; // (undocumented) @@ -1152,7 +1152,7 @@ export class IndexPattern implements IIndexPattern { // (undocumented) getFieldAttrs: () => { [x: string]: { - customName: string; + customLabel: string; }; }; // (undocumented) @@ -1259,8 +1259,8 @@ export class IndexPatternField implements IFieldType { get count(): number; set count(count: number); // (undocumented) - get customName(): string | undefined; - set customName(label: string | undefined); + get customLabel(): string | undefined; + set customLabel(customLabel: string | undefined); // (undocumented) get displayName(): string; // (undocumented) @@ -1299,7 +1299,7 @@ export class IndexPatternField implements IFieldType { aggregatable: boolean; readFromDocValues: boolean; subType: import("../types").IFieldSubType | undefined; - customName: string | undefined; + customLabel: string | undefined; }; // (undocumented) toSpec({ getFormatterForField, }?: { diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 8d1699c4ad5ed..47e17c26398d3 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -507,7 +507,7 @@ export interface IFieldType { // (undocumented) count?: number; // (undocumented) - customName?: string; + customLabel?: string; // (undocumented) displayName?: string; // (undocumented) @@ -612,7 +612,7 @@ export class IndexPattern implements IIndexPattern { // (undocumented) getFieldAttrs: () => { [x: string]: { - customName: string; + customLabel: string; }; }; // (undocumented) diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx index 4b63eb5c56fd1..8dd95adf00cc8 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx @@ -151,9 +151,9 @@ const editDescription = i18n.translate( { defaultMessage: 'Edit' } ); -const customNameDescription = i18n.translate( - 'indexPatternManagement.editIndexPattern.fields.table.customNameTooltip', - { defaultMessage: 'A custom name for the field.' } +const labelDescription = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.table.customLabelTooltip', + { defaultMessage: 'A custom label for the field.' } ); interface IndexedFieldProps { @@ -197,11 +197,11 @@ export class Table extends PureComponent { /> ) : null} - {field.customName && field.customName !== field.name ? ( + {field.customLabel && field.customLabel !== field.name ? (
- + - {field.customName} + {field.customLabel}
diff --git a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap index babfbbfc2a763..29cbec38a5982 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap @@ -54,15 +54,15 @@ exports[`FieldEditor should render create new scripted field correctly 1`] = ` } - label="Custom name" + label="Custom label" > @@ -294,15 +294,15 @@ exports[`FieldEditor should render edit scripted field correctly 1`] = ` } - label="Custom name" + label="Custom label" > } - label="Custom name" + label="Custom label" > } - label="Custom name" + label="Custom label" > } - label="Custom name" + label="Custom label" > { expect(component).toMatchSnapshot(); }); - it('should display and update a customName correctly', async () => { + it('should display and update a custom label correctly', async () => { let testField = ({ name: 'test', format: new Format(), lang: undefined, type: 'string', - customName: 'Test', + customLabel: 'Test', } as unknown) as IndexPatternField; fieldList.push(testField); indexPattern.fields.getByName = (name) => { @@ -219,14 +219,14 @@ describe('FieldEditor', () => { await new Promise((resolve) => process.nextTick(resolve)); component.update(); - const input = findTestSubject(component, 'editorFieldCustomName'); + const input = findTestSubject(component, 'editorFieldCustomLabel'); expect(input.props().value).toBe('Test'); input.simulate('change', { target: { value: 'new Test' } }); const saveBtn = findTestSubject(component, 'fieldSaveButton'); await saveBtn.simulate('click'); await new Promise((resolve) => process.nextTick(resolve)); - expect(testField.customName).toEqual('new Test'); + expect(testField.customLabel).toEqual('new Test'); }); it('should show deprecated lang warning', async () => { diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx index 97d30d88e018c..29a87a65fdff7 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx @@ -126,7 +126,7 @@ export interface FieldEditorState { errors?: string[]; format: any; spec: IndexPatternField['spec']; - customName: string; + customLabel: string; } export interface FieldEdiorProps { @@ -167,7 +167,7 @@ export class FieldEditor extends PureComponent } > { - this.setState({ customName: e.target.value }); + this.setState({ customLabel: e.target.value }); }} /> @@ -802,7 +802,7 @@ export class FieldEditor extends PureComponent { const field = this.state.spec; const { indexPattern } = this.props; - const { fieldFormatId, fieldFormatParams, customName } = this.state; + const { fieldFormatId, fieldFormatParams, customLabel } = this.state; if (field.scripted) { this.setState({ @@ -843,8 +843,8 @@ export class FieldEditor extends PureComponent {this.renderScriptingPanels()} {this.renderName()} - {this.renderCustomName()} + {this.renderCustomLabel()} {this.renderLanguage()} {this.renderType()} {this.renderTypeConflict()} diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js index 83ddc23648ad3..feda9fd239a66 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js @@ -23,10 +23,10 @@ import { includes } from 'lodash'; import { injectI18n } from '@kbn/i18n/react'; import { EuiComboBox } from '@elastic/eui'; import { calculateSiblings } from '../lib/calculate_siblings'; -import { calculateLabel } from '../../../../../../plugins/vis_type_timeseries/common/calculate_label'; -import { basicAggs } from '../../../../../../plugins/vis_type_timeseries/common/basic_aggs'; -import { toPercentileNumber } from '../../../../../../plugins/vis_type_timeseries/common/to_percentile_number'; -import { METRIC_TYPES } from '../../../../../../plugins/vis_type_timeseries/common/metric_types'; +import { calculateLabel } from '../../../../common/calculate_label'; +import { basicAggs } from '../../../../common/basic_aggs'; +import { toPercentileNumber } from '../../../../common/to_percentile_number'; +import { METRIC_TYPES } from '../../../../common/metric_types'; function createTypeFilter(restrict, exclude) { return (metric) => { diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js index fb945d2606bc8..48b6f6192a93c 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js @@ -37,7 +37,7 @@ import { EuiFieldNumber, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { MODEL_TYPES } from '../../../../../../plugins/vis_type_timeseries/common/model_options'; +import { MODEL_TYPES } from '../../../../common/model_options'; const DEFAULTS = { model_type: MODEL_TYPES.UNWEIGHTED, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js index c63beee222b17..1969147efde9a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js @@ -36,7 +36,7 @@ import { } from '@elastic/eui'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; -import { PANEL_TYPES } from '../../../../../../plugins/vis_type_timeseries/common/panel_types'; +import { PANEL_TYPES } from '../../../../common/panel_types'; const isFieldTypeEnabled = (fieldRestrictions, fieldType) => fieldRestrictions.length ? fieldRestrictions.includes(fieldType) : true; diff --git a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js index 30c6d5b51d187..85f31285df69b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js +++ b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js @@ -42,11 +42,8 @@ import { AUTO_INTERVAL, } from './lib/get_interval'; import { i18n } from '@kbn/i18n'; -import { - TIME_RANGE_DATA_MODES, - TIME_RANGE_MODE_KEY, -} from '../../../../../plugins/vis_type_timeseries/common/timerange_data_modes'; -import { PANEL_TYPES } from '../../../../../plugins/vis_type_timeseries/common/panel_types'; +import { TIME_RANGE_DATA_MODES, TIME_RANGE_MODE_KEY } from '../../../common/timerange_data_modes'; +import { PANEL_TYPES } from '../../../common/panel_types'; import { isTimerangeModeEnabled } from '../lib/check_ui_restrictions'; import { VisDataContext } from '../contexts/vis_data_context'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js index 0f64c570088d7..66783f5ef2715 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js @@ -19,7 +19,7 @@ import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; -import { getLastValue } from '../../../../../../plugins/vis_type_timeseries/common/get_last_value'; +import { getLastValue } from '../../../../common/get_last_value'; import { createTickFormatter } from './tick_formatter'; import { labelDateFormatter } from './label_date_formatter'; import moment from 'moment'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js index 86361afca3b12..c1d484765f4cb 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; import { search } from '../../../../../../plugins/data/public'; const { parseEsInterval } = search.aggs; -import { GTE_INTERVAL_RE } from '../../../../../../plugins/vis_type_timeseries/common/interval_regexp'; +import { GTE_INTERVAL_RE } from '../../../../common/interval_regexp'; export const AUTO_INTERVAL = 'auto'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js index 146e7a4bae15a..f8b6f19ac21a2 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js @@ -18,7 +18,7 @@ */ import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; -import { METRIC_TYPES } from '../../../../../../plugins/vis_type_timeseries/common/metric_types'; +import { METRIC_TYPES } from '../../../../common/metric_types'; export function getSupportedFieldsByMetricType(type) { switch (type) { diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js b/src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js index 0638c6e67f5ef..b6b99d7782762 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js @@ -19,7 +19,7 @@ import _ from 'lodash'; import { newMetricAggFn } from './new_metric_agg_fn'; -import { isBasicAgg } from '../../../../../../plugins/vis_type_timeseries/common/agg_lookup'; +import { isBasicAgg } from '../../../../common/agg_lookup'; import { handleAdd, handleChange } from './collection_actions'; export const seriesChangeHandler = (props, items) => (doc) => { diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js index a72c7598509a8..fe6c89ea6985b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js +++ b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js @@ -36,7 +36,7 @@ import { EuiFieldText, } from '@elastic/eui'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; -import { FIELD_TYPES } from '../../../../../../plugins/vis_type_timeseries/common/field_types'; +import { FIELD_TYPES } from '../../../../common/field_types'; import { STACKED_OPTIONS } from '../../visualizations/constants'; const DEFAULTS = { terms_direction: 'desc', terms_size: 10, terms_order_by: '_count' }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js index 47b30f9ab2711..57adecd9d598b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js @@ -28,7 +28,7 @@ import { VisPicker } from './vis_picker'; import { PanelConfig } from './panel_config'; import { createBrushHandler } from '../lib/create_brush_handler'; import { fetchFields } from '../lib/fetch_fields'; -import { extractIndexPatterns } from '../../../../../plugins/vis_type_timeseries/common/extract_index_patterns'; +import { extractIndexPatterns } from '../../../common/extract_index_patterns'; import { getSavedObjectsClient, getUISettings, getDataStart, getCoreStart } from '../../services'; import { CoreStartContextProvider } from '../contexts/query_input_bar_context'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js index 9c2b947bda08e..9742d817f7c0d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js @@ -28,7 +28,7 @@ import { isGteInterval, AUTO_INTERVAL, } from './lib/get_interval'; -import { PANEL_TYPES } from '../../../../../plugins/vis_type_timeseries/common/panel_types'; +import { PANEL_TYPES } from '../../../common/panel_types'; const MIN_CHART_HEIGHT = 300; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js b/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js index c33ed02eadebd..79f5c7abca270 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js @@ -21,7 +21,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { EuiTabs, EuiTab } from '@elastic/eui'; import { injectI18n } from '@kbn/i18n/react'; -import { PANEL_TYPES } from '../../../../../plugins/vis_type_timeseries/common/panel_types'; +import { PANEL_TYPES } from '../../../common/panel_types'; function VisPickerItem(props) { const { label, type, selected } = props; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js index 4c029f1c0d5b0..325e9c8372736 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js @@ -23,7 +23,7 @@ import { visWithSplits } from '../../vis_with_splits'; import { createTickFormatter } from '../../lib/tick_formatter'; import _, { get, isUndefined, assign, includes } from 'lodash'; import { Gauge } from '../../../visualizations/views/gauge'; -import { getLastValue } from '../../../../../../../plugins/vis_type_timeseries/common/get_last_value'; +import { getLastValue } from '../../../../../common/get_last_value'; function getColors(props) { const { model, visData } = props; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js index f37971e990c96..5fe7afe47df9b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js @@ -23,7 +23,7 @@ import { visWithSplits } from '../../vis_with_splits'; import { createTickFormatter } from '../../lib/tick_formatter'; import _, { get, isUndefined, assign, includes, pick } from 'lodash'; import { Metric } from '../../../visualizations/views/metric'; -import { getLastValue } from '../../../../../../../plugins/vis_type_timeseries/common/get_last_value'; +import { getLastValue } from '../../../../../common/get_last_value'; import { isBackgroundInverted } from '../../../lib/set_is_reversed'; function getColors(props) { diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/is_sortable.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/is_sortable.js index b44c94131348d..099dbe6639737 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/is_sortable.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/is_sortable.js @@ -17,7 +17,7 @@ * under the License. */ -import { basicAggs } from '../../../../../../../plugins/vis_type_timeseries/common/basic_aggs'; +import { basicAggs } from '../../../../../common/basic_aggs'; export function isSortable(metric) { return basicAggs.includes(metric.type); diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js index 1341cf02202a0..92109e1a37426 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js @@ -22,7 +22,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { RedirectAppLinks } from '../../../../../../kibana_react/public'; import { createTickFormatter } from '../../lib/tick_formatter'; -import { calculateLabel } from '../../../../../../../plugins/vis_type_timeseries/common/calculate_label'; +import { calculateLabel } from '../../../../../common/calculate_label'; import { isSortable } from './is_sortable'; import { EuiToolTip, EuiIcon } from '@elastic/eui'; import { replaceVars } from '../../lib/replace_vars'; @@ -30,7 +30,7 @@ import { fieldFormats } from '../../../../../../../plugins/data/public'; import { FormattedMessage } from '@kbn/i18n/react'; import { getFieldFormats, getCoreStart } from '../../../../services'; -import { METRIC_TYPES } from '../../../../../../../plugins/vis_type_timeseries/common/metric_types'; +import { METRIC_TYPES } from '../../../../../common/metric_types'; function getColor(rules, colorKey, value) { let color; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js index 680c1c5e78ad4..039763efc78a2 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js @@ -35,7 +35,7 @@ import { import { Split } from '../../split'; import { createTextHandler } from '../../lib/create_text_handler'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -import { PANEL_TYPES } from '../../../../../../../plugins/vis_type_timeseries/common/panel_types'; +import { PANEL_TYPES } from '../../../../../common/panel_types'; const TimeseriesSeriesUI = injectI18n(function (props) { const { diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js index e9f64c93d337f..1c2ebb8264ef3 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js @@ -20,7 +20,7 @@ import { getCoreStart } from '../../../../services'; import { createTickFormatter } from '../../lib/tick_formatter'; import { TopN } from '../../../visualizations/views/top_n'; -import { getLastValue } from '../../../../../../../plugins/vis_type_timeseries/common/get_last_value'; +import { getLastValue } from '../../../../../common/get_last_value'; import { isBackgroundInverted } from '../../../lib/set_is_reversed'; import { replaceVars } from '../../lib/replace_vars'; import PropTypes from 'prop-types'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js index f583d087e60ef..27891cdbb3943 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js @@ -21,7 +21,7 @@ import React from 'react'; import { getDisplayName } from './lib/get_display_name'; import { labelDateFormatter } from './lib/label_date_formatter'; import { last, findIndex, first } from 'lodash'; -import { calculateLabel } from '../../../../../plugins/vis_type_timeseries/common/calculate_label'; +import { calculateLabel } from '../../../common/calculate_label'; export function visWithSplits(WrappedComponent) { function SplitVisComponent(props) { diff --git a/src/plugins/vis_type_timeseries/public/application/lib/check_ui_restrictions.js b/src/plugins/vis_type_timeseries/public/application/lib/check_ui_restrictions.js index 5d18c0a2f09cd..d77f2f327b30d 100644 --- a/src/plugins/vis_type_timeseries/public/application/lib/check_ui_restrictions.js +++ b/src/plugins/vis_type_timeseries/public/application/lib/check_ui_restrictions.js @@ -18,10 +18,7 @@ */ import { get } from 'lodash'; -import { - RESTRICTIONS_KEYS, - DEFAULT_UI_RESTRICTION, -} from '../../../../../plugins/vis_type_timeseries/common/ui_restrictions'; +import { RESTRICTIONS_KEYS, DEFAULT_UI_RESTRICTION } from '../../../common/ui_restrictions'; /** * Generic method for checking all types of the UI Restrictions diff --git a/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js b/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js index e8ddb4ceb5cba..9448a29787097 100644 --- a/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js +++ b/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js @@ -17,7 +17,7 @@ * under the License. */ -import { GTE_INTERVAL_RE } from '../../../../../plugins/vis_type_timeseries/common/interval_regexp'; +import { GTE_INTERVAL_RE } from '../../../common/interval_regexp'; import { i18n } from '@kbn/i18n'; import { search } from '../../../../../plugins/data/public'; const { parseInterval } = search.aggs; diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js index 50a2042425438..0b9e191e4e29e 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js @@ -22,7 +22,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import classNames from 'classnames'; import { isBackgroundInverted, isBackgroundDark } from '../../lib/set_is_reversed'; -import { getLastValue } from '../../../../../../plugins/vis_type_timeseries/common/get_last_value'; +import { getLastValue } from '../../../../common/get_last_value'; import { getValueBy } from '../lib/get_value_by'; import { GaugeVis } from './gauge_vis'; import reactcss from 'reactcss'; diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js index 4c286f61720ac..7356726e6262f 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js @@ -20,8 +20,9 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import _ from 'lodash'; -import { getLastValue } from '../../../../../../plugins/vis_type_timeseries/common/get_last_value'; import reactcss from 'reactcss'; + +import { getLastValue } from '../../../../common/get_last_value'; import { calculateCoordinates } from '../lib/calculate_coordinates'; export class Metric extends Component { diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js index 136ac2506d392..9c6e497b92dab 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js @@ -19,7 +19,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { getLastValue } from '../../../../../../plugins/vis_type_timeseries/common/get_last_value'; +import { getLastValue } from '../../../../common/get_last_value'; import { labelDateFormatter } from '../../components/lib/label_date_formatter'; import reactcss from 'reactcss'; diff --git a/test/functional/fixtures/es_archiver/discover/data.json b/test/functional/fixtures/es_archiver/discover/data.json index 0f9820a6c2f6e..0f2edc8c510c3 100644 --- a/test/functional/fixtures/es_archiver/discover/data.json +++ b/test/functional/fixtures/es_archiver/discover/data.json @@ -8,7 +8,7 @@ "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@message\"}}},{\"name\":\"@tags\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@tags\"}}},{\"name\":\"@timestamp\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"agent\"}}},{\"name\":\"bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"extension\"}}},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"headings\"}}},{\"name\":\"host\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"host\"}}},{\"name\":\"id\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"index\"}}},{\"name\":\"ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"links\"}}},{\"name\":\"machine.os\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"machine.os\"}}},{\"name\":\"machine.ram\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"esTypes\":[\"double\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nestedField.child\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"nested\":{\"path\":\"nestedField\"}}},{\"name\":\"phpmemory\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:section\"}}},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:tag\"}}},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:description\"}}},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image\"}}},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:height\"}}},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:width\"}}},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:site_name\"}}},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:title\"}}},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:type\"}}},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:url\"}}},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:card\"}}},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:description\"}}},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:image\"}}},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:site\"}}},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:title\"}}},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.url\"}}},{\"name\":\"request\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"request\"}}},{\"name\":\"response\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"response\"}}},{\"name\":\"spaces\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"spaces\"}}},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"url\"}}},{\"name\":\"utc_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"xss\"}}}]", "timeFieldName": "@timestamp", "title": "logstash-*", - "fieldAttrs": "{\"referer\":{\"customName\":\"Referer custom\"}}" + "fieldAttrs": "{\"referer\":{\"customLabel\":\"Referer custom\"}}" }, "type": "index-pattern" } diff --git a/test/functional/fixtures/es_archiver/visualize/data.json b/test/functional/fixtures/es_archiver/visualize/data.json index c57cdb40ae952..56397351562de 100644 --- a/test/functional/fixtures/es_archiver/visualize/data.json +++ b/test/functional/fixtures/es_archiver/visualize/data.json @@ -9,7 +9,7 @@ "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", "timeFieldName": "@timestamp", "title": "logstash-*", - "fieldAttrs": "{\"utc_time\":{\"customName\":\"UTC time\"}}" + "fieldAttrs": "{\"utc_time\":{\"customLabel\":\"UTC time\"}}" }, "type": "index-pattern" } diff --git a/tsconfig.json b/tsconfig.json index 88ae3e1e826b3..6e137e445762d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,6 +31,7 @@ { "path": "./src/core/tsconfig.json" }, { "path": "./src/plugins/dev_tools/tsconfig.json" }, { "path": "./src/plugins/inspector/tsconfig.json" }, + { "path": "./src/plugins/kibana_legacy/tsconfig.json" }, { "path": "./src/plugins/kibana_react/tsconfig.json" }, { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, @@ -39,6 +40,7 @@ { "path": "./src/plugins/share/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, + { "path": "./src/plugins/url_forwarding/tsconfig.json" }, { "path": "./src/plugins/usage_collection/tsconfig.json" }, { "path": "./src/test_utils/tsconfig.json" } ] diff --git a/x-pack/examples/alerting_example/common/constants.ts b/x-pack/examples/alerting_example/common/constants.ts index dd9cc21954e61..40cc298db795a 100644 --- a/x-pack/examples/alerting_example/common/constants.ts +++ b/x-pack/examples/alerting_example/common/constants.ts @@ -8,6 +8,15 @@ export const ALERTING_EXAMPLE_APP_ID = 'AlertingExample'; // always firing export const DEFAULT_INSTANCES_TO_GENERATE = 5; +export interface AlwaysFiringParams { + instances?: number; + thresholds?: { + small?: number; + medium?: number; + large?: number; + }; +} +export type AlwaysFiringActionGroupIds = keyof AlwaysFiringParams['thresholds']; // Astros export enum Craft { diff --git a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx index a5d158fca836b..abbe1d2a48d11 100644 --- a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx +++ b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx @@ -4,17 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFieldNumber, EuiFormRow } from '@elastic/eui'; +import React, { Fragment, useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFieldNumber, + EuiFormRow, + EuiPopover, + EuiExpression, + EuiSpacer, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AlertTypeModel } from '../../../../plugins/triggers_actions_ui/public'; -import { DEFAULT_INSTANCES_TO_GENERATE } from '../../common/constants'; - -interface AlwaysFiringParamsProps { - alertParams: { instances?: number }; - setAlertParams: (property: string, value: any) => void; - errors: { [key: string]: string[] }; -} +import { omit, pick } from 'lodash'; +import { + ActionGroupWithCondition, + AlertConditions, + AlertConditionsGroup, + AlertTypeModel, + AlertTypeParamsExpressionProps, + AlertsContextValue, +} from '../../../../plugins/triggers_actions_ui/public'; +import { + AlwaysFiringParams, + AlwaysFiringActionGroupIds, + DEFAULT_INSTANCES_TO_GENERATE, +} from '../../common/constants'; export function getAlertType(): AlertTypeModel { return { @@ -24,7 +38,7 @@ export function getAlertType(): AlertTypeModel { iconClass: 'bolt', documentationUrl: null, alertParamsExpression: AlwaysFiringExpression, - validate: (alertParams: AlwaysFiringParamsProps['alertParams']) => { + validate: (alertParams: AlwaysFiringParams) => { const { instances } = alertParams; const validationResult = { errors: { @@ -44,11 +58,30 @@ export function getAlertType(): AlertTypeModel { }; } -export const AlwaysFiringExpression: React.FunctionComponent = ({ - alertParams, - setAlertParams, -}) => { - const { instances = DEFAULT_INSTANCES_TO_GENERATE } = alertParams; +const DEFAULT_THRESHOLDS: AlwaysFiringParams['thresholds'] = { + small: 0, + medium: 5000, + large: 10000, +}; + +export const AlwaysFiringExpression: React.FunctionComponent> = ({ alertParams, setAlertParams, actionGroups, defaultActionGroupId }) => { + const { + instances = DEFAULT_INSTANCES_TO_GENERATE, + thresholds = pick(DEFAULT_THRESHOLDS, defaultActionGroupId), + } = alertParams; + + const actionGroupsWithConditions = actionGroups.map((actionGroup) => + Number.isInteger(thresholds[actionGroup.id as AlwaysFiringActionGroupIds]) + ? { + ...actionGroup, + conditions: thresholds[actionGroup.id as AlwaysFiringActionGroupIds]!, + } + : actionGroup + ); + return ( @@ -67,6 +100,88 @@ export const AlwaysFiringExpression: React.FunctionComponent + + + + { + setAlertParams('thresholds', { + ...thresholds, + ...pick(DEFAULT_THRESHOLDS, actionGroup.id), + }); + }} + > + { + setAlertParams('thresholds', omit(thresholds, actionGroup.id)); + }} + > + { + setAlertParams('thresholds', { + ...thresholds, + [actionGroup.id]: actionGroup.conditions, + }); + }} + /> + + + + + ); }; + +interface TShirtSelectorProps { + actionGroup?: ActionGroupWithCondition; + setTShirtThreshold: (actionGroup: ActionGroupWithCondition) => void; +} +const TShirtSelector = ({ actionGroup, setTShirtThreshold }: TShirtSelectorProps) => { + const [isOpen, setIsOpen] = useState(false); + + if (!actionGroup) { + return null; + } + + return ( + setIsOpen(true)} + /> + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + ownFocus + anchorPosition="downLeft" + > + + + {'Is Above'} + + + { + const conditions = parseInt(e.target.value, 10); + if (e.target.value && !isNaN(conditions)) { + setTShirtThreshold({ + ...actionGroup, + conditions, + }); + } + }} + /> + + + + ); +}; diff --git a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts index d02406a23045e..1900f55a51a55 100644 --- a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts +++ b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts @@ -5,31 +5,56 @@ */ import uuid from 'uuid'; -import { range, random } from 'lodash'; +import { range } from 'lodash'; import { AlertType } from '../../../../plugins/alerts/server'; -import { DEFAULT_INSTANCES_TO_GENERATE, ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; +import { + DEFAULT_INSTANCES_TO_GENERATE, + ALERTING_EXAMPLE_APP_ID, + AlwaysFiringParams, +} from '../../common/constants'; const ACTION_GROUPS = [ - { id: 'small', name: 'small' }, - { id: 'medium', name: 'medium' }, - { id: 'large', name: 'large' }, + { id: 'small', name: 'Small t-shirt' }, + { id: 'medium', name: 'Medium t-shirt' }, + { id: 'large', name: 'Large t-shirt' }, ]; +const DEFAULT_ACTION_GROUP = 'small'; -export const alertType: AlertType = { +function getTShirtSizeByIdAndThreshold(id: string, thresholds: AlwaysFiringParams['thresholds']) { + const idAsNumber = parseInt(id, 10); + if (!isNaN(idAsNumber)) { + if (thresholds?.large && thresholds.large < idAsNumber) { + return 'large'; + } + if (thresholds?.medium && thresholds.medium < idAsNumber) { + return 'medium'; + } + if (thresholds?.small && thresholds.small < idAsNumber) { + return 'small'; + } + } + return DEFAULT_ACTION_GROUP; +} + +export const alertType: AlertType = { id: 'example.always-firing', name: 'Always firing', actionGroups: ACTION_GROUPS, - defaultActionGroupId: 'small', - async executor({ services, params: { instances = DEFAULT_INSTANCES_TO_GENERATE }, state }) { + defaultActionGroupId: DEFAULT_ACTION_GROUP, + async executor({ + services, + params: { instances = DEFAULT_INSTANCES_TO_GENERATE, thresholds }, + state, + }) { const count = (state.count ?? 0) + 1; range(instances) - .map(() => ({ id: uuid.v4(), tshirtSize: ACTION_GROUPS[random(0, 2)].id! })) - .forEach((instance: { id: string; tshirtSize: string }) => { + .map(() => uuid.v4()) + .forEach((id: string) => { services - .alertInstanceFactory(instance.id) + .alertInstanceFactory(id) .replaceState({ triggerdOnCycle: count }) - .scheduleActions(instance.tshirtSize); + .scheduleActions(getTShirtSizeByIdAndThreshold(id, thresholds)); }); return { diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index ed06bd888f919..8adbedf069d30 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -172,7 +172,7 @@ describe('execute()', () => { apiKey: null, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unable to execute action due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"` + `"Unable to execute action because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."` ); }); diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index f0a22c642cf61..dc400cb90967a 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -41,7 +41,7 @@ export function createExecutionEnqueuerFunction({ ) { if (isESOUsingEphemeralEncryptionKey === true) { throw new Error( - `Unable to execute action due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml` + `Unable to execute action because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` ); } diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 4ff56536e3867..57b88d3e6c1d8 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -31,7 +31,7 @@ const executeParams = { request: {} as KibanaRequest, }; -const spacesMock = spacesServiceMock.createSetupContract(); +const spacesMock = spacesServiceMock.createStartContract(); const loggerMock = loggingSystemMock.create().get(); const getActionsClientWithRequest = jest.fn(); actionExecutor.initialize({ @@ -322,7 +322,7 @@ test('throws an error when passing isESOUsingEphemeralEncryptionKey with value o await expect( customActionExecutor.execute(executeParams) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unable to execute action due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"` + `"Unable to execute action because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."` ); }); diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index af70fbf2ec896..d050bab9b0d9f 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -15,7 +15,7 @@ import { ProxySettings, } from '../types'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; -import { SpacesServiceSetup } from '../../../spaces/server'; +import { SpacesServiceStart } from '../../../spaces/server'; import { EVENT_LOG_ACTIONS } from '../plugin'; import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; import { ActionsClient } from '../actions_client'; @@ -23,7 +23,7 @@ import { ActionExecutionSource } from './action_execution_source'; export interface ActionExecutorContext { logger: Logger; - spaces?: SpacesServiceSetup; + spaces?: SpacesServiceStart; getServices: GetServicesFunction; getActionsClientWithRequest: ( request: KibanaRequest, @@ -74,7 +74,7 @@ export class ActionExecutor { if (this.isESOUsingEphemeralEncryptionKey === true) { throw new Error( - `Unable to execute action due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml` + `Unable to execute action because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` ); } diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts index 18cbd9f9c5fad..136ca5cb98465 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts @@ -12,7 +12,7 @@ import { TaskRunnerFactory } from './task_runner_factory'; import { actionTypeRegistryMock } from '../action_type_registry.mock'; import { actionExecutorMock } from './action_executor.mock'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; -import { savedObjectsClientMock, loggingSystemMock } from 'src/core/server/mocks'; +import { savedObjectsClientMock, loggingSystemMock, httpServiceMock } from 'src/core/server/mocks'; import { eventLoggerMock } from '../../../event_log/server/mocks'; import { ActionTypeDisabledError } from './errors'; import { actionsClientMock } from '../mocks'; @@ -70,7 +70,7 @@ const taskRunnerFactoryInitializerParams = { actionTypeRegistry, logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: mockedEncryptedSavedObjectsClient, - getBasePath: jest.fn().mockReturnValue(undefined), + basePathService: httpServiceMock.createBasePath(), getUnsecuredSavedObjectsClient: jest.fn().mockReturnValue(services.savedObjectsClient), }; @@ -126,27 +126,23 @@ test('executes the task by calling the executor with proper parameters', async ( expect( mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser ).toHaveBeenCalledWith('action_task_params', '3', { namespace: 'namespace-test' }); + expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ actionId: '2', params: { baz: true }, - request: { - getBasePath: expect.any(Function), + request: expect.objectContaining({ headers: { // base64 encoded "123:abc" authorization: 'ApiKey MTIzOmFiYw==', }, - path: '/', - route: { settings: {} }, - url: { - href: '/', - }, - raw: { - req: { - url: '/', - }, - }, - }, + }), }); + + const [executeParams] = mockedActionExecutor.execute.mock.calls[0]; + expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith( + executeParams.request, + '/s/test' + ); }); test('cleans up action_task_params object', async () => { @@ -255,24 +251,19 @@ test('uses API key when provided', async () => { expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ actionId: '2', params: { baz: true }, - request: { - getBasePath: expect.anything(), + request: expect.objectContaining({ headers: { // base64 encoded "123:abc" authorization: 'ApiKey MTIzOmFiYw==', }, - path: '/', - route: { settings: {} }, - url: { - href: '/', - }, - raw: { - req: { - url: '/', - }, - }, - }, + }), }); + + const [executeParams] = mockedActionExecutor.execute.mock.calls[0]; + expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith( + executeParams.request, + '/s/test' + ); }); test(`doesn't use API key when not provided`, async () => { @@ -297,21 +288,16 @@ test(`doesn't use API key when not provided`, async () => { expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ actionId: '2', params: { baz: true }, - request: { - getBasePath: expect.anything(), + request: expect.objectContaining({ headers: {}, - path: '/', - route: { settings: {} }, - url: { - href: '/', - }, - raw: { - req: { - url: '/', - }, - }, - }, + }), }); + + const [executeParams] = mockedActionExecutor.execute.mock.calls[0]; + expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith( + executeParams.request, + '/s/test' + ); }); test(`throws an error when license doesn't support the action type`, async () => { diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index aeeeb4ed7d520..99c8b8b1ff0e1 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -5,14 +5,17 @@ */ import { pick } from 'lodash'; +import type { Request } from '@hapi/hapi'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, fromNullable, getOrElse } from 'fp-ts/lib/Option'; +import { addSpaceIdToPath } from '../../../spaces/server'; import { Logger, SavedObjectsClientContract, KibanaRequest, SavedObjectReference, -} from 'src/core/server'; + IBasePath, +} from '../../../../../src/core/server'; import { ActionExecutorContract } from './action_executor'; import { ExecutorError } from './executor_error'; import { RunContext } from '../../../task_manager/server'; @@ -21,7 +24,6 @@ import { ActionTypeDisabledError } from './errors'; import { ActionTaskParams, ActionTypeRegistryContract, - GetBasePathFunction, SpaceIdToNamespaceFunction, ActionTypeExecutorResult, } from '../types'; @@ -33,7 +35,7 @@ export interface TaskRunnerContext { actionTypeRegistry: ActionTypeRegistryContract; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; spaceIdToNamespace: SpaceIdToNamespaceFunction; - getBasePath: GetBasePathFunction; + basePathService: IBasePath; getUnsecuredSavedObjectsClient: (request: KibanaRequest) => SavedObjectsClientContract; } @@ -64,7 +66,7 @@ export class TaskRunnerFactory { logger, encryptedSavedObjectsClient, spaceIdToNamespace, - getBasePath, + basePathService, getUnsecuredSavedObjectsClient, } = this.taskRunnerContext!; @@ -87,11 +89,12 @@ export class TaskRunnerFactory { requestHeaders.authorization = `ApiKey ${apiKey}`; } + const path = addSpaceIdToPath('/', spaceId); + // Since we're using API keys and accessing elasticsearch can only be done // via a request, we're faking one with the proper authorization headers. - const fakeRequest = ({ + const fakeRequest = KibanaRequest.from(({ headers: requestHeaders, - getBasePath: () => getBasePath(spaceId), path: '/', route: { settings: {} }, url: { @@ -102,7 +105,9 @@ export class TaskRunnerFactory { url: '/', }, }, - } as unknown) as KibanaRequest; + } as unknown) as Request); + + basePathService.set(fakeRequest, path); let executorResult: ActionTypeExecutorResult; try { diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 7f7f9e196da07..ff43b05b6d895 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -56,7 +56,7 @@ describe('Actions Plugin', () => { await plugin.setup(coreSetup as any, pluginsSetup); expect(pluginsSetup.encryptedSavedObjects.usingEphemeralEncryptionKey).toEqual(true); expect(context.logger.get().warn).toHaveBeenCalledWith( - 'APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.' + 'APIs are disabled because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' ); }); @@ -116,7 +116,7 @@ describe('Actions Plugin', () => { httpServerMock.createResponseFactory() )) as unknown) as RequestHandlerContext['actions']; expect(() => actionsContextHandler!.getActionsClient()).toThrowErrorMatchingInlineSnapshot( - `"Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"` + `"Unable to create actions client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."` ); }); }); @@ -252,7 +252,7 @@ describe('Actions Plugin', () => { await expect( pluginStart.getActionsClientWithRequest(httpServerMock.createKibanaRequest()) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"` + `"Unable to create actions client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."` ); }); }); diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 9db07f653872f..a160735e89a93 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -27,7 +27,7 @@ import { } from '../../encrypted_saved_objects/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; -import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server'; +import { SpacesPluginStart } from '../../spaces/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { SecurityPluginSetup } from '../../security/server'; @@ -109,7 +109,6 @@ export interface ActionsPluginsSetup { taskManager: TaskManagerSetupContract; encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; licensing: LicensingPluginSetup; - spaces?: SpacesPluginSetup; eventLog: IEventLogService; usageCollection?: UsageCollectionSetup; security?: SecurityPluginSetup; @@ -119,6 +118,7 @@ export interface ActionsPluginsStart { encryptedSavedObjects: EncryptedSavedObjectsPluginStart; taskManager: TaskManagerStartContract; licensing: LicensingPluginStart; + spaces?: SpacesPluginStart; } const includedHiddenTypes = [ @@ -133,12 +133,10 @@ export class ActionsPlugin implements Plugin, Plugi private readonly logger: Logger; private actionsConfig?: ActionsConfig; - private serverBasePath?: string; private taskRunnerFactory?: TaskRunnerFactory; private actionTypeRegistry?: ActionTypeRegistry; private actionExecutor?: ActionExecutor; private licenseState: ILicenseState | null = null; - private spaces?: SpacesServiceSetup; private security?: SecurityPluginSetup; private eventLogService?: IEventLogService; private eventLogger?: IEventLogger; @@ -171,7 +169,7 @@ export class ActionsPlugin implements Plugin, Plugi if (this.isESOUsingEphemeralEncryptionKey) { this.logger.warn( - 'APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.' + 'APIs are disabled because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' ); } @@ -211,9 +209,7 @@ export class ActionsPlugin implements Plugin, Plugi }); this.taskRunnerFactory = taskRunnerFactory; this.actionTypeRegistry = actionTypeRegistry; - this.serverBasePath = core.http.basePath.serverBasePath; this.actionExecutor = actionExecutor; - this.spaces = plugins.spaces?.spacesService; this.security = plugins.security; registerBuiltInActionTypes({ @@ -292,7 +288,7 @@ export class ActionsPlugin implements Plugin, Plugi ) => { if (isESOUsingEphemeralEncryptionKey === true) { throw new Error( - `Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml` + `Unable to create actions client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` ); } @@ -339,7 +335,7 @@ export class ActionsPlugin implements Plugin, Plugi actionExecutor!.initialize({ logger, eventLogger: this.eventLogger!, - spaces: this.spaces, + spaces: plugins.spaces?.spacesService, getActionsClientWithRequest, getServices: this.getServicesFactory( getScopedSavedObjectsClientWithoutAccessToActions, @@ -359,12 +355,18 @@ export class ActionsPlugin implements Plugin, Plugi : undefined, }); + const spaceIdToNamespace = (spaceId?: string) => { + return plugins.spaces && spaceId + ? plugins.spaces.spacesService.spaceIdToNamespace(spaceId) + : undefined; + }; + taskRunnerFactory!.initialize({ logger, actionTypeRegistry: actionTypeRegistry!, encryptedSavedObjectsClient, - getBasePath: this.getBasePath, - spaceIdToNamespace: this.spaceIdToNamespace, + basePathService: core.http.basePath, + spaceIdToNamespace, getUnsecuredSavedObjectsClient: (request: KibanaRequest) => this.getUnsecuredSavedObjectsClient(core.savedObjects, request), }); @@ -446,7 +448,7 @@ export class ActionsPlugin implements Plugin, Plugi getActionsClient: () => { if (isESOUsingEphemeralEncryptionKey === true) { throw new Error( - `Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml` + `Unable to create actions client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` ); } return new ActionsClient({ @@ -474,14 +476,6 @@ export class ActionsPlugin implements Plugin, Plugi }; }; - private spaceIdToNamespace = (spaceId?: string): string | undefined => { - return this.spaces && spaceId ? this.spaces.spaceIdToNamespace(spaceId) : undefined; - }; - - private getBasePath = (spaceId?: string): string => { - return this.spaces && spaceId ? this.spaces.getBasePath(spaceId) : this.serverBasePath!; - }; - public stop() { if (this.licenseState) { this.licenseState.clean(); diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 1867815bd5f90..79895195d90f3 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -22,7 +22,6 @@ export { ActionTypeExecutorResult } from '../common'; export type WithoutQueryAndParams = Pick>; export type GetServicesFunction = (request: KibanaRequest) => Services; export type ActionTypeRegistryContract = PublicMethodsOf; -export type GetBasePathFunction = (spaceId?: string) => string; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; export type ActionTypeConfig = Record; export type ActionTypeSecrets = Record; diff --git a/x-pack/plugins/alerts/common/alert.ts b/x-pack/plugins/alerts/common/alert.ts index 97a9a58400e38..88f6090d20737 100644 --- a/x-pack/plugins/alerts/common/alert.ts +++ b/x-pack/plugins/alerts/common/alert.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectAttributes } from 'kibana/server'; +import { SavedObjectAttribute, SavedObjectAttributes } from 'kibana/server'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type AlertTypeState = Record; @@ -37,6 +37,7 @@ export interface AlertExecutionStatus { } export type AlertActionParams = SavedObjectAttributes; +export type AlertActionParam = SavedObjectAttribute; export interface AlertAction { group: string; diff --git a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts index e97b37f16faf0..c08ff9449d151 100644 --- a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts @@ -228,14 +228,17 @@ export class AlertsClient { this.validateActions(alertType, data.actions); + const createTime = Date.now(); const { references, actions } = await this.denormalizeActions(data.actions); + const rawAlert: RawAlert = { ...data, ...this.apiKeyAsAlertAttributes(createdAPIKey, username), actions, createdBy: username, updatedBy: username, - createdAt: new Date().toISOString(), + createdAt: new Date(createTime).toISOString(), + updatedAt: new Date(createTime).toISOString(), params: validatedAlertTypeParams as RawAlert['params'], muteAll: false, mutedInstanceIds: [], @@ -289,12 +292,7 @@ export class AlertsClient { }); createdAlert.attributes.scheduledTaskId = scheduledTask.id; } - return this.getAlertFromRaw( - createdAlert.id, - createdAlert.attributes, - createdAlert.updated_at, - references - ); + return this.getAlertFromRaw(createdAlert.id, createdAlert.attributes, references); } public async get({ id }: { id: string }): Promise { @@ -304,7 +302,7 @@ export class AlertsClient { result.attributes.consumer, ReadOperations.Get ); - return this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references); + return this.getAlertFromRaw(result.id, result.attributes, result.references); } public async getAlertState({ id }: { id: string }): Promise { @@ -393,13 +391,11 @@ export class AlertsClient { type: 'alert', }); - // eslint-disable-next-line @typescript-eslint/naming-convention - const authorizedData = data.map(({ id, attributes, updated_at, references }) => { + const authorizedData = data.map(({ id, attributes, references }) => { ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer); return this.getAlertFromRaw( id, fields ? (pick(attributes, fields) as RawAlert) : attributes, - updated_at, references ); }); @@ -585,6 +581,7 @@ export class AlertsClient { params: validatedAlertTypeParams as RawAlert['params'], actions, updatedBy: username, + updatedAt: new Date().toISOString(), }); try { updatedObject = await this.unsecuredSavedObjectsClient.create( @@ -607,12 +604,7 @@ export class AlertsClient { throw e; } - return this.getPartialAlertFromRaw( - id, - updatedObject.attributes, - updatedObject.updated_at, - updatedObject.references - ); + return this.getPartialAlertFromRaw(id, updatedObject.attributes, updatedObject.references); } private apiKeyAsAlertAttributes( @@ -677,6 +669,7 @@ export class AlertsClient { await this.createAPIKey(this.generateAPIKeyName(attributes.alertTypeId, attributes.name)), username ), + updatedAt: new Date().toISOString(), updatedBy: username, }); try { @@ -751,6 +744,7 @@ export class AlertsClient { username ), updatedBy: username, + updatedAt: new Date().toISOString(), }); try { await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); @@ -829,6 +823,7 @@ export class AlertsClient { apiKey: null, apiKeyOwner: null, updatedBy: await this.getUserName(), + updatedAt: new Date().toISOString(), }), { version } ); @@ -875,6 +870,7 @@ export class AlertsClient { muteAll: true, mutedInstanceIds: [], updatedBy: await this.getUserName(), + updatedAt: new Date().toISOString(), }); const updateOptions = { version }; @@ -913,6 +909,7 @@ export class AlertsClient { muteAll: false, mutedInstanceIds: [], updatedBy: await this.getUserName(), + updatedAt: new Date().toISOString(), }); const updateOptions = { version }; @@ -957,6 +954,7 @@ export class AlertsClient { this.updateMeta({ mutedInstanceIds, updatedBy: await this.getUserName(), + updatedAt: new Date().toISOString(), }), { version } ); @@ -999,6 +997,7 @@ export class AlertsClient { alertId, this.updateMeta({ updatedBy: await this.getUserName(), + updatedAt: new Date().toISOString(), mutedInstanceIds: mutedInstanceIds.filter((id: string) => id !== alertInstanceId), }), { version } @@ -1050,19 +1049,17 @@ export class AlertsClient { private getAlertFromRaw( id: string, rawAlert: RawAlert, - updatedAt: SavedObject['updated_at'], references: SavedObjectReference[] | undefined ): Alert { // In order to support the partial update API of Saved Objects we have to support // partial updates of an Alert, but when we receive an actual RawAlert, it is safe // to cast the result to an Alert - return this.getPartialAlertFromRaw(id, rawAlert, updatedAt, references) as Alert; + return this.getPartialAlertFromRaw(id, rawAlert, references) as Alert; } private getPartialAlertFromRaw( id: string, - { createdAt, meta, scheduledTaskId, ...rawAlert }: Partial, - updatedAt: SavedObject['updated_at'] = createdAt, + { createdAt, updatedAt, meta, scheduledTaskId, ...rawAlert }: Partial, references: SavedObjectReference[] | undefined ): PartialAlert { // Not the prettiest code here, but if we want to use most of the diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts index ee407b1a6d50c..6d259029ac480 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts @@ -196,6 +196,7 @@ describe('create()', () => { createdAt: '2019-02-12T21:01:22.479Z', createdBy: 'elastic', updatedBy: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', muteAll: false, mutedInstanceIds: [], actions: [ @@ -330,6 +331,7 @@ describe('create()', () => { "foo", ], "throttle": null, + "updatedAt": "2019-02-12T21:01:22.479Z", "updatedBy": "elastic", } `); @@ -418,6 +420,7 @@ describe('create()', () => { bar: true, }, createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), actions: [ { group: 'default', @@ -555,6 +558,7 @@ describe('create()', () => { bar: true, }, createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), actions: [ { group: 'default', @@ -631,6 +635,7 @@ describe('create()', () => { bar: true, }, createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), actions: [ { group: 'default', @@ -971,6 +976,7 @@ describe('create()', () => { createdBy: 'elastic', createdAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', enabled: true, meta: { versionApiKeyLastmodified: 'v7.10.0', @@ -1092,6 +1098,7 @@ describe('create()', () => { createdBy: 'elastic', createdAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', enabled: false, meta: { versionApiKeyLastmodified: 'v7.10.0', diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts index 11ce0027f82d8..8c9ab9494a50a 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts @@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { getBeforeSetup } from './lib'; +import { getBeforeSetup, setGlobalDate } from './lib'; import { InvalidatePendingApiKey } from '../../types'; const taskManager = taskManagerMock.createStart(); @@ -45,6 +45,8 @@ beforeEach(() => { getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); }); +setGlobalDate(); + describe('disable()', () => { let alertsClient: AlertsClient; const existingAlert = { @@ -136,6 +138,7 @@ describe('disable()', () => { scheduledTaskId: null, apiKey: null, apiKeyOwner: null, + updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', actions: [ { @@ -190,6 +193,7 @@ describe('disable()', () => { scheduledTaskId: null, apiKey: null, apiKeyOwner: null, + updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', actions: [ { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts index 16e83c42d8930..feec1d1b9334a 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts @@ -13,7 +13,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { TaskStatus } from '../../../../task_manager/server'; -import { getBeforeSetup } from './lib'; +import { getBeforeSetup, setGlobalDate } from './lib'; import { InvalidatePendingApiKey } from '../../types'; const taskManager = taskManagerMock.createStart(); @@ -46,6 +46,8 @@ beforeEach(() => { getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); }); +setGlobalDate(); + describe('enable()', () => { let alertsClient: AlertsClient; const existingAlert = { @@ -186,6 +188,7 @@ describe('enable()', () => { meta: { versionApiKeyLastmodified: kibanaVersion, }, + updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', apiKey: null, apiKeyOwner: null, @@ -292,6 +295,7 @@ describe('enable()', () => { apiKey: Buffer.from('123:abc').toString('base64'), apiKeyOwner: 'elastic', updatedBy: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', actions: [ { group: 'default', diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts index 1b3a776bd23e0..3d7473a746986 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts @@ -79,6 +79,7 @@ describe('find()', () => { bar: true, }, createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), actions: [ { group: 'default', diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts index 5c0d80f159b31..3f0c783f424d1 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts @@ -59,6 +59,7 @@ describe('get()', () => { bar: true, }, createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), actions: [ { group: 'default', diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts index 269b2eb2ab7a7..9bd61c0fe66d2 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts @@ -76,6 +76,7 @@ const BaseAlertInstanceSummarySavedObject: SavedObject = { createdBy: null, updatedBy: null, createdAt: mockedDateString, + updatedAt: mockedDateString, apiKey: null, apiKeyOwner: null, throttle: null, diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts index 868fa3d8c6aa2..14ebca2135587 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts @@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { getBeforeSetup } from './lib'; +import { getBeforeSetup, setGlobalDate } from './lib'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -43,6 +43,8 @@ beforeEach(() => { getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); }); +setGlobalDate(); + describe('muteAll()', () => { test('mutes an alert', async () => { const alertsClient = new AlertsClient(alertsClientParams); @@ -74,6 +76,7 @@ describe('muteAll()', () => { { muteAll: true, mutedInstanceIds: [], + updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', }, { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts index 05ca741f480ca..c2188f128cb4d 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts @@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { getBeforeSetup } from './lib'; +import { getBeforeSetup, setGlobalDate } from './lib'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -44,6 +44,8 @@ beforeEach(() => { getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); }); +setGlobalDate(); + describe('muteInstance()', () => { test('mutes an alert instance', async () => { const alertsClient = new AlertsClient(alertsClientParams); @@ -68,6 +70,7 @@ describe('muteInstance()', () => { '1', { mutedInstanceIds: ['2'], + updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', }, { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts index 5ef1af9b6f0ee..d92304ab873be 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts @@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { getBeforeSetup } from './lib'; +import { getBeforeSetup, setGlobalDate } from './lib'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -44,6 +44,8 @@ beforeEach(() => { getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); }); +setGlobalDate(); + describe('unmuteAll()', () => { test('unmutes an alert', async () => { const alertsClient = new AlertsClient(alertsClientParams); @@ -75,6 +77,7 @@ describe('unmuteAll()', () => { { muteAll: false, mutedInstanceIds: [], + updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', }, { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts index 88692239ac2fe..3486df98f2f05 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts @@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { getBeforeSetup } from './lib'; +import { getBeforeSetup, setGlobalDate } from './lib'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -44,6 +44,8 @@ beforeEach(() => { getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); }); +setGlobalDate(); + describe('unmuteInstance()', () => { test('unmutes an alert instance', async () => { const alertsClient = new AlertsClient(alertsClientParams); @@ -69,6 +71,7 @@ describe('unmuteInstance()', () => { { mutedInstanceIds: [], updatedBy: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', }, { version: '123' } ); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts index ad58e36ade722..d0bb2607f7a47 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts @@ -140,8 +140,8 @@ describe('update()', () => { ], scheduledTaskId: 'task-123', createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), }, - updated_at: new Date().toISOString(), references: [ { name: 'action_0', @@ -300,6 +300,7 @@ describe('update()', () => { "foo", ], "throttle": null, + "updatedAt": "2019-02-12T21:01:22.479Z", "updatedBy": "elastic", } `); @@ -362,6 +363,7 @@ describe('update()', () => { bar: true, }, createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), actions: [ { group: 'default', @@ -484,6 +486,7 @@ describe('update()', () => { "foo", ], "throttle": "5m", + "updatedAt": "2019-02-12T21:01:22.479Z", "updatedBy": "elastic", } `); @@ -534,6 +537,7 @@ describe('update()', () => { bar: true, }, createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), actions: [ { group: 'default', @@ -648,6 +652,7 @@ describe('update()', () => { "foo", ], "throttle": "5m", + "updatedAt": "2019-02-12T21:01:22.479Z", "updatedBy": "elastic", } `); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts index af178a1fac5f5..ca5f44078f513 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts @@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { getBeforeSetup } from './lib'; +import { getBeforeSetup, setGlobalDate } from './lib'; import { InvalidatePendingApiKey } from '../../types'; const taskManager = taskManagerMock.createStart(); @@ -44,6 +44,8 @@ beforeEach(() => { getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); }); +setGlobalDate(); + describe('updateApiKey()', () => { let alertsClient: AlertsClient; const existingAlert = { @@ -113,6 +115,7 @@ describe('updateApiKey()', () => { apiKey: Buffer.from('234:abc').toString('base64'), apiKeyOwner: 'elastic', updatedBy: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', actions: [ { group: 'default', @@ -162,6 +165,7 @@ describe('updateApiKey()', () => { enabled: true, apiKey: Buffer.from('234:abc').toString('base64'), apiKeyOwner: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', actions: [ { diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index 62f4b7d5a3fc4..fee7901c4ea55 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -52,7 +52,7 @@ describe('Alerting Plugin', () => { expect(statusMock.set).toHaveBeenCalledTimes(1); expect(encryptedSavedObjectsSetup.usingEphemeralEncryptionKey).toEqual(true); expect(context.logger.get().warn).toHaveBeenCalledWith( - 'APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.' + 'APIs are disabled because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' ); }); }); @@ -113,7 +113,7 @@ describe('Alerting Plugin', () => { expect(() => startContract.getAlertsClientWithRequest({} as KibanaRequest) ).toThrowErrorMatchingInlineSnapshot( - `"Unable to create alerts client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"` + `"Unable to create alerts client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."` ); }); @@ -158,7 +158,6 @@ describe('Alerting Plugin', () => { getActionsClientWithRequest: jest.fn(), getActionsAuthorizationWithRequest: jest.fn(), }, - spaces: () => null, encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), features: mockFeatures(), } as unknown) as AlertingPluginsStart diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 0c91e93938346..99cb45130718a 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -13,7 +13,7 @@ import { EncryptedSavedObjectsPluginStart, } from '../../encrypted_saved_objects/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; -import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server'; +import { SpacesPluginStart } from '../../spaces/server'; import { AlertsClient } from './alerts_client'; import { AlertTypeRegistry } from './alert_type_registry'; import { TaskRunnerFactory } from './task_runner'; @@ -101,7 +101,6 @@ export interface AlertingPluginsSetup { actions: ActionsPluginSetupContract; encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; licensing: LicensingPluginSetup; - spaces?: SpacesPluginSetup; usageCollection?: UsageCollectionSetup; eventLog: IEventLogService; statusService: StatusServiceSetup; @@ -112,6 +111,7 @@ export interface AlertingPluginsStart { encryptedSavedObjects: EncryptedSavedObjectsPluginStart; features: FeaturesPluginStart; eventLog: IEventLogClientService; + spaces?: SpacesPluginStart; } export class AlertingPlugin { @@ -119,10 +119,8 @@ export class AlertingPlugin { private readonly logger: Logger; private alertTypeRegistry?: AlertTypeRegistry; private readonly taskRunnerFactory: TaskRunnerFactory; - private serverBasePath?: string; private licenseState: LicenseState | null = null; private isESOUsingEphemeralEncryptionKey?: boolean; - private spaces?: SpacesServiceSetup; private security?: SecurityPluginSetup; private readonly alertsClientFactory: AlertsClientFactory; private readonly telemetryLogger: Logger; @@ -151,7 +149,6 @@ export class AlertingPlugin { plugins: AlertingPluginsSetup ): Promise { this.licenseState = new LicenseState(plugins.licensing.license$); - this.spaces = plugins.spaces?.spacesService; this.security = plugins.security; core.capabilities.registerProvider(() => { @@ -169,7 +166,7 @@ export class AlertingPlugin { if (this.isESOUsingEphemeralEncryptionKey) { this.logger.warn( - 'APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.' + 'APIs are disabled because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' ); } @@ -188,8 +185,6 @@ export class AlertingPlugin { }); this.alertTypeRegistry = alertTypeRegistry; - this.serverBasePath = core.http.basePath.serverBasePath; - const usageCollection = plugins.usageCollection; if (usageCollection) { initializeAlertingTelemetry( @@ -261,7 +256,6 @@ export class AlertingPlugin { public start(core: CoreStart, plugins: AlertingPluginsStart): PluginStartContract { const { - spaces, isESOUsingEphemeralEncryptionKey, logger, taskRunnerFactory, @@ -274,18 +268,24 @@ export class AlertingPlugin { includedHiddenTypes: ['alert'], }); + const spaceIdToNamespace = (spaceId?: string) => { + return plugins.spaces && spaceId + ? plugins.spaces.spacesService.spaceIdToNamespace(spaceId) + : undefined; + }; + alertsClientFactory.initialize({ alertTypeRegistry: alertTypeRegistry!, logger, taskManager: plugins.taskManager, securityPluginSetup: security, encryptedSavedObjectsClient, - spaceIdToNamespace: this.spaceIdToNamespace, + spaceIdToNamespace, getSpaceId(request: KibanaRequest) { - return spaces?.getSpaceId(request); + return plugins.spaces?.spacesService.getSpaceId(request); }, async getSpace(request: KibanaRequest) { - return spaces?.getActiveSpace(request); + return plugins.spaces?.spacesService.getActiveSpace(request); }, actions: plugins.actions, features: plugins.features, @@ -296,7 +296,7 @@ export class AlertingPlugin { const getAlertsClientWithRequest = (request: KibanaRequest) => { if (isESOUsingEphemeralEncryptionKey === true) { throw new Error( - `Unable to create alerts client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml` + `Unable to create alerts client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` ); } return alertsClientFactory!.create(request, core.savedObjects); @@ -306,10 +306,10 @@ export class AlertingPlugin { logger, getServices: this.getServicesFactory(core.savedObjects, core.elasticsearch), getAlertsClientWithRequest, - spaceIdToNamespace: this.spaceIdToNamespace, + spaceIdToNamespace, actionsPlugin: plugins.actions, encryptedSavedObjectsClient, - getBasePath: this.getBasePath, + basePathService: core.http.basePath, eventLogger: this.eventLogger!, internalSavedObjectsRepository: core.savedObjects.createInternalRepository(['alert']), }); @@ -363,14 +363,6 @@ export class AlertingPlugin { }); } - private spaceIdToNamespace = (spaceId?: string): string | undefined => { - return this.spaces && spaceId ? this.spaces.spaceIdToNamespace(spaceId) : undefined; - }; - - private getBasePath = (spaceId?: string): string => { - return this.spaces && spaceId ? this.spaces.getBasePath(spaceId) : this.serverBasePath!; - }; - private getScopedClientWithAlertSavedObjectType( savedObjects: SavedObjectsServiceStart, request: KibanaRequest diff --git a/x-pack/plugins/alerts/server/saved_objects/index.ts b/x-pack/plugins/alerts/server/saved_objects/index.ts index da30273e93c6b..dfe122f56bc48 100644 --- a/x-pack/plugins/alerts/server/saved_objects/index.ts +++ b/x-pack/plugins/alerts/server/saved_objects/index.ts @@ -16,6 +16,7 @@ export const AlertAttributesExcludedFromAAD = [ 'muteAll', 'mutedInstanceIds', 'updatedBy', + 'updatedAt', 'executionStatus', ]; @@ -28,6 +29,7 @@ export type AlertAttributesExcludedFromAADType = | 'muteAll' | 'mutedInstanceIds' | 'updatedBy' + | 'updatedAt' | 'executionStatus'; export function setupSavedObjects( diff --git a/x-pack/plugins/alerts/server/saved_objects/mappings.json b/x-pack/plugins/alerts/server/saved_objects/mappings.json index a6c92080f18be..f40a7d9075eed 100644 --- a/x-pack/plugins/alerts/server/saved_objects/mappings.json +++ b/x-pack/plugins/alerts/server/saved_objects/mappings.json @@ -62,6 +62,9 @@ "createdAt": { "type": "date" }, + "updatedAt": { + "type": "date" + }, "apiKey": { "type": "binary" }, diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts index 8c9d10769b18a..a4cbc18e13b47 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts @@ -261,8 +261,48 @@ describe('7.10.0 migrates with failure', () => { }); }); +describe('7.11.0', () => { + beforeEach(() => { + jest.resetAllMocks(); + encryptedSavedObjectsSetup.createMigration.mockImplementation( + (shouldMigrateWhenPredicate, migration) => migration + ); + }); + + test('add updatedAt field to alert - set to SavedObject updated_at attribute', () => { + const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0']; + const alert = getMockData({}, true); + expect(migration711(alert, { log })).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + updatedAt: alert.updated_at, + }, + }); + }); + + test('add updatedAt field to alert - set to createdAt when SavedObject updated_at is not defined', () => { + const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0']; + const alert = getMockData({}); + expect(migration711(alert, { log })).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + updatedAt: alert.attributes.createdAt, + }, + }); + }); +}); + +function getUpdatedAt(): string { + const updatedAt = new Date(); + updatedAt.setHours(updatedAt.getHours() + 2); + return updatedAt.toISOString(); +} + function getMockData( - overwrites: Record = {} + overwrites: Record = {}, + withSavedObjectUpdatedAt: boolean = false ): SavedObjectUnsanitizedDoc> { return { attributes: { @@ -295,6 +335,7 @@ function getMockData( ], ...overwrites, }, + updated_at: withSavedObjectUpdatedAt ? getUpdatedAt() : undefined, id: uuid.v4(), type: 'alert', }; diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts index 0b2c86b84f67b..d8ebced03c5a6 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -37,8 +37,15 @@ export function getMigrations( ) ); + const migrationAlertUpdatedAtDate = encryptedSavedObjects.createMigration( + // migrate all documents in 7.11 in order to add the "updatedAt" field + (doc): doc is SavedObjectUnsanitizedDoc => true, + pipeMigrations(setAlertUpdatedAtDate) + ); + return { '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'), + '7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtDate, '7.11.0'), }; } @@ -59,6 +66,19 @@ function executeMigrationWithErrorHandling( }; } +const setAlertUpdatedAtDate = ( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc => { + const updatedAt = doc.updated_at || doc.attributes.createdAt; + return { + ...doc, + attributes: { + ...doc.attributes, + updatedAt, + }, + }; +}; + const consumersToChange: Map = new Map( Object.entries({ alerting: 'alerts', diff --git a/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts b/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts index 50815c797e399..8041ec551bb0d 100644 --- a/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts +++ b/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts @@ -95,6 +95,7 @@ const DefaultAttributes = { muteAll: true, mutedInstanceIds: ['muted-instance-id-1', 'muted-instance-id-2'], updatedBy: 'someone', + updatedAt: '2019-02-12T21:01:22.479Z', }; const InvalidAttributes = { ...DefaultAttributes, foo: 'bar' }; diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index bd583159af5d5..07d08f5837d54 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -18,6 +18,7 @@ import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/serv import { loggingSystemMock, savedObjectsRepositoryMock, + httpServiceMock, } from '../../../../../src/core/server/mocks'; import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; import { actionsMock, actionsClientMock } from '../../../actions/server/mocks'; @@ -78,7 +79,7 @@ describe('Task Runner', () => { encryptedSavedObjectsClient, logger: loggingSystemMock.create().get(), spaceIdToNamespace: jest.fn().mockReturnValue(undefined), - getBasePath: jest.fn().mockReturnValue(undefined), + basePathService: httpServiceMock.createBasePath(), eventLogger: eventLoggerMock.create(), internalSavedObjectsRepository: savedObjectsRepositoryMock.create(), }; @@ -375,23 +376,24 @@ describe('Task Runner', () => { await taskRunner.run(); expect( taskRunnerFactoryInitializerParams.actionsPlugin.getActionsClientWithRequest - ).toHaveBeenCalledWith({ - getBasePath: expect.anything(), - headers: { - // base64 encoded "123:abc" - authorization: 'ApiKey MTIzOmFiYw==', - }, - path: '/', - route: { settings: {} }, - url: { - href: '/', - }, - raw: { - req: { - url: '/', + ).toHaveBeenCalledWith( + expect.objectContaining({ + headers: { + // base64 encoded "123:abc" + authorization: 'ApiKey MTIzOmFiYw==', }, - }, - }); + }) + ); + + const [ + request, + ] = taskRunnerFactoryInitializerParams.actionsPlugin.getActionsClientWithRequest.mock.calls[0]; + + expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith( + request, + '/' + ); + expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -768,23 +770,20 @@ describe('Task Runner', () => { }); await taskRunner.run(); - expect(taskRunnerFactoryInitializerParams.getServices).toHaveBeenCalledWith({ - getBasePath: expect.anything(), - headers: { - // base64 encoded "123:abc" - authorization: 'ApiKey MTIzOmFiYw==', - }, - path: '/', - route: { settings: {} }, - url: { - href: '/', - }, - raw: { - req: { - url: '/', + expect(taskRunnerFactoryInitializerParams.getServices).toHaveBeenCalledWith( + expect.objectContaining({ + headers: { + // base64 encoded "123:abc" + authorization: 'ApiKey MTIzOmFiYw==', }, - }, - }); + }) + ); + const [request] = taskRunnerFactoryInitializerParams.getServices.mock.calls[0]; + + expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith( + request, + '/' + ); }); test(`doesn't use API key when not provided`, async () => { @@ -803,20 +802,18 @@ describe('Task Runner', () => { await taskRunner.run(); - expect(taskRunnerFactoryInitializerParams.getServices).toHaveBeenCalledWith({ - getBasePath: expect.anything(), - headers: {}, - path: '/', - route: { settings: {} }, - url: { - href: '/', - }, - raw: { - req: { - url: '/', - }, - }, - }); + expect(taskRunnerFactoryInitializerParams.getServices).toHaveBeenCalledWith( + expect.objectContaining({ + headers: {}, + }) + ); + + const [request] = taskRunnerFactoryInitializerParams.getServices.mock.calls[0]; + + expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith( + request, + '/' + ); }); test('rescheduled the Alert if the schedule has update during a task run', async () => { diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 0dad952a86590..24d96788c3395 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -5,6 +5,8 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; import { Dictionary, pickBy, mapValues, without, cloneDeep } from 'lodash'; +import type { Request } from '@hapi/hapi'; +import { addSpaceIdToPath } from '../../../spaces/server'; import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance, throwUnrecoverableError } from '../../../task_manager/server'; @@ -91,9 +93,10 @@ export class TaskRunner { requestHeaders.authorization = `ApiKey ${apiKey}`; } - return ({ + const path = addSpaceIdToPath('/', spaceId); + + const fakeRequest = KibanaRequest.from(({ headers: requestHeaders, - getBasePath: () => this.context.getBasePath(spaceId), path: '/', route: { settings: {} }, url: { @@ -104,7 +107,11 @@ export class TaskRunner { url: '/', }, }, - } as unknown) as KibanaRequest; + } as unknown) as Request); + + this.context.basePathService.set(fakeRequest, path); + + return fakeRequest; } private getServicesWithSpaceLevelPermissions( diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts index 5da8e4296f4dd..1c10a997d8cdd 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts @@ -11,6 +11,7 @@ import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/serv import { loggingSystemMock, savedObjectsRepositoryMock, + httpServiceMock, } from '../../../../../src/core/server/mocks'; import { actionsMock } from '../../../actions/server/mocks'; import { alertsMock, alertsClientMock } from '../mocks'; @@ -64,7 +65,7 @@ describe('Task Runner Factory', () => { encryptedSavedObjectsClient: encryptedSavedObjectsPlugin.getClient(), logger: loggingSystemMock.create().get(), spaceIdToNamespace: jest.fn().mockReturnValue(undefined), - getBasePath: jest.fn().mockReturnValue(undefined), + basePathService: httpServiceMock.createBasePath(), eventLogger: eventLoggerMock.create(), internalSavedObjectsRepository: savedObjectsRepositoryMock.create(), }; diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts index df6f306c6ccc5..2a2d74c1fc259 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { Logger, KibanaRequest, ISavedObjectsRepository } from '../../../../../src/core/server'; +import { + Logger, + KibanaRequest, + ISavedObjectsRepository, + IBasePath, +} from '../../../../../src/core/server'; import { RunContext } from '../../../task_manager/server'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; import { PluginStartContract as ActionsPluginStartContract } from '../../../actions/server'; -import { - AlertType, - GetBasePathFunction, - GetServicesFunction, - SpaceIdToNamespaceFunction, -} from '../types'; +import { AlertType, GetServicesFunction, SpaceIdToNamespaceFunction } from '../types'; import { TaskRunner } from './task_runner'; import { IEventLogger } from '../../../event_log/server'; import { AlertsClient } from '../alerts_client'; @@ -26,7 +26,7 @@ export interface TaskRunnerContext { eventLogger: IEventLogger; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; spaceIdToNamespace: SpaceIdToNamespaceFunction; - getBasePath: GetBasePathFunction; + basePathService: IBasePath; internalSavedObjectsRepository: ISavedObjectsRepository; } diff --git a/x-pack/plugins/alerts/server/types.ts b/x-pack/plugins/alerts/server/types.ts index dde1628156658..500c681a1d2b9 100644 --- a/x-pack/plugins/alerts/server/types.ts +++ b/x-pack/plugins/alerts/server/types.ts @@ -32,7 +32,6 @@ import { export type WithoutQueryAndParams = Pick>; export type GetServicesFunction = (request: KibanaRequest) => Services; -export type GetBasePathFunction = (spaceId?: string) => string; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; declare module 'src/core/server' { @@ -148,6 +147,7 @@ export interface RawAlert extends SavedObjectAttributes { createdBy: string | null; updatedBy: string | null; createdAt: string; + updatedAt: string; apiKey: string | null; apiKeyOwner: string | null; throttle: string | null; diff --git a/x-pack/plugins/apm/common/utils/formatters/duration.ts b/x-pack/plugins/apm/common/utils/formatters/duration.ts index c0a99e0152fa7..8e563399a0f1f 100644 --- a/x-pack/plugins/apm/common/utils/formatters/duration.ts +++ b/x-pack/plugins/apm/common/utils/formatters/duration.ts @@ -8,9 +8,10 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { memoize } from 'lodash'; import { NOT_AVAILABLE_LABEL } from '../../../common/i18n'; -import { asDecimalOrInteger, asInteger } from './formatters'; +import { asDecimal, asDecimalOrInteger, asInteger } from './formatters'; import { TimeUnit } from './datetime'; import { Maybe } from '../../../typings/common'; +import { isFiniteNumber } from '../is_finite_number'; interface FormatterOptions { defaultValue?: string; @@ -99,7 +100,7 @@ function convertTo({ microseconds: Maybe; defaultValue?: string; }): ConvertedDuration { - if (microseconds == null) { + if (!isFiniteNumber(microseconds)) { return { value: defaultValue, formatted: defaultValue }; } @@ -143,6 +144,29 @@ export const getDurationFormatter: TimeFormatterBuilder = memoize( } ); +export function asTransactionRate(value: Maybe) { + if (!isFiniteNumber(value)) { + return NOT_AVAILABLE_LABEL; + } + + let displayedValue: string; + + if (value === 0) { + displayedValue = '0'; + } else if (value <= 0.1) { + displayedValue = '< 0.1'; + } else { + displayedValue = asDecimal(value); + } + + return i18n.translate('xpack.apm.transactionRateLabel', { + defaultMessage: `{value} tpm`, + values: { + value: displayedValue, + }, + }); +} + /** * Converts value and returns it formatted - 00 unit */ @@ -150,7 +174,7 @@ export function asDuration( value: Maybe, { defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} ) { - if (value == null) { + if (!isFiniteNumber(value)) { return defaultValue; } diff --git a/x-pack/plugins/apm/common/utils/formatters/formatters.ts b/x-pack/plugins/apm/common/utils/formatters/formatters.ts index d84bf86d0de2f..2314e915e3161 100644 --- a/x-pack/plugins/apm/common/utils/formatters/formatters.ts +++ b/x-pack/plugins/apm/common/utils/formatters/formatters.ts @@ -5,6 +5,9 @@ */ import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; +import { Maybe } from '../../../typings/common'; +import { NOT_AVAILABLE_LABEL } from '../../i18n'; +import { isFiniteNumber } from '../is_finite_number'; export function asDecimal(value: number) { return numeral(value).format('0,0.0'); @@ -25,11 +28,11 @@ export function tpmUnit(type?: string) { } export function asPercent( - numerator: number, + numerator: Maybe, denominator: number | undefined, - fallbackResult = '' + fallbackResult = NOT_AVAILABLE_LABEL ) { - if (!denominator || isNaN(numerator)) { + if (!denominator || !isFiniteNumber(numerator)) { return fallbackResult; } diff --git a/x-pack/plugins/apm/common/utils/is_finite_number.ts b/x-pack/plugins/apm/common/utils/is_finite_number.ts new file mode 100644 index 0000000000000..47c4f5fdbd0ee --- /dev/null +++ b/x-pack/plugins/apm/common/utils/is_finite_number.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. + */ +import { isFinite } from 'lodash'; +import { Maybe } from '../../typings/common'; + +// _.isNumber() returns true for NaN, _.isFinite() does not refine +export function isFiniteNumber(value: Maybe): value is number { + return isFinite(value); +} diff --git a/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx b/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx new file mode 100644 index 0000000000000..3ad71b52b6037 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + ScaleType, + Chart, + LineSeries, + Axis, + CurveType, + Position, + timeFormatter, + Settings, +} from '@elastic/charts'; +import React, { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; +import { + APIReturnType, + callApmApi, +} from '../../../services/rest/createCallApmApi'; +import { px } from '../../../style/variables'; +import { SignificantTermsTable } from './SignificantTermsTable'; +import { ChartContainer } from '../../shared/charts/chart_container'; + +type CorrelationsApiResponse = NonNullable< + APIReturnType<'GET /api/apm/correlations/failed_transactions'> +>; + +type SignificantTerm = NonNullable< + CorrelationsApiResponse['significantTerms'] +>[0]; + +export function ErrorCorrelations() { + const [ + selectedSignificantTerm, + setSelectedSignificantTerm, + ] = useState(null); + + const { serviceName } = useParams<{ serviceName?: string }>(); + const { urlParams, uiFilters } = useUrlParams(); + const { transactionName, transactionType, start, end } = urlParams; + + const { data, status } = useFetcher(() => { + if (start && end) { + return callApmApi({ + endpoint: 'GET /api/apm/correlations/failed_transactions', + params: { + query: { + serviceName, + transactionName, + transactionType, + start, + end, + uiFilters: JSON.stringify(uiFilters), + fieldNames: + 'transaction.name,user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,kubernetes.pod.name,url.domain,container.id,service.node.name', + }, + }, + }); + } + }, [serviceName, start, end, transactionName, transactionType, uiFilters]); + + return ( + <> + + + +

Error rate over time

+
+ +
+ + + +
+ + ); +} + +function ErrorTimeseriesChart({ + data, + selectedSignificantTerm, + status, +}: { + data?: CorrelationsApiResponse; + selectedSignificantTerm: SignificantTerm | null; + status: FETCH_STATUS; +}) { + const dateFormatter = timeFormatter('HH:mm:ss'); + + return ( + + + + + + `${roundFloat(d * 100)}%`} + /> + + + + {selectedSignificantTerm !== null ? ( + + ) : null} + + + ); +} + +function roundFloat(n: number, digits = 2) { + const factor = Math.pow(10, digits); + return Math.round(n * factor) / factor; +} diff --git a/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx b/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx new file mode 100644 index 0000000000000..4364731501b89 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx @@ -0,0 +1,273 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + ScaleType, + Chart, + LineSeries, + Axis, + CurveType, + BarSeries, + Position, + timeFormatter, + Settings, +} from '@elastic/charts'; +import React, { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { getDurationFormatter } from '../../../../common/utils/formatters'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; +import { + APIReturnType, + callApmApi, +} from '../../../services/rest/createCallApmApi'; +import { SignificantTermsTable } from './SignificantTermsTable'; +import { ChartContainer } from '../../shared/charts/chart_container'; + +type CorrelationsApiResponse = NonNullable< + APIReturnType<'GET /api/apm/correlations/slow_transactions'> +>; + +type SignificantTerm = NonNullable< + CorrelationsApiResponse['significantTerms'] +>[0]; + +export function LatencyCorrelations() { + const [ + selectedSignificantTerm, + setSelectedSignificantTerm, + ] = useState(null); + + const { serviceName } = useParams<{ serviceName?: string }>(); + const { urlParams, uiFilters } = useUrlParams(); + const { transactionName, transactionType, start, end } = urlParams; + + const { data, status } = useFetcher(() => { + if (start && end) { + return callApmApi({ + endpoint: 'GET /api/apm/correlations/slow_transactions', + params: { + query: { + serviceName, + transactionName, + transactionType, + start, + end, + uiFilters: JSON.stringify(uiFilters), + durationPercentile: '50', + fieldNames: + 'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,kubernetes.pod.name,url.domain,container.id,service.node.name', + }, + }, + }); + } + }, [serviceName, start, end, transactionName, transactionType, uiFilters]); + + return ( + <> + + + + + +

Average latency over time

+
+ +
+ + +

Latency distribution

+
+ +
+
+
+ + + +
+ + ); +} + +function getTimeseriesYMax(data?: CorrelationsApiResponse) { + if (!data?.overall) { + return 0; + } + + const yValues = [ + ...data.overall.timeseries.map((p) => p.y ?? 0), + ...data.significantTerms.flatMap((term) => + term.timeseries.map((p) => p.y ?? 0) + ), + ]; + return Math.max(...yValues); +} + +function getDistributionYMax(data?: CorrelationsApiResponse) { + if (!data?.overall) { + return 0; + } + + const yValues = [ + ...data.overall.distribution.map((p) => p.y ?? 0), + ...data.significantTerms.flatMap((term) => + term.distribution.map((p) => p.y ?? 0) + ), + ]; + return Math.max(...yValues); +} + +function LatencyTimeseriesChart({ + data, + selectedSignificantTerm, + status, +}: { + data?: CorrelationsApiResponse; + selectedSignificantTerm: SignificantTerm | null; + status: FETCH_STATUS; +}) { + const dateFormatter = timeFormatter('HH:mm:ss'); + + const yMax = getTimeseriesYMax(data); + const durationFormatter = getDurationFormatter(yMax); + + return ( + + + + + + durationFormatter(d).formatted} + /> + + + + {selectedSignificantTerm !== null ? ( + + ) : null} + + + ); +} + +function LatencyDistributionChart({ + data, + selectedSignificantTerm, + status, +}: { + data?: CorrelationsApiResponse; + selectedSignificantTerm: SignificantTerm | null; + status: FETCH_STATUS; +}) { + const xMax = Math.max( + ...(data?.overall?.distribution.map((p) => p.x ?? 0) ?? []) + ); + const durationFormatter = getDurationFormatter(xMax); + const yMax = getDistributionYMax(data); + + return ( + + + { + const start = durationFormatter(obj.value); + const end = durationFormatter( + obj.value + data?.distributionInterval + ); + + return `${start.value} - ${end.formatted}`; + }, + }} + /> + durationFormatter(d).formatted} + /> + `${d}%`} + domain={{ min: 0, max: yMax }} + /> + + `${roundFloat(d)}%`} + /> + + {selectedSignificantTerm !== null ? ( + `${roundFloat(d)}%`} + /> + ) : null} + + + ); +} + +function roundFloat(n: number, digits = 2) { + const factor = Math.pow(10, digits); + return Math.round(n * factor) / factor; +} diff --git a/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx b/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx new file mode 100644 index 0000000000000..b74517902f89b --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx @@ -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 React from 'react'; +import { EuiBadge, EuiIcon, EuiToolTip, EuiLink } from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; +import { EuiBasicTable } from '@elastic/eui'; +import { asPercent, asInteger } from '../../../../common/utils/formatters'; +import { APIReturnType } from '../../../services/rest/createCallApmApi'; +import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { createHref } from '../../shared/Links/url_helpers'; + +type CorrelationsApiResponse = + | APIReturnType<'GET /api/apm/correlations/failed_transactions'> + | APIReturnType<'GET /api/apm/correlations/slow_transactions'>; + +type SignificantTerm = NonNullable< + NonNullable['significantTerms'] +>[0]; + +interface Props { + significantTerms?: T[]; + status: FETCH_STATUS; + setSelectedSignificantTerm: (term: T | null) => void; +} + +export function SignificantTermsTable({ + significantTerms, + status, + setSelectedSignificantTerm, +}: Props) { + const history = useHistory(); + const columns = [ + { + field: 'matches', + name: 'Matches', + render: (_: any, term: T) => { + return ( + + <> + 0.03 ? 'primary' : 'secondary' + } + > + {asPercent(term.fgCount, term.bgCount)} + + ({Math.round(term.score)}) + + + ); + }, + }, + { + field: 'fieldName', + name: 'Field name', + }, + { + field: 'filedValue', + name: 'Field value', + render: (_: any, term: T) => String(term.fieldValue).slice(0, 50), + }, + { + field: 'filedValue', + name: '', + render: (_: any, term: T) => { + return ( + <> + + + + + + + + ); + }, + }, + ]; + + return ( + { + return { + onMouseEnter: () => setSelectedSignificantTerm(term), + onMouseLeave: () => setSelectedSignificantTerm(null), + }; + }} + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Correlations/index.tsx b/x-pack/plugins/apm/public/components/app/Correlations/index.tsx index e3dea70a232eb..b0f6b83485e39 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Correlations/index.tsx @@ -4,82 +4,75 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import url from 'url'; -import { useParams } from 'react-router-dom'; -import { useLocation } from 'react-router-dom'; -import { EuiTitle, EuiListGroup } from '@elastic/eui'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import React, { useState } from 'react'; +import { + EuiButtonEmpty, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiPortal, + EuiCode, + EuiLink, + EuiCallOut, + EuiButton, +} from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; +import { enableCorrelations } from '../../../../common/ui_settings_keys'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; - -const SESSION_STORAGE_KEY = 'apm.debug.show_correlations'; +import { LatencyCorrelations } from './LatencyCorrelations'; +import { ErrorCorrelations } from './ErrorCorrelations'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { createHref } from '../../shared/Links/url_helpers'; export function Correlations() { - const location = useLocation(); - const { serviceName } = useParams<{ serviceName?: string }>(); - const { urlParams, uiFilters } = useUrlParams(); - const { core } = useApmPluginContext(); - const { transactionName, transactionType, start, end } = urlParams; - - if ( - !location.search.includes('&_show_correlations') && - sessionStorage.getItem(SESSION_STORAGE_KEY) !== 'true' - ) { + const { uiSettings } = useApmPluginContext().core; + const { urlParams } = useUrlParams(); + const history = useHistory(); + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + if (!uiSettings.get(enableCorrelations)) { return null; } - sessionStorage.setItem(SESSION_STORAGE_KEY, 'true'); - - const query = { - serviceName, - transactionName, - transactionType, - start, - end, - uiFilters: JSON.stringify(uiFilters), - fieldNames: - 'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,url.domain,container.id,service.node.name', - }; - - const listItems = [ - { - label: 'Show correlations between two ranges', - href: url.format({ - query: { - ...query, - gap: 24, - }, - pathname: core.http.basePath.prepend(`/api/apm/correlations/ranges`), - }), - isDisabled: false, - iconType: 'tokenRange', - size: 's' as const, - }, - - { - label: 'Show correlations for slow transactions', - href: url.format({ - query: { - ...query, - durationPercentile: 95, - }, - pathname: core.http.basePath.prepend( - `/api/apm/correlations/slow_durations` - ), - }), - isDisabled: false, - iconType: 'clock', - size: 's' as const, - }, - ]; - return ( <> - -

Correlations

-
+ { + setIsFlyoutVisible(true); + }} + > + View correlations + + + {isFlyoutVisible && ( + + setIsFlyoutVisible(false)} + > + + +

Correlations

+
+
+ + {urlParams.kuery ? ( + + Filtering by + {urlParams.kuery} + + Clear + + + ) : null} - + + + +
+
+ )} ); } diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx index 5202ca13ed102..777ee014d3e58 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx @@ -20,8 +20,7 @@ import { first } from 'lodash'; import React from 'react'; import { useHistory } from 'react-router-dom'; import styled from 'styled-components'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ErrorGroupAPIResponse } from '../../../../../server/lib/errors/get_error_group'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { px, unit, units } from '../../../../style/variables'; @@ -56,7 +55,9 @@ const TransactionLinkName = styled.div` `; interface Props { - errorGroup: ErrorGroupAPIResponse; + errorGroup: APIReturnType< + 'GET /api/apm/services/{serviceName}/errors/{groupId}' + >; urlParams: IUrlParams; location: Location; } diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index a17bf7e93e466..fd656b8be6ec7 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -18,11 +18,14 @@ import { import { EuiTitle } from '@elastic/eui'; import d3 from 'd3'; import React from 'react'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asRelativeDateTimeRange } from '../../../../../common/utils/formatters'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { ErrorDistributionAPIResponse } from '../../../../../server/lib/errors/distribution/get_distribution'; import { useTheme } from '../../../../hooks/useTheme'; +type ErrorDistributionAPIResponse = APIReturnType< + 'GET /api/apm/services/{serviceName}/errors/distribution' +>; + interface FormattedBucket { x0: number; x: number; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx index e1f6239112555..bfa426985d1c6 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx @@ -10,9 +10,8 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import styled from 'styled-components'; import { EuiIconTip } from '@elastic/eui'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ErrorGroupListAPIResponse } from '../../../../../server/lib/errors/get_error_groups'; import { fontFamilyCode, fontSizes, @@ -49,6 +48,10 @@ const Culprit = styled.div` font-family: ${fontFamilyCode}; `; +type ErrorGroupListAPIResponse = APIReturnType< + 'GET /api/apm/services/{serviceName}/errors' +>; + interface Props { items: ErrorGroupListAPIResponse; serviceName: string; 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 1628a664a6c27..8463da0824bde 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 @@ -65,21 +65,19 @@ export function ServiceStatsList({ title: i18n.translate('xpack.apm.serviceMap.errorRatePopoverStat', { defaultMessage: 'Trans. error rate (avg.)', }), - description: isNumber(avgErrorRate) ? asPercent(avgErrorRate, 1) : null, + description: asPercent(avgErrorRate, 1, ''), }, { title: i18n.translate('xpack.apm.serviceMap.avgCpuUsagePopoverStat', { defaultMessage: 'CPU usage (avg.)', }), - description: isNumber(avgCpuUsage) ? asPercent(avgCpuUsage, 1) : null, + description: asPercent(avgCpuUsage, 1, ''), }, { title: i18n.translate('xpack.apm.serviceMap.avgMemoryUsagePopoverStat', { defaultMessage: 'Memory usage (avg.)', }), - description: isNumber(avgMemoryUsage) - ? asPercent(avgMemoryUsage, 1) - : null, + description: asPercent(avgMemoryUsage, 1, ''), }, ]; 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 5c9677e3c7af2..89c5c801a5683 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx @@ -128,7 +128,7 @@ function ServiceNodeOverview({ serviceName }: ServiceNodeOverviewProps) { }), field: 'cpu', sortable: true, - render: (value: number | null) => asPercent(value || 0, 1), + render: (value: number | null) => asPercent(value, 1), }, { name: i18n.translate('xpack.apm.jvmsTable.heapMemoryColumnLabel', { diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx index 3483ad0822801..adae50db85ada 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx @@ -8,13 +8,14 @@ import React, { useState } from 'react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { NotificationsStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AgentConfigurationListAPIResponse } from '../../../../../../server/lib/settings/agent_configuration/list_configurations'; import { getOptionLabel } from '../../../../../../common/agent_configuration/all_option'; -import { callApmApi } from '../../../../../services/rest/createCallApmApi'; +import { + APIReturnType, + callApmApi, +} from '../../../../../services/rest/createCallApmApi'; import { useApmPluginContext } from '../../../../../hooks/useApmPluginContext'; -type Config = AgentConfigurationListAPIResponse[0]; +type Config = APIReturnType<'GET /api/apm/settings/agent-configuration'>[0]; interface Props { config: Config; diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx index a67df86b21b1e..81079d78a148a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx @@ -16,9 +16,8 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React, { useState } from 'react'; import { useLocation } from 'react-router-dom'; +import { APIReturnType } from '../../../../../services/rest/createCallApmApi'; import { getOptionLabel } from '../../../../../../common/agent_configuration/all_option'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AgentConfigurationListAPIResponse } from '../../../../../../server/lib/settings/agent_configuration/list_configurations'; import { useApmPluginContext } from '../../../../../hooks/useApmPluginContext'; import { FETCH_STATUS } from '../../../../../hooks/useFetcher'; import { useTheme } from '../../../../../hooks/useTheme'; @@ -32,7 +31,7 @@ import { ITableColumn, ManagedTable } from '../../../../shared/ManagedTable'; import { TimestampTooltip } from '../../../../shared/TimestampTooltip'; import { ConfirmDeleteModal } from './ConfirmDeleteModal'; -type Config = AgentConfigurationListAPIResponse[0]; +type Config = APIReturnType<'GET /api/apm/settings/agent-configuration'>[0]; interface Props { status: FETCH_STATUS; diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx index 1c21824656754..4704230d7c68c 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx @@ -8,8 +8,6 @@ import { EuiIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import styled from 'styled-components'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TransactionGroup } from '../../../../server/lib/transaction_groups/fetcher'; import { asMillisecondDuration } from '../../../../common/utils/formatters'; import { fontSizes, truncate } from '../../../style/variables'; import { EmptyMessage } from '../../shared/EmptyMessage'; @@ -17,6 +15,9 @@ import { ImpactBar } from '../../shared/ImpactBar'; import { ITableColumn, ManagedTable } from '../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../shared/LoadingStatePrompt'; import { TransactionDetailLink } from '../../shared/Links/apm/TransactionDetailLink'; +import { APIReturnType } from '../../../services/rest/createCallApmApi'; + +type TraceGroup = APIReturnType<'GET /api/apm/traces'>['items'][0]; const StyledTransactionLink = styled(TransactionDetailLink)` font-size: ${fontSizes.large}; @@ -24,11 +25,11 @@ const StyledTransactionLink = styled(TransactionDetailLink)` `; interface Props { - items: TransactionGroup[]; + items: TraceGroup[]; isLoading: boolean; } -const traceListColumns: Array> = [ +const traceListColumns: Array> = [ { field: 'name', name: i18n.translate('xpack.apm.tracesTable.nameColumnLabel', { @@ -38,7 +39,7 @@ const traceListColumns: Array> = [ sortable: true, render: ( _: string, - { serviceName, transactionName, transactionType }: TransactionGroup + { serviceName, transactionName, transactionType }: TraceGroup ) => ( ; + +type DistributionApiResponse = APIReturnType< + 'GET /api/apm/services/{serviceName}/transaction_groups/distribution' +>; + +type DistributionBucket = DistributionApiResponse['buckets'][0]; + interface IChartPoint { x0: number; x: number; 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 3bb23fd6396ca..86221a6e92853 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 @@ -17,8 +17,7 @@ import { i18n } from '@kbn/i18n'; import { Location } from 'history'; import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { DistributionBucket } from '../../../../../server/lib/transactions/distribution/get_buckets'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; @@ -28,6 +27,12 @@ import { MaybeViewTraceLink } from './MaybeViewTraceLink'; import { TransactionTabs } from './TransactionTabs'; import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; +type DistributionApiResponse = APIReturnType< + 'GET /api/apm/services/{serviceName}/transaction_groups/distribution' +>; + +type DistributionBucket = DistributionApiResponse['buckets'][0]; + interface Props { urlParams: IUrlParams; location: Location; 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 9d9261fec6c1e..cc6bacc4f3ccb 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -119,9 +119,9 @@ export function TransactionDetails({ - + diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/TransactionList.stories.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/TransactionList.stories.tsx index 049c5934813a2..65dfdd19fa0c5 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/TransactionList.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/TransactionList.stories.tsx @@ -6,11 +6,14 @@ import React, { ComponentType } from 'react'; import { MemoryRouter } from 'react-router-dom'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TransactionGroup } from '../../../../../server/lib/transaction_groups/fetcher'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; import { TransactionList } from './'; +type TransactionGroup = APIReturnType< + 'GET /api/apm/services/{serviceName}/transaction_groups' +>['items'][0]; + export default { title: 'app/TransactionOverview/TransactionList', component: TransactionList, diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/index.tsx index 7f1dd100d721c..b084d05ee16e8 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/index.tsx @@ -8,8 +8,7 @@ import { EuiToolTip, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import styled from 'styled-components'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TransactionGroup } from '../../../../../server/lib/transaction_groups/fetcher'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asDecimal, asMillisecondDuration, @@ -21,6 +20,10 @@ import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; import { EmptyMessage } from '../../../shared/EmptyMessage'; import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; +type TransactionGroup = APIReturnType< + 'GET /api/apm/services/{serviceName}/transaction_groups' +>['items'][0]; + // Truncate both the link and the child span (the tooltip anchor.) The link so // it doesn't overflow, and the anchor so we get the ellipsis. const TransactionNameLink = styled(TransactionDetailLink)` 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 a4f8d37867dd5..8208916c20337 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -123,10 +123,11 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { return ( <> - + + ; valueLabel: React.ReactNode; }) { - const { - urlParams: { start, end }, - } = useUrlParams(); - return ( ; +type Items = ServiceListAPIResponse['items']; + interface Props { - items: ServiceListAPIResponse['items']; + items: Items; noItemsMessage?: React.ReactNode; } - -type ServiceListItem = ValuesType; +type ServiceListItem = ValuesType; function formatNumber(value: number) { if (value === 0) { @@ -176,8 +177,7 @@ export const SERVICE_COLUMNS: Array> = [ render: (_, { transactionErrorRate }) => { const value = transactionErrorRate?.value; - const valueLabel = - value !== null && value !== undefined ? asPercent(value, 1) : ''; + const valueLabel = asPercent(value, 1); return ( ; + function Wrapper({ children }: { children?: ReactNode }) { return ( diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index 83f5f4deb89a3..3fa047d840dda 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -129,9 +129,9 @@ export function ServiceInventory() { <> - + diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 50667d3135f1a..f734abe27573c 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -18,9 +18,9 @@ import { isRumAgentName } from '../../../../common/agent_name'; import { ChartsSyncContextProvider } from '../../../context/charts_sync_context'; import { TransactionErrorRateChart } from '../../shared/charts/transaction_error_rate_chart'; import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink'; -import { TransactionOverviewLink } from '../../shared/Links/apm/TransactionOverviewLink'; import { SearchBar } from '../../shared/search_bar'; import { ServiceOverviewErrorsTable } from './service_overview_errors_table'; +import { ServiceOverviewTransactionsTable } from './service_overview_transactions_table'; import { TableLinkFlexItem } from './table_link_flex_item'; /** @@ -78,30 +78,7 @@ export function ServiceOverview({ - - - -

- {i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableTitle', - { - defaultMessage: 'Transactions', - } - )} -

-
-
- - - {i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableLinkText', - { - defaultMessage: 'View transactions', - } - )} - - -
+
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index 82dbd6dd86aab..b4228878dd9f5 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -21,10 +21,10 @@ import { px, truncate, unit } from '../../../../style/variables'; import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; +import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; import { TimestampTooltip } from '../../../shared/TimestampTooltip'; import { ServiceOverviewTable } from '../service_overview_table'; import { TableLinkFlexItem } from '../table_link_flex_item'; -import { FetchWrapper } from './fetch_wrapper'; interface Props { serviceName: string; @@ -135,8 +135,6 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { }, } )} - start={parseFloat(start!)} - end={parseFloat(end!)} /> ); }, @@ -225,7 +223,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
- + - +
); diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx new file mode 100644 index 0000000000000..e91ab338c4a27 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx @@ -0,0 +1,319 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + EuiBasicTableColumn, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { EuiToolTip } from '@elastic/eui'; +import { ValuesType } from 'utility-types'; +import { + asDuration, + asPercent, + asTransactionRate, +} from '../../../../../common/utils/formatters'; +import { px, truncate, unit } from '../../../../style/variables'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { + APIReturnType, + callApmApi, +} from '../../../../services/rest/createCallApmApi'; +import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; +import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink'; +import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; +import { TableLinkFlexItem } from '../table_link_flex_item'; +import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; +import { ImpactBar } from '../../../shared/ImpactBar'; +import { ServiceOverviewTable } from '../service_overview_table'; + +type ServiceTransactionGroupItem = ValuesType< + APIReturnType< + 'GET /api/apm/services/{serviceName}/overview_transaction_groups' + >['transactionGroups'] +>; + +interface Props { + serviceName: string; +} + +type SortField = 'latency' | 'throughput' | 'errorRate' | 'impact'; +type SortDirection = 'asc' | 'desc'; + +const PAGE_SIZE = 5; +const DEFAULT_SORT = { + direction: 'desc' as const, + field: 'impact' as const, +}; + +const TransactionGroupLinkWrapper = styled.div` + width: 100%; + .euiToolTipAnchor { + width: 100% !important; + } +`; + +const StyledTransactionDetailLink = styled(TransactionDetailLink)` + display: block; + ${truncate('100%')} +`; + +export function ServiceOverviewTransactionsTable(props: Props) { + const { serviceName } = props; + + const { + uiFilters, + urlParams: { start, end }, + } = useUrlParams(); + + const [tableOptions, setTableOptions] = useState<{ + pageIndex: number; + sort: { + direction: SortDirection; + field: SortField; + }; + }>({ + pageIndex: 0, + sort: DEFAULT_SORT, + }); + + const { + data = { + totalItemCount: 0, + items: [], + tableOptions: { + pageIndex: 0, + sort: DEFAULT_SORT, + }, + }, + status, + } = useFetcher(() => { + if (!start || !end) { + return; + } + + return callApmApi({ + endpoint: + 'GET /api/apm/services/{serviceName}/overview_transaction_groups', + params: { + path: { serviceName }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + size: PAGE_SIZE, + numBuckets: 20, + pageIndex: tableOptions.pageIndex, + sortField: tableOptions.sort.field, + sortDirection: tableOptions.sort.direction, + }, + }, + }).then((response) => { + return { + items: response.transactionGroups, + totalItemCount: response.totalTransactionGroups, + tableOptions: { + pageIndex: tableOptions.pageIndex, + sort: { + field: tableOptions.sort.field, + direction: tableOptions.sort.direction, + }, + }, + }; + }); + }, [ + serviceName, + start, + end, + uiFilters, + tableOptions.pageIndex, + tableOptions.sort.field, + tableOptions.sort.direction, + ]); + + const { + items, + totalItemCount, + tableOptions: { pageIndex, sort }, + } = data; + + const columns: Array> = [ + { + field: 'name', + name: i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableColumnName', + { + defaultMessage: 'Name', + } + ), + render: (_, { name, transactionType }) => { + return ( + + + + {name} + + + + ); + }, + }, + { + field: 'latency', + name: i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableColumnLatency', + { + defaultMessage: 'Latency', + } + ), + width: px(unit * 10), + render: (_, { latency }) => { + return ( + + ); + }, + }, + { + field: 'throughput', + name: i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableColumnTroughput', + { + defaultMessage: 'Traffic', + } + ), + width: px(unit * 10), + render: (_, { throughput }) => { + return ( + + ); + }, + }, + { + field: 'error_rate', + name: i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableColumnErrorRate', + { + defaultMessage: 'Error rate', + } + ), + width: px(unit * 8), + render: (_, { errorRate }) => { + return ( + + ); + }, + }, + { + field: 'impact', + name: i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableColumnImpact', + { + defaultMessage: 'Impact', + } + ), + width: px(unit * 5), + render: (_, { impact }) => { + return ; + }, + }, + ]; + + return ( + + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableTitle', + { + defaultMessage: 'Transactions', + } + )} +

+
+
+ + + {i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableLinkText', + { + defaultMessage: 'View transactions', + } + )} + + +
+
+ + + + { + setTableOptions({ + pageIndex: newTableOptions.page?.index ?? 0, + sort: newTableOptions.sort + ? { + field: newTableOptions.sort.field as SortField, + direction: newTableOptions.sort.direction, + } + : DEFAULT_SORT, + }); + }} + sorting={{ + enableAllColumns: true, + sort: { + direction: sort.direction, + field: sort.field, + }, + }} + /> + + + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx b/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx index ed931191cfb96..f5d71ad15f1ce 100644 --- a/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx @@ -10,11 +10,23 @@ import React from 'react'; // TODO: extend from EUI's EuiProgress prop interface export interface ImpactBarProps extends Record { value: number; + size?: 'l' | 'm'; max?: number; } -export function ImpactBar({ value, max = 100, ...rest }: ImpactBarProps) { +export function ImpactBar({ + value, + size = 'l', + max = 100, + ...rest +}: ImpactBarProps) { return ( - + ); } diff --git a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts index 991735a450724..9da26b3fcefac 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { History } from 'history'; import { parse, stringify } from 'query-string'; import { url } from '../../../../../../../src/plugins/kibana_utils/public'; import { LocalUIFilterName } from '../../../../common/ui_filter'; @@ -20,6 +21,48 @@ export function fromQuery(query: Record) { return stringify(encodedQuery, { sort: false, encode: false }); } +type LocationWithQuery = Partial< + History['location'] & { + query: Record; + } +>; + +function getNextLocation( + history: History, + locationWithQuery: LocationWithQuery +) { + const { query, ...rest } = locationWithQuery; + return { + ...history.location, + ...rest, + search: fromQuery({ + ...toQuery(history.location.search), + ...query, + }), + }; +} + +export function replace( + history: History, + locationWithQuery: LocationWithQuery +) { + const location = getNextLocation(history, locationWithQuery); + return history.replace(location); +} + +export function push(history: History, locationWithQuery: LocationWithQuery) { + const location = getNextLocation(history, locationWithQuery); + return history.push(location); +} + +export function createHref( + history: History, + locationWithQuery: LocationWithQuery +) { + const location = getNextLocation(history, locationWithQuery); + return history.createHref(location); +} + export type APMQueryParams = { transactionId?: string; transactionName?: string; diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx index 18b914afea995..d96f3cd698aed 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx @@ -15,16 +15,15 @@ import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; interface Props { color: string; - series: Array<{ x: number; y: number | null }>; + series?: Array<{ x: number; y: number | null }>; + width: string; } export function SparkPlot(props: Props) { - const { series, color } = props; + const { series, color, width } = props; const chartTheme = useChartTheme(); - const isEmpty = series.every((point) => point.y === null); - - if (isEmpty) { + if (!series || series.every((point) => point.y === null)) { return ( @@ -40,7 +39,7 @@ export function SparkPlot(props: Props) { } return ( - + ; valueLabel: React.ReactNode; + compact?: boolean; }) { const theme = useTheme(); @@ -45,7 +43,8 @@ export function SparkPlotWithValueLabel({ diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx b/x-pack/plugins/apm/public/components/shared/table_fetch_wrapper/index.tsx similarity index 74% rename from x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx rename to x-pack/plugins/apm/public/components/shared/table_fetch_wrapper/index.tsx index 912490d866e88..e8d62cd8bd85b 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx +++ b/x-pack/plugins/apm/public/components/shared/table_fetch_wrapper/index.tsx @@ -5,10 +5,10 @@ */ import React, { ReactNode } from 'react'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; -import { ErrorStatePrompt } from '../../../shared/ErrorStatePrompt'; +import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { ErrorStatePrompt } from '../ErrorStatePrompt'; -export function FetchWrapper({ +export function TableFetchWrapper({ status, children, }: { diff --git a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts index 36b5a7c00d4be..9980569ee54dd 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts @@ -9,13 +9,16 @@ import { useHistory, useParams } from 'react-router-dom'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { useFetcher } from './useFetcher'; import { useUiFilters } from '../context/UrlParamsContext'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { TransactionDistributionAPIResponse } from '../../server/lib/transactions/distribution'; import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; import { maybe } from '../../common/utils/maybe'; +import { APIReturnType } from '../services/rest/createCallApmApi'; + +type APIResponse = APIReturnType< + 'GET /api/apm/services/{serviceName}/transaction_groups/distribution' +>; const INITIAL_DATA = { - buckets: [] as TransactionDistributionAPIResponse['buckets'], + buckets: [] as APIResponse['buckets'], noHits: true, bucketSize: 0, }; diff --git a/x-pack/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md index 0adfb99e7164e..00d7e8e1dd5e4 100644 --- a/x-pack/plugins/apm/readme.md +++ b/x-pack/plugins/apm/readme.md @@ -115,6 +115,12 @@ For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) _Note: Run the following commands from `kibana/`._ +### Typescript + +``` +yarn tsc --noEmit --project x-pack/tsconfig.json --skipLibCheck +``` + ### Prettier ``` diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index a10762622b2c6..449aa88752f21 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -10,7 +10,6 @@ import { snakeCase } from 'lodash'; import Boom from '@hapi/boom'; import { ProcessorEvent } from '../../../common/processor_event'; import { ML_ERRORS } from '../../../common/anomaly_detection'; -import { PromiseReturnType } from '../../../../observability/typings/common'; import { Setup } from '../helpers/setup_request'; import { TRANSACTION_DURATION, @@ -19,9 +18,6 @@ import { import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; -export type CreateAnomalyDetectionJobsAPIResponse = PromiseReturnType< - typeof createAnomalyDetectionJobs ->; export async function createAnomalyDetectionJobs( setup: Setup, environments: string[], diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts new file mode 100644 index 0000000000000..ba739310bc342 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { EventOutcome } from '../../../../common/event_outcome'; +import { + formatTopSignificantTerms, + TopSigTerm, +} from '../get_correlations_for_slow_transactions/format_top_significant_terms'; +import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch/aggregations'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { + EVENT_OUTCOME, + SERVICE_NAME, + TRANSACTION_NAME, + TRANSACTION_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { + getOutcomeAggregation, + getTransactionErrorRateTimeSeries, +} from '../../helpers/transaction_error_rate'; + +export async function getCorrelationsForFailedTransactions({ + serviceName, + transactionType, + transactionName, + fieldNames, + setup, +}: { + serviceName: string | undefined; + transactionType: string | undefined; + transactionName: string | undefined; + fieldNames: string[]; + setup: Setup & SetupTimeRange; +}) { + const { start, end, esFilter, apmEventClient } = setup; + + const backgroundFilters: ESFilter[] = [ + ...esFilter, + { range: rangeFilter(start, end) }, + ]; + + if (serviceName) { + backgroundFilters.push({ term: { [SERVICE_NAME]: serviceName } }); + } + + if (transactionType) { + backgroundFilters.push({ term: { [TRANSACTION_TYPE]: transactionType } }); + } + + if (transactionName) { + backgroundFilters.push({ term: { [TRANSACTION_NAME]: transactionName } }); + } + + const params = { + apm: { events: [ProcessorEvent.transaction] }, + body: { + size: 0, + query: { + bool: { + // foreground filters + filter: [ + ...backgroundFilters, + { term: { [EVENT_OUTCOME]: EventOutcome.failure } }, + ], + }, + }, + aggs: fieldNames.reduce((acc, fieldName) => { + return { + ...acc, + [fieldName]: { + significant_terms: { + size: 10, + field: fieldName, + background_filter: { bool: { filter: backgroundFilters } }, + }, + }, + }; + }, {} as Record), + }, + }; + + const response = await apmEventClient.search(params); + const topSigTerms = formatTopSignificantTerms(response.aggregations); + return getChartsForTopSigTerms({ setup, backgroundFilters, topSigTerms }); +} + +export async function getChartsForTopSigTerms({ + setup, + backgroundFilters, + topSigTerms, +}: { + setup: Setup & SetupTimeRange; + backgroundFilters: ESFilter[]; + topSigTerms: TopSigTerm[]; +}) { + const { start, end, apmEventClient } = setup; + const { intervalString } = getBucketSize({ start, end, numBuckets: 30 }); + + if (isEmpty(topSigTerms)) { + return {}; + } + + const timeseriesAgg = { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, + aggs: { + // TODO: add support for metrics + outcomes: getOutcomeAggregation({ searchAggregatedTransactions: false }), + }, + }; + + const perTermAggs = topSigTerms.reduce( + (acc, term, index) => { + acc[`term_${index}`] = { + filter: { term: { [term.fieldName]: term.fieldValue } }, + aggs: { timeseries: timeseriesAgg }, + }; + return acc; + }, + {} as Record< + string, + { + filter: AggregationOptionsByType['filter']; + aggs: { timeseries: typeof timeseriesAgg }; + } + > + ); + + const params = { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: { + size: 0, + query: { bool: { filter: backgroundFilters } }, + aggs: { + // overall aggs + timeseries: timeseriesAgg, + + // per term aggs + ...perTermAggs, + }, + }, + }; + + const response = await apmEventClient.search(params); + type Agg = NonNullable; + + if (!response.aggregations) { + return {}; + } + + return { + overall: { + timeseries: getTransactionErrorRateTimeSeries( + response.aggregations.timeseries.buckets + ), + }, + significantTerms: topSigTerms.map((topSig, index) => { + // @ts-expect-error + const agg = response.aggregations[`term_${index}`] as Agg; + + return { + ...topSig, + timeseries: getTransactionErrorRateTimeSeries(agg.timeseries.buckets), + }; + }), + }; +} diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/format_top_significant_terms.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/format_top_significant_terms.ts new file mode 100644 index 0000000000000..f168b49fb18fd --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/format_top_significant_terms.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { orderBy } from 'lodash'; +import { + AggregationOptionsByType, + AggregationResultOf, +} from '../../../../../../typings/elasticsearch/aggregations'; + +export interface TopSigTerm { + bgCount: number; + fgCount: number; + fieldName: string; + fieldValue: string | number; + score: number; +} + +type SigTermAggs = AggregationResultOf< + { significant_terms: AggregationOptionsByType['significant_terms'] }, + {} +>; + +export function formatTopSignificantTerms( + aggregations?: Record +) { + const significantTerms = Object.entries(aggregations ?? []).flatMap( + ([fieldName, agg]) => { + return agg.buckets.map((bucket) => ({ + fieldName, + fieldValue: bucket.key, + bgCount: bucket.bg_count, + fgCount: bucket.doc_count, + score: bucket.score, + })); + } + ); + + // get top 10 terms ordered by score + const topSigTerms = orderBy(significantTerms, 'score', 'desc').slice(0, 10); + return topSigTerms; +} diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_charts_for_top_sig_terms.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_charts_for_top_sig_terms.ts new file mode 100644 index 0000000000000..cbefd5e2133e5 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_charts_for_top_sig_terms.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { AggregationOptionsByType } from '../../../../../../typings/elasticsearch/aggregations'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { TopSigTerm } from './format_top_significant_terms'; +import { getMaxLatency } from './get_max_latency'; + +export async function getChartsForTopSigTerms({ + setup, + backgroundFilters, + topSigTerms, +}: { + setup: Setup & SetupTimeRange; + backgroundFilters: ESFilter[]; + topSigTerms: TopSigTerm[]; +}) { + const { start, end, apmEventClient } = setup; + const { intervalString } = getBucketSize({ start, end, numBuckets: 30 }); + + if (isEmpty(topSigTerms)) { + return {}; + } + + const maxLatency = await getMaxLatency({ + setup, + backgroundFilters, + topSigTerms, + }); + + if (!maxLatency) { + return {}; + } + + const intervalBuckets = 20; + const distributionInterval = roundtoTenth(maxLatency / intervalBuckets); + + const distributionAgg = { + // filter out outliers not included in the significant term docs + filter: { range: { [TRANSACTION_DURATION]: { lte: maxLatency } } }, + aggs: { + dist_filtered_by_latency: { + histogram: { + // TODO: add support for metrics + field: TRANSACTION_DURATION, + interval: distributionInterval, + min_doc_count: 0, + extended_bounds: { + min: 0, + max: maxLatency, + }, + }, + }, + }, + }; + + const timeseriesAgg = { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, + aggs: { + average: { + avg: { + // TODO: add support for metrics + field: TRANSACTION_DURATION, + }, + }, + }, + }; + + const perTermAggs = topSigTerms.reduce( + (acc, term, index) => { + acc[`term_${index}`] = { + filter: { term: { [term.fieldName]: term.fieldValue } }, + aggs: { + distribution: distributionAgg, + timeseries: timeseriesAgg, + }, + }; + return acc; + }, + {} as Record< + string, + { + filter: AggregationOptionsByType['filter']; + aggs: { + distribution: typeof distributionAgg; + timeseries: typeof timeseriesAgg; + }; + } + > + ); + + const params = { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: { + size: 0, + query: { bool: { filter: backgroundFilters } }, + aggs: { + // overall aggs + distribution: distributionAgg, + timeseries: timeseriesAgg, + + // per term aggs + ...perTermAggs, + }, + }, + }; + + const response = await apmEventClient.search(params); + type Agg = NonNullable; + + if (!response.aggregations) { + return; + } + + function formatTimeseries(timeseries: Agg['timeseries']) { + return timeseries.buckets.map((bucket) => ({ + x: bucket.key, + y: bucket.average.value, + })); + } + + function formatDistribution(distribution: Agg['distribution']) { + const total = distribution.doc_count; + return distribution.dist_filtered_by_latency.buckets.map((bucket) => ({ + x: bucket.key, + y: (bucket.doc_count / total) * 100, + })); + } + + return { + distributionInterval, + overall: { + timeseries: formatTimeseries(response.aggregations.timeseries), + distribution: formatDistribution(response.aggregations.distribution), + }, + significantTerms: topSigTerms.map((topSig, index) => { + // @ts-expect-error + const agg = response.aggregations[`term_${index}`] as Agg; + + return { + ...topSig, + timeseries: formatTimeseries(agg.timeseries), + distribution: formatDistribution(agg.distribution), + }; + }), + }; +} + +function roundtoTenth(v: number) { + return Math.pow(10, Math.round(Math.log10(v))); +} diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_duration_for_percentile.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_duration_for_percentile.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_duration_for_percentile.ts rename to x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_duration_for_percentile.ts diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_max_latency.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_max_latency.ts new file mode 100644 index 0000000000000..3f86d2900e85b --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_max_latency.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 { ESFilter } from '../../../../../../typings/elasticsearch'; +import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { TopSigTerm } from './format_top_significant_terms'; + +export async function getMaxLatency({ + setup, + backgroundFilters, + topSigTerms, +}: { + setup: Setup & SetupTimeRange; + backgroundFilters: ESFilter[]; + topSigTerms: TopSigTerm[]; +}) { + const { apmEventClient } = setup; + + const params = { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: { + size: 0, + query: { + bool: { + filter: backgroundFilters, + + // only include docs containing the significant terms + should: topSigTerms.map((term) => ({ + term: { [term.fieldName]: term.fieldValue }, + })), + minimum_should_match: 1, + }, + }, + aggs: { + // TODO: add support for metrics + // max_latency: { max: { field: TRANSACTION_DURATION } }, + max_latency: { + percentiles: { field: TRANSACTION_DURATION, percents: [99] }, + }, + }, + }, + }; + + const response = await apmEventClient.search(params); + // return response.aggregations?.max_latency.value; + return Object.values(response.aggregations?.max_latency.values ?? {})[0]; +} diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_slow_transactions.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts similarity index 72% rename from x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_slow_transactions.ts rename to x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts index 76e595c928cf2..b8a5ab93591a4 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_slow_transactions.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch/aggregations'; import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { rangeFilter } from '../../../../common/utils/range_filter'; import { SERVICE_NAME, TRANSACTION_DURATION, @@ -12,15 +14,10 @@ import { TRANSACTION_TYPE, } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { asDuration } from '../../../../common/utils/formatters'; -import { rangeFilter } from '../../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getDurationForPercentile } from './get_duration_for_percentile'; -import { - formatAggregationResponse, - getSignificantTermsAgg, -} from './get_significant_terms_agg'; -import { SignificantTermsScoring } from './scoring_rt'; +import { formatTopSignificantTerms } from './format_top_significant_terms'; +import { getChartsForTopSigTerms } from './get_charts_for_top_sig_terms'; export async function getCorrelationsForSlowTransactions({ serviceName, @@ -28,13 +25,11 @@ export async function getCorrelationsForSlowTransactions({ transactionName, durationPercentile, fieldNames, - scoring, setup, }: { serviceName: string | undefined; transactionType: string | undefined; transactionName: string | undefined; - scoring: SignificantTermsScoring; durationPercentile: number; fieldNames: string[]; setup: Setup & SetupTimeRange; @@ -79,16 +74,22 @@ export async function getCorrelationsForSlowTransactions({ ], }, }, - aggs: getSignificantTermsAgg({ fieldNames, backgroundFilters, scoring }), + aggs: fieldNames.reduce((acc, fieldName) => { + return { + ...acc, + [fieldName]: { + significant_terms: { + size: 10, + field: fieldName, + background_filter: { bool: { filter: backgroundFilters } }, + }, + }, + }; + }, {} as Record), }, }; const response = await apmEventClient.search(params); - - return { - message: `Showing significant fields for transactions slower than ${durationPercentile}th percentile (${asDuration( - durationForPercentile - )})`, - response: formatAggregationResponse(response.aggregations), - }; + const topSigTerms = formatTopSignificantTerms(response.aggregations); + return getChartsForTopSigTerms({ setup, backgroundFilters, topSigTerms }); } diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts index 7866c99353451..8c63d097fe56e 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PromiseReturnType } from '../../../../../observability/typings/common'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { BUCKET_TARGET_COUNT } from '../../transactions/constants'; import { getBuckets } from './get_buckets'; @@ -13,10 +12,6 @@ function getBucketSize({ start, end }: SetupTimeRange) { return Math.floor((end - start) / BUCKET_TARGET_COUNT); } -export type ErrorDistributionAPIResponse = PromiseReturnType< - typeof getErrorDistribution ->; - export async function getErrorDistribution({ serviceName, groupId, diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts index 37be72beedeb1..965cc28952b7a 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PromiseReturnType } from '../../../../observability/typings/common'; import { ERROR_GROUP_ID, SERVICE_NAME, @@ -15,8 +14,6 @@ import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getTransaction } from '../transactions/get_transaction'; -export type ErrorGroupAPIResponse = PromiseReturnType; - // TODO: rename from "getErrorGroup" to "getErrorGroupSample" (since a single error is returned, not an errorGroup) export async function getErrorGroup({ serviceName, diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index 97c03924538c8..3a3cf02297701 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -5,7 +5,6 @@ */ import { SortOptions } from '../../../../../typings/elasticsearch/aggregations'; -import { PromiseReturnType } from '../../../../observability/typings/common'; import { ERROR_CULPRIT, ERROR_EXC_HANDLED, @@ -19,10 +18,6 @@ import { mergeProjection } from '../../projections/util/merge_projection'; import { getErrorName } from '../helpers/get_error_name'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -export type ErrorGroupListAPIResponse = PromiseReturnType< - typeof getErrorGroups ->; - export async function getErrorGroups({ serviceName, sortField, diff --git a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts index 5b78d97d5b681..2a891bc6f8990 100644 --- a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts @@ -8,11 +8,15 @@ import moment from 'moment'; // @ts-expect-error import { calculateAuto } from './calculate_auto'; -export function getBucketSize( - start: number, - end: number, - numBuckets: number = 100 -) { +export function getBucketSize({ + start, + end, + numBuckets = 100, +}: { + start: number; + end: number; + numBuckets?: number; +}) { const duration = moment.duration(end - start, 'ms'); const bucketSize = Math.max( calculateAuto.near(numBuckets, duration).asSeconds(), diff --git a/x-pack/plugins/apm/server/lib/helpers/metrics.ts b/x-pack/plugins/apm/server/lib/helpers/metrics.ts index ea018868f9517..7ea8dc35b41d0 100644 --- a/x-pack/plugins/apm/server/lib/helpers/metrics.ts +++ b/x-pack/plugins/apm/server/lib/helpers/metrics.ts @@ -11,7 +11,7 @@ export function getMetricsDateHistogramParams( end: number, metricsInterval: number ) { - const { bucketSize } = getBucketSize(start, end); + const { bucketSize } = getBucketSize({ start, end }); return { field: '@timestamp', diff --git a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts index 03a44e77ba2d3..536be56e152a3 100644 --- a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts @@ -20,6 +20,8 @@ export function getOutcomeAggregation({ return { terms: { field: EVENT_OUTCOME }, aggs: { + // simply using the doc count to get the number of requests is not possible for transaction metrics (histograms) + // to work around this we get the number of transactions by counting the number of latency values count: { value_count: { field: getTransactionDurationFieldForAggregatedTransactions( 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 2ed11480a7585..10aa56e79f06b 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 @@ -40,7 +40,7 @@ export async function fetchAndTransformGcMetrics({ }) { const { start, end, apmEventClient, config } = setup; - const { bucketSize } = getBucketSize(start, end); + const { bucketSize } = getBucketSize({ start, end }); const projection = getMetricsProjection({ setup, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts index 99d978116180b..0ca085105c30c 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts @@ -43,7 +43,7 @@ export async function getServiceErrorGroups({ }) { const { apmEventClient, start, end, esFilter } = setup; - const { intervalString } = getBucketSize(start, end, numBuckets); + const { intervalString } = getBucketSize({ start, end, numBuckets }); const response = await apmEventClient.search({ apm: { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_timeseries_data_for_transaction_groups.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_timeseries_data_for_transaction_groups.ts new file mode 100644 index 0000000000000..73b91429f5101 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_timeseries_data_for_transaction_groups.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 { PromiseReturnType } from '../../../../../observability/typings/common'; +import { EventOutcome } from '../../../../common/event_outcome'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { + EVENT_OUTCOME, + SERVICE_NAME, + TRANSACTION_NAME, + TRANSACTION_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; + +import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { + getProcessorEventForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, +} from '../../helpers/aggregated_transactions'; +import { APMEventClient } from '../../helpers/create_es_client/create_apm_event_client'; +import { getBucketSize } from '../../helpers/get_bucket_size'; + +export type TransactionGroupTimeseriesData = PromiseReturnType< + typeof getTimeseriesDataForTransactionGroups +>; + +export async function getTimeseriesDataForTransactionGroups({ + apmEventClient, + start, + end, + serviceName, + transactionNames, + esFilter, + searchAggregatedTransactions, + size, + numBuckets, +}: { + apmEventClient: APMEventClient; + start: number; + end: number; + serviceName: string; + transactionNames: string[]; + esFilter: ESFilter[]; + searchAggregatedTransactions: boolean; + size: number; + numBuckets: number; +}) { + const { intervalString } = getBucketSize({ start, end, numBuckets }); + + const timeseriesResponse = await apmEventClient.search({ + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { terms: { [TRANSACTION_NAME]: transactionNames } }, + { term: { [SERVICE_NAME]: serviceName } }, + { range: rangeFilter(start, end) }, + ...esFilter, + ], + }, + }, + aggs: { + transaction_groups: { + terms: { + field: TRANSACTION_NAME, + size, + }, + aggs: { + transaction_types: { + terms: { + field: TRANSACTION_TYPE, + }, + }, + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, + }, + }, + aggs: { + avg_latency: { + avg: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + transaction_count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + [EVENT_OUTCOME]: { + filter: { + term: { + [EVENT_OUTCOME]: EventOutcome.failure, + }, + }, + aggs: { + transaction_count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }); + + return timeseriesResponse.aggregations?.transaction_groups.buckets ?? []; +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_transaction_groups_for_page.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_transaction_groups_for_page.ts new file mode 100644 index 0000000000000..5d3d7014ba8f8 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_transaction_groups_for_page.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { orderBy } from 'lodash'; +import { ValuesType } from 'utility-types'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; +import { EventOutcome } from '../../../../common/event_outcome'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { + EVENT_OUTCOME, + SERVICE_NAME, + TRANSACTION_NAME, +} from '../../../../common/elasticsearch_fieldnames'; +import { + getProcessorEventForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, +} from '../../helpers/aggregated_transactions'; +import { APMEventClient } from '../../helpers/create_es_client/create_apm_event_client'; + +export type ServiceOverviewTransactionGroupSortField = + | 'latency' + | 'throughput' + | 'errorRate' + | 'impact'; + +export type TransactionGroupWithoutTimeseriesData = ValuesType< + PromiseReturnType['transactionGroups'] +>; + +export async function getTransactionGroupsForPage({ + apmEventClient, + searchAggregatedTransactions, + serviceName, + start, + end, + esFilter, + sortField, + sortDirection, + pageIndex, + size, +}: { + apmEventClient: APMEventClient; + searchAggregatedTransactions: boolean; + serviceName: string; + start: number; + end: number; + esFilter: ESFilter[]; + sortField: ServiceOverviewTransactionGroupSortField; + sortDirection: 'asc' | 'desc'; + pageIndex: number; + size: number; +}) { + const response = await apmEventClient.search({ + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { range: rangeFilter(start, end) }, + ...esFilter, + ], + }, + }, + aggs: { + transaction_groups: { + terms: { + field: TRANSACTION_NAME, + size: 500, + order: { + _count: 'desc', + }, + }, + aggs: { + avg_latency: { + avg: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + transaction_count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + [EVENT_OUTCOME]: { + filter: { + term: { + [EVENT_OUTCOME]: EventOutcome.failure, + }, + }, + aggs: { + transaction_count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + }, + }, + }, + }, + }, + }, + }); + + const transactionGroups = + response.aggregations?.transaction_groups.buckets.map((bucket) => { + const errorRate = + bucket.transaction_count.value > 0 + ? (bucket[EVENT_OUTCOME].transaction_count.value ?? 0) / + bucket.transaction_count.value + : null; + + return { + name: bucket.key as string, + latency: bucket.avg_latency.value, + throughput: bucket.transaction_count.value, + errorRate, + }; + }) ?? []; + + const totalDurationValues = transactionGroups.map( + (group) => (group.latency ?? 0) * group.throughput + ); + + const minTotalDuration = Math.min(...totalDurationValues); + const maxTotalDuration = Math.max(...totalDurationValues); + + const transactionGroupsWithImpact = transactionGroups.map((group) => ({ + ...group, + impact: + (((group.latency ?? 0) * group.throughput - minTotalDuration) / + (maxTotalDuration - minTotalDuration)) * + 100, + })); + + // Sort transaction groups first, and only get timeseries for data in view. + // This is to limit the possibility of creating too many buckets. + + const sortedAndSlicedTransactionGroups = orderBy( + transactionGroupsWithImpact, + sortField, + [sortDirection] + ).slice(pageIndex * size, pageIndex * size + size); + + return { + transactionGroups: sortedAndSlicedTransactionGroups, + totalTransactionGroups: transactionGroups.length, + isAggregationAccurate: + (response.aggregations?.transaction_groups.sum_other_doc_count ?? 0) === + 0, + }; +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/index.ts new file mode 100644 index 0000000000000..88fd189712e07 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/index.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { getTimeseriesDataForTransactionGroups } from './get_timeseries_data_for_transaction_groups'; +import { + getTransactionGroupsForPage, + ServiceOverviewTransactionGroupSortField, +} from './get_transaction_groups_for_page'; +import { mergeTransactionGroupData } from './merge_transaction_group_data'; + +export async function getServiceTransactionGroups({ + serviceName, + setup, + size, + numBuckets, + pageIndex, + sortDirection, + sortField, + searchAggregatedTransactions, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + size: number; + pageIndex: number; + numBuckets: number; + sortDirection: 'asc' | 'desc'; + sortField: ServiceOverviewTransactionGroupSortField; + searchAggregatedTransactions: boolean; +}) { + const { apmEventClient, start, end, esFilter } = setup; + + const { + transactionGroups, + totalTransactionGroups, + isAggregationAccurate, + } = await getTransactionGroupsForPage({ + apmEventClient, + start, + end, + serviceName, + esFilter, + pageIndex, + sortField, + sortDirection, + size, + searchAggregatedTransactions, + }); + + const transactionNames = transactionGroups.map((group) => group.name); + + const timeseriesData = await getTimeseriesDataForTransactionGroups({ + apmEventClient, + start, + end, + esFilter, + numBuckets, + searchAggregatedTransactions, + serviceName, + size, + transactionNames, + }); + + return { + transactionGroups: mergeTransactionGroupData({ + transactionGroups, + timeseriesData, + start, + end, + }), + totalTransactionGroups, + isAggregationAccurate, + }; +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts new file mode 100644 index 0000000000000..f9266baddaf27 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; + +import { + TRANSACTION_PAGE_LOAD, + TRANSACTION_REQUEST, +} from '../../../../common/transaction_types'; + +import { TransactionGroupTimeseriesData } from './get_timeseries_data_for_transaction_groups'; + +import { TransactionGroupWithoutTimeseriesData } from './get_transaction_groups_for_page'; + +export function mergeTransactionGroupData({ + start, + end, + transactionGroups, + timeseriesData, +}: { + start: number; + end: number; + transactionGroups: TransactionGroupWithoutTimeseriesData[]; + timeseriesData: TransactionGroupTimeseriesData; +}) { + const deltaAsMinutes = (end - start) / 1000 / 60; + + return transactionGroups.map((transactionGroup) => { + const groupBucket = timeseriesData.find( + ({ key }) => key === transactionGroup.name + ); + + const transactionTypes = + groupBucket?.transaction_types.buckets.map( + (bucket) => bucket.key as string + ) ?? []; + + const transactionType = + transactionTypes.find( + (type) => type === TRANSACTION_PAGE_LOAD || type === TRANSACTION_REQUEST + ) ?? transactionTypes[0]; + + const timeseriesBuckets = groupBucket?.timeseries.buckets ?? []; + + return timeseriesBuckets.reduce( + (prev, point) => { + return { + ...prev, + latency: { + ...prev.latency, + timeseries: prev.latency.timeseries.concat({ + x: point.key, + y: point.avg_latency.value, + }), + }, + throughput: { + ...prev.throughput, + timeseries: prev.throughput.timeseries.concat({ + x: point.key, + y: point.transaction_count.value / deltaAsMinutes, + }), + }, + errorRate: { + ...prev.errorRate, + timeseries: prev.errorRate.timeseries.concat({ + x: point.key, + y: + point.transaction_count.value > 0 + ? (point[EVENT_OUTCOME].transaction_count.value ?? 0) / + point.transaction_count.value + : null, + }), + }, + }; + }, + { + name: transactionGroup.name, + transactionType, + latency: { + value: transactionGroup.latency, + timeseries: [] as Array<{ x: number; y: number | null }>, + }, + throughput: { + value: transactionGroup.throughput, + timeseries: [] as Array<{ x: number; y: number }>, + }, + errorRate: { + value: transactionGroup.errorRate, + timeseries: [] as Array<{ x: number; y: number | null }>, + }, + impact: transactionGroup.impact, + } + ); + }); +} diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 89915e798b7cd..11f3e44fce87c 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import { Logger } from '@kbn/logging'; -import { PromiseReturnType } from '../../../../../observability/typings/common'; import { joinByKey } from '../../../../common/utils/join_by_key'; import { getServicesProjection } from '../../../projections/services'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; @@ -17,7 +16,6 @@ import { getTransactionRates, } from './get_services_items_stats'; -export type ServiceListAPIResponse = PromiseReturnType; export type ServicesItemsSetup = Setup & SetupTimeRange; export type ServicesItemsProjection = ReturnType; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts index fac80cf22c310..c8ebaa13d9df9 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts @@ -37,7 +37,8 @@ import { function getDateHistogramOpts(start: number, end: number) { return { field: '@timestamp', - fixed_interval: getBucketSize(start, end, 20).intervalString, + fixed_interval: getBucketSize({ start, end, numBuckets: 20 }) + .intervalString, min_doc_count: 0, extended_bounds: { min: start, max: end }, }; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/index.ts b/x-pack/plugins/apm/server/lib/services/get_services/index.ts index 9d450804e421d..6a77392550bfe 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/index.ts @@ -6,14 +6,11 @@ import { Logger } from '@kbn/logging'; import { isEmpty } from 'lodash'; -import { PromiseReturnType } from '../../../../../observability/typings/common'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getLegacyDataStatus } from './get_legacy_data_status'; import { getServicesItems } from './get_services_items'; import { hasHistoricalAgentData } from './has_historical_agent_data'; -export type ServiceListAPIResponse = PromiseReturnType; - export async function getServices({ setup, searchAggregatedTransactions, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts index d76f9600b3d93..d68863e250684 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts @@ -4,14 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PromiseReturnType } from '../../../../../observability/typings/common'; import { Setup } from '../../helpers/setup_request'; import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; import { convertConfigSettingsToString } from './convert_settings_to_string'; -export type AgentConfigurationListAPIResponse = PromiseReturnType< - typeof listConfigurations ->; export async function listConfigurations({ setup }: { setup: Setup }) { const { internalClient, indices } = setup; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_ranges.ts b/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_ranges.ts deleted file mode 100644 index 3cf0271baa1c6..0000000000000 --- a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_ranges.ts +++ /dev/null @@ -1,90 +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 { rangeFilter } from '../../../../common/utils/range_filter'; -import { - SERVICE_NAME, - TRANSACTION_NAME, - TRANSACTION_TYPE, -} from '../../../../common/elasticsearch_fieldnames'; -import { ProcessorEvent } from '../../../../common/processor_event'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { - getSignificantTermsAgg, - formatAggregationResponse, -} from './get_significant_terms_agg'; -import { SignificantTermsScoring } from './scoring_rt'; - -export async function getCorrelationsForRanges({ - serviceName, - transactionType, - transactionName, - scoring, - gapBetweenRanges, - fieldNames, - setup, -}: { - serviceName: string | undefined; - transactionType: string | undefined; - transactionName: string | undefined; - scoring: SignificantTermsScoring; - gapBetweenRanges: number; - fieldNames: string[]; - setup: Setup & SetupTimeRange; -}) { - const { start, end, esFilter, apmEventClient } = setup; - - const baseFilters = [...esFilter]; - - if (serviceName) { - baseFilters.push({ term: { [SERVICE_NAME]: serviceName } }); - } - - if (transactionType) { - baseFilters.push({ term: { [TRANSACTION_TYPE]: transactionType } }); - } - - if (transactionName) { - baseFilters.push({ term: { [TRANSACTION_NAME]: transactionName } }); - } - - const diff = end - start + gapBetweenRanges; - const baseRangeStart = start - diff; - const baseRangeEnd = end - diff; - const backgroundFilters = [ - ...baseFilters, - { range: rangeFilter(baseRangeStart, baseRangeEnd) }, - ]; - - const params = { - apm: { events: [ProcessorEvent.transaction] }, - body: { - size: 0, - query: { - bool: { filter: [...baseFilters, { range: rangeFilter(start, end) }] }, - }, - aggs: getSignificantTermsAgg({ - fieldNames, - backgroundFilters, - backgroundIsSuperset: false, - scoring, - }), - }, - }; - - const response = await apmEventClient.search(params); - - return { - message: `Showing significant fields between the ranges`, - firstRange: `${new Date(baseRangeStart).toISOString()} - ${new Date( - baseRangeEnd - ).toISOString()}`, - lastRange: `${new Date(start).toISOString()} - ${new Date( - end - ).toISOString()}`, - response: formatAggregationResponse(response.aggregations), - }; -} diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_significant_terms_agg.ts b/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_significant_terms_agg.ts deleted file mode 100644 index c5ab8d8f1d111..0000000000000 --- a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_significant_terms_agg.ts +++ /dev/null @@ -1,68 +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 { ESFilter } from '../../../../../../typings/elasticsearch'; -import { SignificantTermsScoring } from './scoring_rt'; - -export function getSignificantTermsAgg({ - fieldNames, - backgroundFilters, - backgroundIsSuperset = true, - scoring = 'percentage', -}: { - fieldNames: string[]; - backgroundFilters: ESFilter[]; - backgroundIsSuperset?: boolean; - scoring: SignificantTermsScoring; -}) { - return fieldNames.reduce((acc, fieldName) => { - return { - ...acc, - [fieldName]: { - significant_terms: { - size: 10, - field: fieldName, - background_filter: { bool: { filter: backgroundFilters } }, - - // indicate whether background is a superset of the foreground - mutual_information: { background_is_superset: backgroundIsSuperset }, - - // different scorings https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-significantterms-aggregation.html#significantterms-aggregation-parameters - [scoring]: {}, - min_doc_count: 5, - shard_min_doc_count: 5, - }, - }, - [`cardinality-${fieldName}`]: { - cardinality: { field: fieldName }, - }, - }; - }, {} as Record); -} - -export function formatAggregationResponse(aggs?: Record) { - if (!aggs) { - return; - } - - return Object.entries(aggs).reduce((acc, [key, value]) => { - if (key.startsWith('cardinality-')) { - if (value.value > 0) { - const fieldName = key.slice(12); - acc[fieldName] = { - ...acc[fieldName], - cardinality: value.value, - }; - } - } else if (value.buckets.length > 0) { - acc[key] = { - ...acc[key], - value, - }; - } - return acc; - }, {} as Record); -} 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 89fff260a7d23..e57ea3aecb09a 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -204,7 +204,7 @@ export async function transactionGroupsFetcher( }; } -export interface TransactionGroup { +interface TransactionGroup { key: string | Record<'service.name' | 'transaction.name', string>; serviceName: string; transactionName: string; 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 e9d273dad6262..dfd11203b87f1 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 @@ -78,7 +78,7 @@ export async function getErrorRate({ timeseries: { date_histogram: { field: '@timestamp', - fixed_interval: getBucketSize(start, end).intervalString, + fixed_interval: getBucketSize({ start, end }).intervalString, min_doc_count: 0, extended_bounds: { min: start, max: end }, }, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts index f11623eaa2dae..e72219a3cbd72 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts @@ -77,7 +77,7 @@ export async function getAnomalySeries({ return; } - const { intervalString, bucketSize } = getBucketSize(start, end); + const { intervalString, bucketSize } = getBucketSize({ start, end }); const esResponse = await anomalySeriesFetcher({ serviceName, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index a2da3977b81c7..cffec688806b5 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -36,7 +36,7 @@ export function timeseriesFetcher({ searchAggregatedTransactions: boolean; }) { const { start, end, apmEventClient } = setup; - const { intervalString } = getBucketSize(start, end); + const { intervalString } = getBucketSize({ start, end }); const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts index c0421005dd06e..6c923290848a1 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts @@ -17,7 +17,7 @@ export async function getApmTimeseriesData(options: { searchAggregatedTransactions: boolean; }) { const { start, end } = options.setup; - const { bucketSize } = getBucketSize(start, end); + const { bucketSize } = getBucketSize({ start, end }); const durationAsMinutes = (end - start) / 1000 / 60; const timeseriesResponse = await timeseriesFetcher(options); diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts index 010acd09239a3..98df68e50220d 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts @@ -3,8 +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 { ValuesType } from 'utility-types'; -import { PromiseReturnType } from '../../../../../../observability/typings/common'; + import { SERVICE_NAME, TRACE_ID, @@ -196,7 +195,3 @@ export async function getBuckets({ buckets, }; } - -export type DistributionBucket = ValuesType< - PromiseReturnType['buckets'] ->; diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts index deafc37ee42e2..af6e05a2ca336 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PromiseReturnType } from '../../../../../observability/typings/common'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getBuckets } from './get_buckets'; import { getDistributionMax } from './get_distribution_max'; @@ -18,9 +17,6 @@ function getBucketSize(max: number) { ); } -export type TransactionDistributionAPIResponse = PromiseReturnType< - typeof getTransactionDistribution ->; export async function getTransactionDistribution({ serviceName, transactionName, diff --git a/x-pack/plugins/apm/server/routes/correlations.ts b/x-pack/plugins/apm/server/routes/correlations.ts index 19eb639a72bb9..6d1aead9292e3 100644 --- a/x-pack/plugins/apm/server/routes/correlations.ts +++ b/x-pack/plugins/apm/server/routes/correlations.ts @@ -6,21 +6,19 @@ import * as t from 'io-ts'; import { rangeRt } from './default_api_types'; -import { getCorrelationsForSlowTransactions } from '../lib/transaction_groups/correlations/get_correlations_for_slow_transactions'; -import { getCorrelationsForRanges } from '../lib/transaction_groups/correlations/get_correlations_for_ranges'; -import { scoringRt } from '../lib/transaction_groups/correlations/scoring_rt'; +import { getCorrelationsForSlowTransactions } from '../lib/correlations/get_correlations_for_slow_transactions'; +import { getCorrelationsForFailedTransactions } from '../lib/correlations/get_correlations_for_failed_transactions'; import { createRoute } from './create_route'; import { setupRequest } from '../lib/helpers/setup_request'; export const correlationsForSlowTransactionsRoute = createRoute({ - endpoint: 'GET /api/apm/correlations/slow_durations', + endpoint: 'GET /api/apm/correlations/slow_transactions', params: t.type({ query: t.intersection([ t.partial({ serviceName: t.string, transactionName: t.string, transactionType: t.string, - scoring: scoringRt, }), t.type({ durationPercentile: t.string, @@ -30,6 +28,7 @@ export const correlationsForSlowTransactionsRoute = createRoute({ rangeRt, ]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { @@ -38,7 +37,6 @@ export const correlationsForSlowTransactionsRoute = createRoute({ transactionName, durationPercentile, fieldNames, - scoring = 'percentage', } = context.params.query; return getCorrelationsForSlowTransactions({ @@ -47,22 +45,19 @@ export const correlationsForSlowTransactionsRoute = createRoute({ transactionName, durationPercentile: parseInt(durationPercentile, 10), fieldNames: fieldNames.split(','), - scoring, setup, }); }, }); -export const correlationsForRangesRoute = createRoute({ - endpoint: 'GET /api/apm/correlations/ranges', +export const correlationsForFailedTransactionsRoute = createRoute({ + endpoint: 'GET /api/apm/correlations/failed_transactions', params: t.type({ query: t.intersection([ t.partial({ serviceName: t.string, transactionName: t.string, transactionType: t.string, - scoring: scoringRt, - gap: t.string, }), t.type({ fieldNames: t.string, @@ -71,29 +66,21 @@ export const correlationsForRangesRoute = createRoute({ rangeRt, ]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - const { serviceName, transactionType, transactionName, - scoring = 'percentage', - gap, + fieldNames, } = context.params.query; - const gapBetweenRanges = parseInt(gap || '0', 10) * 3600 * 1000; - if (gapBetweenRanges < 0) { - throw new Error('gap must be 0 or positive'); - } - - return getCorrelationsForRanges({ + return getCorrelationsForFailedTransactions({ serviceName, transactionType, transactionName, - scoring, - gapBetweenRanges, fieldNames: fieldNames.split(','), setup, }); diff --git a/x-pack/plugins/apm/server/routes/create_api/index.test.ts b/x-pack/plugins/apm/server/routes/create_api/index.test.ts index 32a5e5c5a5c8a..206c57d2cd6d5 100644 --- a/x-pack/plugins/apm/server/routes/create_api/index.test.ts +++ b/x-pack/plugins/apm/server/routes/create_api/index.test.ts @@ -53,6 +53,7 @@ describe('createApi', () => { createApi() .add(() => ({ endpoint: 'GET /foo', + options: { tags: ['access:apm'] }, handler: async () => null, })) .add(() => ({ @@ -60,6 +61,7 @@ describe('createApi', () => { params: t.type({ body: t.string, }), + options: { tags: ['access:apm'] }, handler: async () => null, })) .add(() => ({ @@ -125,6 +127,7 @@ describe('createApi', () => { .add(() => ({ endpoint: 'GET /foo', params, + options: { tags: ['access:apm'] }, handler: handlerMock, })) .init(mock, context); diff --git a/x-pack/plugins/apm/server/routes/create_api/index.ts b/x-pack/plugins/apm/server/routes/create_api/index.ts index 25a074ea100e5..ef445617e9295 100644 --- a/x-pack/plugins/apm/server/routes/create_api/index.ts +++ b/x-pack/plugins/apm/server/routes/create_api/index.ts @@ -50,12 +50,7 @@ export function createApi() { ? routeOrFactoryFn(core) : routeOrFactoryFn; - const { - params, - endpoint, - options = { tags: ['access:apm'] }, - handler, - } = route; + const { params, endpoint, options, handler } = route; const [method, path] = endpoint.split(' '); diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index a272b448deaf1..019482dd44485 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -22,6 +22,7 @@ import { serviceAnnotationsRoute, serviceAnnotationsCreateRoute, serviceErrorGroupsRoute, + serviceTransactionGroupsRoute, } from './services'; import { agentConfigurationRoute, @@ -43,8 +44,8 @@ import { serviceNodesRoute } from './service_nodes'; import { tracesRoute, tracesByIdRoute } from './traces'; import { transactionByTraceIdRoute } from './transaction'; import { - correlationsForRangesRoute, correlationsForSlowTransactionsRoute, + correlationsForFailedTransactionsRoute, } from './correlations'; import { transactionGroupsBreakdownRoute, @@ -116,6 +117,7 @@ const createApmApi = () => { .add(serviceAnnotationsRoute) .add(serviceAnnotationsCreateRoute) .add(serviceErrorGroupsRoute) + .add(serviceTransactionGroupsRoute) // Agent configuration .add(getSingleAgentConfigurationRoute) @@ -129,7 +131,7 @@ const createApmApi = () => { // Correlations .add(correlationsForSlowTransactionsRoute) - .add(correlationsForRangesRoute) + .add(correlationsForFailedTransactionsRoute) // APM indices .add(apmIndexSettingsRoute) diff --git a/x-pack/plugins/apm/server/routes/errors.ts b/x-pack/plugins/apm/server/routes/errors.ts index 189a18698b56f..64864ec2258ba 100644 --- a/x-pack/plugins/apm/server/routes/errors.ts +++ b/x-pack/plugins/apm/server/routes/errors.ts @@ -27,6 +27,7 @@ export const errorsRoute = createRoute({ rangeRt, ]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { params } = context; @@ -51,6 +52,7 @@ export const errorGroupsRoute = createRoute({ }), query: t.intersection([uiFiltersRt, rangeRt]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName, groupId } = context.params.path; @@ -72,6 +74,7 @@ export const errorDistributionRoute = createRoute({ rangeRt, ]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { params } = context; diff --git a/x-pack/plugins/apm/server/routes/index_pattern.ts b/x-pack/plugins/apm/server/routes/index_pattern.ts index 5b9b211032bf5..391a38fd3e5c9 100644 --- a/x-pack/plugins/apm/server/routes/index_pattern.ts +++ b/x-pack/plugins/apm/server/routes/index_pattern.ts @@ -15,6 +15,7 @@ import { UIProcessorEvent } from '../../common/processor_event'; export const staticIndexPatternRoute = createRoute((core) => ({ endpoint: 'POST /api/apm/index_pattern/static', + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const savedObjectsClient = await getInternalSavedObjectsClient(core); @@ -37,6 +38,7 @@ export const dynamicIndexPatternRoute = createRoute({ ]), }), }), + options: { tags: ['access:apm'] }, handler: async ({ context }) => { const indices = await getApmIndices({ config: context.config, @@ -59,6 +61,7 @@ export const dynamicIndexPatternRoute = createRoute({ export const apmIndexPatternTitleRoute = createRoute({ endpoint: 'GET /api/apm/index_pattern/title', + options: { tags: ['access:apm'] }, handler: async ({ context }) => { return getApmIndexPatternTitle(context); }, diff --git a/x-pack/plugins/apm/server/routes/metrics.ts b/x-pack/plugins/apm/server/routes/metrics.ts index 82697a78b424c..980444595deb4 100644 --- a/x-pack/plugins/apm/server/routes/metrics.ts +++ b/x-pack/plugins/apm/server/routes/metrics.ts @@ -27,6 +27,7 @@ export const metricsChartsRoute = createRoute({ rangeRt, ]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { params } = context; diff --git a/x-pack/plugins/apm/server/routes/observability_overview.ts b/x-pack/plugins/apm/server/routes/observability_overview.ts index e6d6bc8157a3e..0c12e171c9904 100644 --- a/x-pack/plugins/apm/server/routes/observability_overview.ts +++ b/x-pack/plugins/apm/server/routes/observability_overview.ts @@ -14,6 +14,7 @@ import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_trans export const observabilityOverviewHasDataRoute = createRoute({ endpoint: 'GET /api/apm/observability_overview/has_data', + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); return await hasData({ setup }); @@ -25,6 +26,7 @@ export const observabilityOverviewRoute = createRoute({ params: t.type({ query: t.intersection([rangeRt, t.type({ bucketSize: t.string })]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { bucketSize } = context.params.query; diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index ead774c0c7915..e99d132de8d22 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -36,6 +36,7 @@ export const rumClientMetricsRoute = createRoute({ params: t.type({ query: uxQueryRt, }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -56,6 +57,7 @@ export const rumPageLoadDistributionRoute = createRoute({ params: t.type({ query: t.intersection([uxQueryRt, percentileRangeRt]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -81,6 +83,7 @@ export const rumPageLoadDistBreakdownRoute = createRoute({ t.type({ breakdown: t.string }), ]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -103,6 +106,7 @@ export const rumPageViewsTrendRoute = createRoute({ params: t.type({ query: t.intersection([uxQueryRt, t.partial({ breakdowns: t.string })]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -123,6 +127,7 @@ export const rumServicesRoute = createRoute({ params: t.type({ query: t.intersection([uiFiltersRt, rangeRt]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -135,6 +140,7 @@ export const rumVisitorsBreakdownRoute = createRoute({ params: t.type({ query: uxQueryRt, }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -154,6 +160,7 @@ export const rumWebCoreVitals = createRoute({ params: t.type({ query: uxQueryRt, }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -174,6 +181,7 @@ export const rumLongTaskMetrics = createRoute({ params: t.type({ query: uxQueryRt, }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -194,6 +202,7 @@ export const rumUrlSearch = createRoute({ params: t.type({ query: uxQueryRt, }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -215,6 +224,7 @@ export const rumJSErrors = createRoute({ t.partial({ urlQuery: t.string }), ]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -236,6 +246,7 @@ export const rumHasDataRoute = createRoute({ params: t.type({ query: t.intersection([uiFiltersRt, rangeRt]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); return await hasRumData({ setup }); diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 2ad9d97130d1a..452b00a7ae320 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -29,6 +29,7 @@ export const serviceMapRoute = createRoute({ rangeRt, ]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { if (!context.config['xpack.apm.serviceMapEnabled']) { throw Boom.notFound(); @@ -69,6 +70,7 @@ export const serviceMapServiceNodeRoute = createRoute({ }), query: t.intersection([rangeRt, uiFiltersRt]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { if (!context.config['xpack.apm.serviceMapEnabled']) { throw Boom.notFound(); diff --git a/x-pack/plugins/apm/server/routes/service_nodes.ts b/x-pack/plugins/apm/server/routes/service_nodes.ts index df01a034b06cc..fd439ebb13831 100644 --- a/x-pack/plugins/apm/server/routes/service_nodes.ts +++ b/x-pack/plugins/apm/server/routes/service_nodes.ts @@ -17,6 +17,7 @@ export const serviceNodesRoute = createRoute({ }), query: t.intersection([rangeRt, uiFiltersRt]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { params } = context; diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 10af35df4b0e9..5e02fad2155ad 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -19,12 +19,14 @@ import { dateAsStringRt } from '../../common/runtime_types/date_as_string_rt'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { getServiceErrorGroups } from '../lib/services/get_service_error_groups'; import { toNumberRt } from '../../common/runtime_types/to_number_rt'; +import { getServiceTransactionGroups } from '../lib/services/get_service_transaction_groups'; export const servicesRoute = createRoute({ endpoint: 'GET /api/apm/services', params: t.type({ query: t.intersection([uiFiltersRt, rangeRt]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -50,6 +52,7 @@ export const serviceAgentNameRoute = createRoute({ }), query: rangeRt, }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; @@ -73,6 +76,7 @@ export const serviceTransactionTypesRoute = createRoute({ }), query: rangeRt, }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; @@ -96,6 +100,7 @@ export const serviceNodeMetadataRoute = createRoute({ }), query: t.intersection([uiFiltersRt, rangeRt]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName, serviceNodeName } = context.params.path; @@ -116,6 +121,7 @@ export const serviceAnnotationsRoute = createRoute({ }), ]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; @@ -220,6 +226,7 @@ export const serviceErrorGroupsRoute = createRoute({ }), ]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -227,7 +234,6 @@ export const serviceErrorGroupsRoute = createRoute({ path: { serviceName }, query: { size, numBuckets, pageIndex, sortDirection, sortField }, } = context.params; - return getServiceErrorGroups({ serviceName, setup, @@ -239,3 +245,52 @@ export const serviceErrorGroupsRoute = createRoute({ }); }, }); + +export const serviceTransactionGroupsRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/overview_transaction_groups', + params: t.type({ + path: t.type({ serviceName: t.string }), + query: t.intersection([ + rangeRt, + uiFiltersRt, + t.type({ + size: toNumberRt, + numBuckets: toNumberRt, + pageIndex: toNumberRt, + sortDirection: t.union([t.literal('asc'), t.literal('desc')]), + sortField: t.union([ + t.literal('latency'), + t.literal('throughput'), + t.literal('errorRate'), + t.literal('impact'), + ]), + }), + ]), + }), + options: { + tags: ['access:apm'], + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + + const { + path: { serviceName }, + query: { size, numBuckets, pageIndex, sortDirection, sortField }, + } = context.params; + + return getServiceTransactionGroups({ + setup, + serviceName, + pageIndex, + searchAggregatedTransactions, + size, + sortDirection, + sortField, + numBuckets, + }); + }, +}); diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts index 942fef5b559ba..07e2dc3c2f71b 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts @@ -27,6 +27,7 @@ import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_tr // get list of configurations export const agentConfigurationRoute = createRoute({ endpoint: 'GET /api/apm/settings/agent-configuration', + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); return await listConfigurations({ setup }); @@ -39,6 +40,7 @@ export const getSingleAgentConfigurationRoute = createRoute({ params: t.partial({ query: serviceRt, }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { name, environment } = context.params.query; @@ -148,6 +150,7 @@ export const agentConfigurationSearchRoute = createRoute({ params: t.type({ body: searchParamsRt, }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const { service, @@ -194,6 +197,7 @@ export const agentConfigurationSearchRoute = createRoute({ // get list of services export const listAgentConfigurationServicesRoute = createRoute({ endpoint: 'GET /api/apm/settings/agent-configuration/services', + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const searchAggregatedTransactions = await getSearchAggregatedTransactions( @@ -212,6 +216,7 @@ export const listAgentConfigurationEnvironmentsRoute = createRoute({ params: t.partial({ query: t.partial({ serviceName: t.string }), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.query; @@ -233,6 +238,7 @@ export const agentConfigurationAgentNameRoute = createRoute({ params: t.type({ query: t.type({ serviceName: t.string }), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.query; diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index 633c284e91a4d..e7405ad16a63e 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -71,6 +71,7 @@ export const createAnomalyDetectionJobsRoute = createRoute({ // get all available environments to create anomaly detection jobs for export const anomalyDetectionEnvironmentsRoute = createRoute({ endpoint: 'GET /api/apm/settings/anomaly-detection/environments', + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts index 760ee4225ede2..79099d0232f05 100644 --- a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts +++ b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts @@ -15,6 +15,7 @@ import { saveApmIndices } from '../../lib/settings/apm_indices/save_apm_indices' // get list of apm indices and values export const apmIndexSettingsRoute = createRoute({ endpoint: 'GET /api/apm/settings/apm-index-settings', + options: { tags: ['access:apm'] }, handler: async ({ context }) => { return await getApmIndexSettings({ context }); }, @@ -23,6 +24,7 @@ export const apmIndexSettingsRoute = createRoute({ // get apm indices configuration object export const apmIndicesRoute = createRoute({ endpoint: 'GET /api/apm/settings/apm-indices', + options: { tags: ['access:apm'] }, handler: async ({ context }) => { return await getApmIndices({ savedObjectsClient: context.core.savedObjects.client, diff --git a/x-pack/plugins/apm/server/routes/settings/custom_link.ts b/x-pack/plugins/apm/server/routes/settings/custom_link.ts index 6f06ed4e970df..fdf2fe3521d7e 100644 --- a/x-pack/plugins/apm/server/routes/settings/custom_link.ts +++ b/x-pack/plugins/apm/server/routes/settings/custom_link.ts @@ -28,6 +28,7 @@ function isActiveGoldLicense(license: ILicense) { export const customLinkTransactionRoute = createRoute({ endpoint: 'GET /api/apm/settings/custom_links/transaction', + options: { tags: ['access:apm'] }, params: t.partial({ query: filterOptionsRt, }), @@ -42,6 +43,7 @@ export const customLinkTransactionRoute = createRoute({ export const listCustomLinksRoute = createRoute({ endpoint: 'GET /api/apm/settings/custom_links', + options: { tags: ['access:apm'] }, params: t.partial({ query: filterOptionsRt, }), @@ -62,9 +64,7 @@ export const createCustomLinkRoute = createRoute({ params: t.type({ body: payloadRt, }), - options: { - tags: ['access:apm', 'access:apm_write'], - }, + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { if (!isActiveGoldLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE); diff --git a/x-pack/plugins/apm/server/routes/traces.ts b/x-pack/plugins/apm/server/routes/traces.ts index 9bbf6f1cc9061..0c79d391e1fd7 100644 --- a/x-pack/plugins/apm/server/routes/traces.ts +++ b/x-pack/plugins/apm/server/routes/traces.ts @@ -17,6 +17,7 @@ export const tracesRoute = createRoute({ params: t.type({ query: t.intersection([rangeRt, uiFiltersRt]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const searchAggregatedTransactions = await getSearchAggregatedTransactions( @@ -37,6 +38,7 @@ export const tracesByIdRoute = createRoute({ }), query: rangeRt, }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); return getTrace(context.params.path.traceId, setup); diff --git a/x-pack/plugins/apm/server/routes/transaction.ts b/x-pack/plugins/apm/server/routes/transaction.ts index 04f6c2e1ce247..3294d2e9a8227 100644 --- a/x-pack/plugins/apm/server/routes/transaction.ts +++ b/x-pack/plugins/apm/server/routes/transaction.ts @@ -16,6 +16,7 @@ export const transactionByTraceIdRoute = createRoute({ traceId: t.string, }), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const { traceId } = context.params.path; const setup = await setupRequest(context, request); diff --git a/x-pack/plugins/apm/server/routes/transaction_groups.ts b/x-pack/plugins/apm/server/routes/transaction_groups.ts index 423506afebe77..58c1ce3451a29 100644 --- a/x-pack/plugins/apm/server/routes/transaction_groups.ts +++ b/x-pack/plugins/apm/server/routes/transaction_groups.ts @@ -31,6 +31,7 @@ export const transactionGroupsRoute = createRoute({ rangeRt, ]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; @@ -67,6 +68,7 @@ export const transactionGroupsChartsRoute = createRoute({ rangeRt, ]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const logger = context.logger; @@ -116,6 +118,7 @@ export const transactionGroupsDistributionRoute = createRoute({ rangeRt, ]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; @@ -159,6 +162,7 @@ export const transactionGroupsBreakdownRoute = createRoute({ rangeRt, ]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; @@ -182,6 +186,7 @@ export const transactionSampleForGroupRoute = createRoute({ t.type({ serviceName: t.string, transactionName: t.string }), ]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -212,6 +217,7 @@ export const transactionGroupsErrorRateRoute = createRoute({ }), ]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { params } = context; diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 5f1b344ead5cb..81b25e572a28d 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -59,7 +59,7 @@ export interface Route< TReturn > { endpoint: TEndpoint; - options?: RouteOptions; + options: RouteOptions; params?: TRouteParamsRT; handler: RouteHandler; } diff --git a/x-pack/plugins/apm/server/routes/ui_filters.ts b/x-pack/plugins/apm/server/routes/ui_filters.ts index 67e23ebbe2493..dae2962a76d10 100644 --- a/x-pack/plugins/apm/server/routes/ui_filters.ts +++ b/x-pack/plugins/apm/server/routes/ui_filters.ts @@ -40,6 +40,7 @@ export const uiFiltersEnvironmentsRoute = createRoute({ rangeRt, ]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.query; @@ -94,6 +95,7 @@ function createLocalFiltersRoute< params: t.type({ query: t.intersection([localUiBaseQueryRt, queryRt]), }), + options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { uiFilters } = setup; diff --git a/x-pack/plugins/encrypted_saved_objects/server/config.test.ts b/x-pack/plugins/encrypted_saved_objects/server/config.test.ts index cbe987830717f..57f332ff7bc23 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/config.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/config.test.ts @@ -138,7 +138,7 @@ describe('createConfig()', () => { expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(` Array [ Array [ - "Generating a random key for xpack.encryptedSavedObjects.encryptionKey. To be able to decrypt encrypted saved objects attributes after restart, please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml", + "Generating a random key for xpack.encryptedSavedObjects.encryptionKey. To decrypt encrypted saved objects attributes after restart, please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.", ], ] `); diff --git a/x-pack/plugins/encrypted_saved_objects/server/config.ts b/x-pack/plugins/encrypted_saved_objects/server/config.ts index f06c6fa1823ba..3f2858d7afea8 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/config.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/config.ts @@ -39,8 +39,8 @@ export function createConfig(config: TypeOf, logger: Logger if (encryptionKey === undefined) { logger.warn( 'Generating a random key for xpack.encryptedSavedObjects.encryptionKey. ' + - 'To be able to decrypt encrypted saved objects attributes after restart, ' + - 'please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml' + 'To decrypt encrypted saved objects attributes after restart, ' + + 'please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' ); encryptionKey = crypto.randomBytes(16).toString('hex'); diff --git a/x-pack/plugins/enterprise_search/kibana.json b/x-pack/plugins/enterprise_search/kibana.json index 063c7a6a1fa19..d60ab5c7d37f0 100644 --- a/x-pack/plugins/enterprise_search/kibana.json +++ b/x-pack/plugins/enterprise_search/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "requiredPlugins": ["features", "licensing"], "configPath": ["enterpriseSearch"], - "optionalPlugins": ["usageCollection", "security", "home"], + "optionalPlugins": ["usageCollection", "security", "home", "spaces"], "server": true, "ui": true, "requiredBundles": ["home"] diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/query_params/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/query_params/index.ts new file mode 100644 index 0000000000000..61eb1792911ee --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/query_params/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 { parseQueryParams } from './query_params'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/query_params/query_params.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/query_params/query_params.test.ts new file mode 100644 index 0000000000000..1e543b3fbfb00 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/query_params/query_params.test.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. + */ + +import { parseQueryParams } from './'; + +describe('parseQueryParams', () => { + it('parse query strings', () => { + expect(parseQueryParams('?foo=bar')).toEqual({ foo: 'bar' }); + expect(parseQueryParams('?foo[]=bar&foo[]=baz')).toEqual({ foo: ['bar', 'baz'] }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/scoring_rt.ts b/x-pack/plugins/enterprise_search/public/applications/shared/query_params/query_params.ts similarity index 51% rename from x-pack/plugins/apm/server/lib/transaction_groups/correlations/scoring_rt.ts rename to x-pack/plugins/enterprise_search/public/applications/shared/query_params/query_params.ts index cb94b6251eb07..f39760d27fbf3 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/scoring_rt.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/query_params/query_params.ts @@ -4,13 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as t from 'io-ts'; +import queryString from 'query-string'; -export const scoringRt = t.union([ - t.literal('jlh'), - t.literal('chi_square'), - t.literal('gnd'), - t.literal('percentage'), -]); - -export type SignificantTermsScoring = t.TypeOf; +export const parseQueryParams = (search: string) => + queryString.parse(search, { arrayFormat: 'bracket' }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/supports_acl.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/supports_acl.svg new file mode 100644 index 0000000000000..f1267ae57f0bd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/supports_acl.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 8f62984db1b5e..be95c6ffe6f38 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -124,3 +124,5 @@ export const getContentSourcePath = ( export const getGroupPath = (groupId: string) => generatePath(GROUP_PATH, { groupId }); export const getGroupSourcePrioritizationPath = (groupId: string) => `${GROUPS_PATH}/${groupId}/source_prioritization`; +export const getSourcesPath = (path: string, isOrganization: boolean) => + isOrganization ? `${ORG_PATH}${path}` : path; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index 1bd3cabb0227d..73e7f7ed701d8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -88,6 +88,12 @@ export interface ContentSource { name: string; } +export interface SourceContentItem { + id: string; + last_updated: string; + [key: string]: string; +} + export interface ContentSourceDetails extends ContentSource { status: string; statusMessage: string; @@ -105,11 +111,23 @@ interface DescriptionList { description: string; } +export interface DocumentSummaryItem { + count: number; + type: string; +} + +interface SourceActivity { + details: string[]; + event: string; + time: string; + status: string; +} + export interface ContentSourceFullData extends ContentSourceDetails { - activities: object[]; + activities: SourceActivity[]; details: DescriptionList[]; - summary: object[]; - groups: object[]; + summary: DocumentSummaryItem[]; + groups: Group[]; custom: boolean; accessToken: string; key: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx new file mode 100644 index 0000000000000..7b6d02c36c0cc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx @@ -0,0 +1,235 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { History } from 'history'; +import { useActions, useValues } from 'kea'; +import { useHistory } from 'react-router-dom'; + +import { AppLogic } from '../../../../app_logic'; +import { Loading } from '../../../../../../applications/shared/loading'; +import { ViewContentHeader } from '../../../../components/shared/view_content_header'; +import { CUSTOM_SERVICE_TYPE } from '../../../../constants'; +import { staticSourceData } from '../../source_data'; +import { SourceLogic } from '../../source_logic'; +import { SourceDataItem, FeatureIds } from '../../../../types'; +import { SOURCE_ADDED_PATH, getSourcesPath } from '../../../../routes'; + +import { AddSourceHeader } from './add_source_header'; +import { ConfigCompleted } from './config_completed'; +import { ConfigurationIntro } from './configuration_intro'; +import { ConfigureCustom } from './configure_custom'; +import { ConfigureOauth } from './configure_oauth'; +import { ConnectInstance } from './connect_instance'; +import { ReAuthenticate } from './re_authenticate'; +import { SaveConfig } from './save_config'; +import { SaveCustom } from './save_custom'; + +enum Steps { + ConfigIntroStep = 'Config Intro', + SaveConfigStep = 'Save Config', + ConfigCompletedStep = 'Config Completed', + ConnectInstanceStep = 'Connect Instance', + ConfigureCustomStep = 'Configure Custom', + ConfigureOauthStep = 'Configure Oauth', + SaveCustomStep = 'Save Custom', + ReAuthenticateStep = 'ReAuthenticate', +} + +interface AddSourceProps { + sourceIndex: number; + connect?: boolean; + configure?: boolean; + reAuthenticate?: boolean; +} + +export const AddSource: React.FC = ({ + sourceIndex, + connect, + configure, + reAuthenticate, +}) => { + const history = useHistory() as History; + const { + getSourceConfigData, + saveSourceConfig, + createContentSource, + resetSourceState, + } = useActions(SourceLogic); + const { + sourceConfigData: { + name, + categories, + needsPermissions, + accountContextOnly, + privateSourcesEnabled, + }, + dataLoading, + newCustomSource, + } = useValues(SourceLogic); + + const { + serviceType, + configuration, + features, + objTypes, + sourceDescription, + connectStepDescription, + addPath, + } = staticSourceData[sourceIndex] as SourceDataItem; + + const { isOrganization } = useValues(AppLogic); + + useEffect(() => { + getSourceConfigData(serviceType); + return resetSourceState; + }, []); + + const isCustom = serviceType === CUSTOM_SERVICE_TYPE; + const isRemote = features?.platinumPrivateContext.includes(FeatureIds.Remote); + + const getFirstStep = () => { + if (isCustom) return Steps.ConfigureCustomStep; + if (connect) return Steps.ConnectInstanceStep; + if (configure) return Steps.ConfigureOauthStep; + if (reAuthenticate) return Steps.ReAuthenticateStep; + return Steps.ConfigIntroStep; + }; + + const [currentStep, setStep] = useState(getFirstStep()); + + if (dataLoading) return ; + + const goToConfigurationIntro = () => setStep(Steps.ConfigIntroStep); + const goToSaveConfig = () => setStep(Steps.SaveConfigStep); + const setConfigCompletedStep = () => setStep(Steps.ConfigCompletedStep); + const goToConfigCompleted = () => saveSourceConfig(false, setConfigCompletedStep); + + const goToConnectInstance = () => { + setStep(Steps.ConnectInstanceStep); + history.push(`${getSourcesPath(addPath, isOrganization)}/connect`); + }; + + const saveCustomSuccess = () => setStep(Steps.SaveCustomStep); + const goToSaveCustom = () => createContentSource(CUSTOM_SERVICE_TYPE, saveCustomSuccess); + + const goToFormSourceCreated = (sourceName: string) => { + history.push(`${getSourcesPath(SOURCE_ADDED_PATH, isOrganization)}/?name=${sourceName}`); + }; + + const pageTitle = () => { + if (currentStep === Steps.ConnectInstanceStep || currentStep === Steps.ConfigureOauthStep) { + return 'Connect'; + } + if (currentStep === Steps.ReAuthenticateStep) { + return 'Re-authenticate'; + } + if (currentStep === Steps.ConfigureCustomStep || currentStep === Steps.SaveCustomStep) { + return 'Create a'; + } + return 'Configure'; + }; + + const CREATE_CUSTOM_SOURCE_SIDEBAR_BLURB = + 'Custom API Sources provide a set of feature-rich endpoints for indexing data from any content repository.'; + const CONFIGURE_ORGANIZATION_SOURCE_SIDEBAR_BLURB = + 'Follow the configuration flow to add a new content source to Workplace Search. First, create an OAuth application in the content source. After that, connect as many instances of the content source that you need.'; + const CONFIGURE_PRIVATE_SOURCE_SIDEBAR_BLURB = + 'Follow the configuration flow to add a new private content source to Workplace Search. Private content sources are added by each person via their own personal dashboards. Their data stays safe and visible only to them.'; + const CONNECT_ORGANIZATION_SOURCE_SIDEBAR_BLURB = `Upon successfully connecting ${name}, source content will be synced to your organization and will be made available and searchable.`; + const CONNECT_PRIVATE_REMOTE_SOURCE_SIDEBAR_BLURB = ( + <> + {name} is a remote source, which means that each time you search, we reach + out to the content source and get matching results directly from {name}'s servers. + + ); + const CONNECT_PRIVATE_STANDARD_SOURCE_SIDEBAR_BLURB = ( + <> + {name} is a standard source for which content is synchronized on a regular + basis, in a relevant and secure way. + + ); + + const CONNECT_PRIVATE_SOURCE_SIDEBAR_BLURB = isRemote + ? CONNECT_PRIVATE_REMOTE_SOURCE_SIDEBAR_BLURB + : CONNECT_PRIVATE_STANDARD_SOURCE_SIDEBAR_BLURB; + const CONFIGURE_SOURCE_SIDEBAR_BLURB = accountContextOnly + ? CONFIGURE_PRIVATE_SOURCE_SIDEBAR_BLURB + : CONFIGURE_ORGANIZATION_SOURCE_SIDEBAR_BLURB; + + const CONFIG_SIDEBAR_BLURB = isCustom + ? CREATE_CUSTOM_SOURCE_SIDEBAR_BLURB + : CONFIGURE_SOURCE_SIDEBAR_BLURB; + const CONNECT_SIDEBAR_BLURB = isOrganization + ? CONNECT_ORGANIZATION_SOURCE_SIDEBAR_BLURB + : CONNECT_PRIVATE_SOURCE_SIDEBAR_BLURB; + + const PAGE_DESCRIPTION = + currentStep === Steps.ConnectInstanceStep ? CONNECT_SIDEBAR_BLURB : CONFIG_SIDEBAR_BLURB; + + const header = ; + + return ( + <> + + {currentStep === Steps.ConfigIntroStep && ( + + )} + {currentStep === Steps.SaveConfigStep && ( + + )} + {currentStep === Steps.ConfigCompletedStep && ( + + )} + {currentStep === Steps.ConnectInstanceStep && ( + + )} + {currentStep === Steps.ConfigureCustomStep && ( + + )} + {currentStep === Steps.ConfigureOauthStep && ( + + )} + {currentStep === Steps.SaveCustomStep && ( + + )} + {currentStep === Steps.ReAuthenticateStep && } + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx new file mode 100644 index 0000000000000..22230bb59f847 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.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 from 'react'; + +import { startCase } from 'lodash'; + +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTextColor } from '@elastic/eui'; + +import { SourceIcon } from '../../../../components/shared/source_icon'; + +interface AddSourceHeaderProps { + name: string; + serviceType: string; + categories: string[]; +} + +export const AddSourceHeader: React.FC = ({ + name, + serviceType, + categories, +}) => { + return ( + <> + + + + + + + +

+ {name} +

+
+ + {categories.map((category) => startCase(category)).join(', ')} + +
+
+ + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx new file mode 100644 index 0000000000000..c8fabaac2a4d1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, ChangeEvent } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiFieldSearch, + EuiFormRow, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiEmptyPrompt, +} from '@elastic/eui'; +import noSharedSourcesIcon from '../../../../assets/share_circle.svg'; + +import { AppLogic } from '../../../../app_logic'; +import { ContentSection } from '../../../../components/shared/content_section'; +import { ViewContentHeader } from '../../../../components/shared/view_content_header'; +import { Loading } from '../../../../../../applications/shared/loading'; +import { CUSTOM_SERVICE_TYPE } from '../../../../constants'; +import { SourceDataItem } from '../../../../types'; + +import { SourcesLogic } from '../../sources_logic'; +import { AvailableSourcesList } from './available_sources_list'; +import { ConfiguredSourcesList } from './configured_sources_list'; + +const NEW_SOURCE_DESCRIPTION = + 'When configuring and connecting a source, you are creating distinct entities with searchable content synchronized from the content platform itself. A source can be added using one of the available source connectors or via Custom API Sources, for additional flexibility.'; +const ORG_SOURCE_DESCRIPTION = + 'Shared content sources are available to your entire organization or can be assigned to specific user groups.'; +const PRIVATE_SOURCE_DESCRIPTION = + 'Connect a new source to add its content and documents to your search experience.'; +const NO_SOURCES_TITLE = 'Configure and connect your first content source'; +const ORG_SOURCES_TITLE = 'Add a shared content source'; +const PRIVATE_SOURCES_TITLE = 'Add a new content source'; +const PLACEHOLDER = 'Filter sources...'; + +export const AddSourceList: React.FC = () => { + const { contentSources, dataLoading, availableSources, configuredSources } = useValues( + SourcesLogic + ); + + const { initializeSources, resetSourcesState } = useActions(SourcesLogic); + + const { isOrganization } = useValues(AppLogic); + + const [filterValue, setFilterValue] = useState(''); + + useEffect(() => { + initializeSources(); + return resetSourcesState; + }, []); + + if (dataLoading) return ; + + const hasSources = contentSources.length > 0; + const showConfiguredSourcesList = configuredSources.find( + ({ serviceType }) => serviceType !== CUSTOM_SERVICE_TYPE + ); + + const BASE_DESCRIPTION = hasSources ? '' : NEW_SOURCE_DESCRIPTION; + const PAGE_CONTEXT_DESCRIPTION = isOrganization + ? ORG_SOURCE_DESCRIPTION + : PRIVATE_SOURCE_DESCRIPTION; + + const PAGE_DESCRIPTION = BASE_DESCRIPTION + PAGE_CONTEXT_DESCRIPTION; + const HAS_SOURCES_TITLE = isOrganization ? ORG_SOURCES_TITLE : PRIVATE_SOURCES_TITLE; + const PAGE_TITLE = hasSources ? HAS_SOURCES_TITLE : NO_SOURCES_TITLE; + + const handleFilterChange = (e: ChangeEvent) => setFilterValue(e.target.value); + + const filterSources = (source: SourceDataItem, sources: SourceDataItem[]): boolean => { + if (!filterValue) return true; + const filterSource = sources.find(({ serviceType }) => serviceType === source.serviceType); + const filteredName = filterSource?.name || ''; + return filteredName.toLowerCase().indexOf(filterValue.toLowerCase()) > -1; + }; + + const filterAvailableSources = (source: SourceDataItem) => + filterSources(source, availableSources); + const filterConfiguredSources = (source: SourceDataItem) => + filterSources(source, configuredSources); + + const visibleAvailableSources = availableSources.filter( + filterAvailableSources + ) as SourceDataItem[]; + const visibleConfiguredSources = configuredSources.filter( + filterConfiguredSources + ) as SourceDataItem[]; + + return ( + <> + + {showConfiguredSourcesList || isOrganization ? ( + + + + + + + {showConfiguredSourcesList && ( + + )} + {isOrganization && } + + ) : ( + + + + + + + + No available sources} + body={ +

+ Sources will be available for search when an administrator adds them to this + organization. +

+ } + /> + + +
+ +
+
+
+ )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx new file mode 100644 index 0000000000000..0d4345c67cfb3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.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 React from 'react'; +import { Link } from 'react-router-dom'; + +import { + EuiCard, + EuiFlexGrid, + EuiFlexItem, + EuiSpacer, + EuiTitle, + EuiText, + EuiToolTip, +} from '@elastic/eui'; + +import { useValues } from 'kea'; + +import { LicensingLogic } from '../../../../../../applications/shared/licensing'; + +import { SourceIcon } from '../../../../components/shared/source_icon'; +import { SourceDataItem } from '../../../../types'; +import { ADD_CUSTOM_PATH, getSourcesPath } from '../../../../routes'; + +interface AvailableSourcesListProps { + sources: SourceDataItem[]; +} + +export const AvailableSourcesList: React.FC = ({ sources }) => { + const { hasPlatinumLicense } = useValues(LicensingLogic); + + const getSourceCard = ({ name, serviceType, addPath, accountContextOnly }: SourceDataItem) => { + const disabled = !hasPlatinumLicense && accountContextOnly; + const card = ( + } + isDisabled={disabled} + icon={ + + } + /> + ); + + if (disabled) { + return ( + + {card} + + ); + } + return {card}; + }; + + const visibleSources = ( + + {sources.map((source, i) => ( + + {getSourceCard(source)} + + ))} + + ); + + const emptyState =

No available sources matching your query.

; + + return ( + <> + +

Available for configuration

+
+ +

+ Configure an available source or build your own with the{' '} + + Custom API Source + + . +

+
+ + {sources.length > 0 ? visibleSources : emptyState} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx new file mode 100644 index 0000000000000..0409bbf578d5a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { Link } from 'react-router-dom'; + +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiSpacer, + EuiText, + EuiTextAlign, +} from '@elastic/eui'; + +import { + getSourcesPath, + ADD_SOURCE_PATH, + SECURITY_PATH, + PRIVATE_SOURCES_DOCS_URL, +} from '../../../../routes'; + +interface ConfigCompletedProps { + header: React.ReactNode; + name: string; + accountContextOnly?: boolean; + privateSourcesEnabled: boolean; + advanceStep(): void; +} + +export const ConfigCompleted: React.FC = ({ + name, + advanceStep, + accountContextOnly, + header, + privateSourcesEnabled, +}) => ( +
+ {header} + + + + + + + + + + +

{name} Configured

+
+
+ + + {!accountContextOnly ? ( +

{name} can now be connected to Workplace Search

+ ) : ( + +

Users can now link their {name} accounts from their personal dashboards.

+ {!privateSourcesEnabled && ( +

+ Remember to{' '} + + enable private source connection + {' '} + in Security settings. +

+ )} +

+ + Learn more about private content sources. + +

+
+ )} +
+
+
+
+
+
+ + + + + + Configure a new content source + + + + {!accountContextOnly && ( + + + Connect {name} + + + )} + +
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.tsx new file mode 100644 index 0000000000000..b666c859948d5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.tsx @@ -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 React from 'react'; + +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +interface ConfigDocsLinksProps { + name: string; + documentationUrl: string; + applicationPortalUrl?: string; + applicationLinkTitle?: string; +} + +export const ConfigDocsLinks: React.FC = ({ + name, + documentationUrl, + applicationPortalUrl, + applicationLinkTitle, +}) => ( + + + + Documentation + + + + {applicationPortalUrl && ( + + {applicationLinkTitle || `${name} Application Portal`} + + )} + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx new file mode 100644 index 0000000000000..2bf5134e59e26 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + EuiBadge, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import connectionIllustration from 'workplace_search/components/assets/connectionIllustration.svg'; + +interface ConfigurationIntroProps { + header: React.ReactNode; + name: string; + advanceStep(): void; +} + +export const ConfigurationIntro: React.FC = ({ + name, + advanceStep, + header, +}) => ( +
+ {header} + + + + +
+ connection illustration +
+
+ + + + + +

How to add {name}

+
+ + +

Quick setup, then all of your documents will be searchable.

+
+ +
+ + + +
+ +

Step 1

+
+
+
+ + +

+ Configure an OAuth application  + One-Time Action +

+

+ Setup a secure OAuth application through the content source that you or your + team will use to connect and synchronize content. You only have to do this + once per content source. +

+
+
+
+
+ + + +
+ +

Step 2

+
+
+
+ + +

Connect the content source

+

+ Use the new OAuth application to connect any number of instances of the + content source to Workplace Search. +

+
+
+
+
+ + + + + Configure {name} + + + + +
+
+
+
+
+
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx new file mode 100644 index 0000000000000..3788071979e67 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { ChangeEvent, FormEvent } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiLink, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import { CUSTOM_SOURCE_DOCS_URL } from '../../../../routes'; +import { SourceLogic } from '../../source_logic'; + +interface ConfigureCustomProps { + header: React.ReactNode; + helpText: string; + advanceStep(): void; +} + +export const ConfigureCustom: React.FC = ({ + helpText, + advanceStep, + header, +}) => { + const { setCustomSourceNameValue } = useActions(SourceLogic); + const { customSourceNameValue, buttonLoading } = useValues(SourceLogic); + + const handleFormSubmit = (e: FormEvent) => { + e.preventDefault(); + advanceStep(); + }; + + const handleNameChange = (e: ChangeEvent) => + setCustomSourceNameValue(e.target.value); + + return ( +
+ {header} +
+ + +

{helpText}

+

+ + Read the documentation + {' '} + to learn more about Custom API Sources. +

+
+ + + + + + + + Create Custom API Source + + +
+
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx new file mode 100644 index 0000000000000..9c2084483c816 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, FormEvent } from 'react'; + +import { Location } from 'history'; +import { useActions, useValues } from 'kea'; +import { useLocation } from 'react-router-dom'; + +import { + EuiButton, + EuiCheckboxGroup, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, +} from '@elastic/eui'; + +import { EuiCheckboxGroupIdToSelectedMap } from '@elastic/eui/src/components/form/checkbox/checkbox_group'; + +import { parseQueryParams } from '../../../../../../applications/shared/query_params'; +import { Loading } from '../../../../../../applications/shared/loading'; +import { SourceLogic } from '../../source_logic'; + +interface OauthQueryParams { + preContentSourceId: string; +} + +interface ConfigureOauthProps { + header: React.ReactNode; + name: string; + onFormCreated(name: string): void; +} + +export const ConfigureOauth: React.FC = ({ name, onFormCreated, header }) => { + const { search } = useLocation() as Location; + + const { preContentSourceId } = (parseQueryParams(search) as unknown) as OauthQueryParams; + const [formLoading, setFormLoading] = useState(false); + + const { + getPreContentSourceConfigData, + setSelectedGithubOrganizations, + createContentSource, + } = useActions(SourceLogic); + const { + currentServiceType, + githubOrganizations, + selectedGithubOrganizationsMap, + sectionLoading, + } = useValues(SourceLogic); + + const checkboxOptions = githubOrganizations.map((item) => ({ id: item, label: item })); + + useEffect(() => { + getPreContentSourceConfigData(preContentSourceId); + }, []); + + const handleChange = (option: string) => setSelectedGithubOrganizations(option); + const formSubmitSuccess = () => onFormCreated(name); + const handleFormSubmitError = () => setFormLoading(false); + const handleFormSubmut = (e: FormEvent) => { + setFormLoading(true); + e.preventDefault(); + createContentSource(currentServiceType, formSubmitSuccess, handleFormSubmitError); + }; + + const configfieldsForm = ( +
+ + + + + + + + + Complete connection + + + + +
+ ); + + return ( +
+ {header} + {sectionLoading ? : configfieldsForm} +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx new file mode 100644 index 0000000000000..a95d5ca75b0b6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { Link } from 'react-router-dom'; + +import { + EuiButtonEmpty, + EuiFlexGrid, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, + EuiTitle, + EuiToken, + EuiToolTip, +} from '@elastic/eui'; + +import { SourceIcon } from '../../../../components/shared/source_icon'; +import { SourceDataItem } from '../../../../types'; +import { getSourcesPath } from '../../../../routes'; + +interface ConfiguredSourcesProps { + sources: SourceDataItem[]; + isOrganization: boolean; +} + +export const ConfiguredSourcesList: React.FC = ({ + sources, + isOrganization, +}) => { + const unConnectedTooltip = ( + + + + + + ); + + const accountOnlyTooltip = ( + + + + + + ); + + const visibleSources = ( + + {sources.map(({ name, serviceType, addPath, connected, accountContextOnly }, i) => ( + + +
+ + + + + + + + +

+ {name}  + {!connected && + !accountContextOnly && + isOrganization && + unConnectedTooltip} + {accountContextOnly && isOrganization && accountOnlyTooltip} +

+
+
+
+
+ {(!isOrganization || (isOrganization && !accountContextOnly)) && ( + + + Connect + + + )} +
+
+
+
+ ))} +
+ ); + + const emptyState =

There are no configured sources matching your query.

; + + return ( + <> + +

Configured content sources

+
+ +

Configured and ready for connection.

+
+ + {sources.length > 0 ? visibleSources : emptyState} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx new file mode 100644 index 0000000000000..ad183181b4eca --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx @@ -0,0 +1,256 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, FormEvent } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiFieldText, + EuiFormRow, + EuiLink, + EuiSpacer, + EuiSwitch, + EuiText, + EuiTitle, + EuiTextColor, + EuiBadge, + EuiBadgeGroup, +} from '@elastic/eui'; + +import { LicensingLogic } from '../../../../../../applications/shared/licensing'; + +import { AppLogic } from '../../../../app_logic'; +import { SourceLogic } from '../../source_logic'; +import { FeatureIds, Configuration, Features } from '../../../../types'; +import { DOCUMENT_PERMISSIONS_DOCS_URL } from '../../../../routes'; +import { SourceFeatures } from './source_features'; + +interface ConnectInstanceProps { + header: React.ReactNode; + configuration: Configuration; + features?: Features; + objTypes?: string[]; + name: string; + serviceType: string; + sourceDescription: string; + connectStepDescription: string; + needsPermissions: boolean; + onFormCreated(name: string): void; +} + +export const ConnectInstance: React.FC = ({ + configuration: { needsSubdomain, hasOauthRedirect }, + features, + objTypes, + name, + serviceType, + sourceDescription, + connectStepDescription, + needsPermissions, + onFormCreated, + header, +}) => { + const [formLoading, setFormLoading] = useState(false); + + const { hasPlatinumLicense } = useValues(LicensingLogic); + + const { + getSourceConnectData, + createContentSource, + setSourceLoginValue, + setSourcePasswordValue, + setSourceSubdomainValue, + setSourceIndexPermissionsValue, + } = useActions(SourceLogic); + + const { loginValue, passwordValue, indexPermissionsValue, subdomainValue } = useValues( + SourceLogic + ); + + const { isOrganization } = useValues(AppLogic); + + // Default indexPermissions to true, if needed + useEffect(() => { + setSourceIndexPermissionsValue(needsPermissions && isOrganization && hasPlatinumLicense); + }, []); + + const redirectOauth = (oauthUrl: string) => (window.location.href = oauthUrl); + const redirectFormCreated = () => onFormCreated(name); + const onOauthFormSubmit = () => getSourceConnectData(serviceType, redirectOauth); + const handleFormSubmitError = () => setFormLoading(false); + const onCredentialsFormSubmit = () => + createContentSource(serviceType, redirectFormCreated, handleFormSubmitError); + + const handleFormSubmit = (e: FormEvent) => { + setFormLoading(true); + e.preventDefault(); + const onSubmit = hasOauthRedirect ? onOauthFormSubmit : onCredentialsFormSubmit; + onSubmit(); + }; + + const credentialsFields = ( + <> + + setSourceLoginValue(e.target.value)} + /> + + + setSourcePasswordValue(e.target.value)} + /> + + + + ); + + const subdomainField = ( + <> + + setSourceSubdomainValue(e.target.value)} + /> + + + + ); + + const featureBadgeGroup = () => { + if (isOrganization) { + return null; + } + + const isRemote = features?.platinumPrivateContext.includes(FeatureIds.Remote); + const isPrivate = features?.platinumPrivateContext.includes(FeatureIds.Private); + + if (isRemote || isPrivate) { + return ( + <> + + {isRemote && Remote} + {isPrivate && Private} + + + + ); + } + }; + + const descriptionBlock = ( + + {sourceDescription &&

{sourceDescription}

} + {connectStepDescription &&

{connectStepDescription}

} + +
+ ); + + const whichDocsLink = ( + + Which option should I choose? + + ); + + const permissionField = ( + <> + + + Document-level permissions + + + + Enable document-level permission synchronization} + name="index_permissions" + onChange={(e) => setSourceIndexPermissionsValue(e.target.checked)} + checked={indexPermissionsValue} + disabled={!needsPermissions} + /> + + + {!needsPermissions && ( + + Document-level permissions are not yet available for this source.{' '} + + Learn more + + + )} + {needsPermissions && indexPermissionsValue && ( + + Document-level permission information will be synchronized. Additional configuration is + required following the initial connection before documents are available for search. +
+ {whichDocsLink} +
+ )} +
+ + {!indexPermissionsValue && ( + +

+ All documents accessible to the connecting service user will be synchronized and made + available to the organization’s users, or group’s users. Documents are immediately + available for search. {needsPermissions && whichDocsLink} +

+
+ )} + + + ); + + const formFields = ( + <> + {isOrganization && hasPlatinumLicense && permissionField} + {!hasOauthRedirect && credentialsFields} + {needsSubdomain && subdomainField} + + + + Connect {name} + + + + ); + + return ( +
+
+ + + {header} + {featureBadgeGroup()} + {descriptionBlock} + {formFields} + + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/index.ts new file mode 100644 index 0000000000000..8a46eaa7d70e9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/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 { AddSource } from './add_source'; +export { AddSourceList } from './add_source_list'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx new file mode 100644 index 0000000000000..7336a3b51a444 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.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, { useEffect, useState, FormEvent } from 'react'; + +import { Location } from 'history'; +import { useActions, useValues } from 'kea'; +import { useLocation } from 'react-router-dom'; + +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { parseQueryParams } from '../../../../../../applications/shared/query_params'; + +import { SourceLogic } from '../../source_logic'; + +interface SourceQueryParams { + sourceId: string; +} + +interface ReAuthenticateProps { + name: string; + header: React.ReactNode; +} + +export const ReAuthenticate: React.FC = ({ name, header }) => { + const { search } = useLocation() as Location; + + const { sourceId } = (parseQueryParams(search) as unknown) as SourceQueryParams; + const [formLoading, setFormLoading] = useState(false); + + const { getSourceReConnectData } = useActions(SourceLogic); + const { + sourceConnectData: { oauthUrl }, + } = useValues(SourceLogic); + + useEffect(() => { + getSourceReConnectData(sourceId); + }, []); + + const handleFormSubmit = (e: FormEvent) => { + e.preventDefault(); + setFormLoading(true); + window.location.href = oauthUrl; + }; + + return ( +
+ {header} +
+ + +

+ Your {name} credentials are no longer valid. Please re-authenticate with the original + credentials to resume content syncing. +

+
+
+ + + + Re-authenticate {name} + + + +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx new file mode 100644 index 0000000000000..4036bb6a771bb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { FormEvent } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiSteps, +} from '@elastic/eui'; + +import { LicensingLogic } from '../../../../../../applications/shared/licensing'; + +import { ApiKey } from '../../../../components/shared/api_key'; +import { SourceLogic } from '../../source_logic'; +import { Configuration } from '../../../../types'; + +import { ConfigDocsLinks } from './config_docs_links'; + +interface SaveConfigProps { + header: React.ReactNode; + name: string; + configuration: Configuration; + advanceStep(): void; + goBackStep?(): void; + onDeleteConfig?(): void; +} + +export const SaveConfig: React.FC = ({ + name, + configuration: { + isPublicKey, + needsBaseUrl, + documentationUrl, + applicationPortalUrl, + applicationLinkTitle, + baseUrlTitle, + }, + advanceStep, + goBackStep, + onDeleteConfig, + header, +}) => { + const { hasPlatinumLicense } = useValues(LicensingLogic); + + const { setClientIdValue, setClientSecretValue, setBaseUrlValue } = useActions(SourceLogic); + + const { + sourceConfigData, + buttonLoading, + clientIdValue, + clientSecretValue, + baseUrlValue, + } = useValues(SourceLogic); + + const { + accountContextOnly, + configuredFields: { publicKey, consumerKey }, + } = sourceConfigData; + + const handleFormSubmission = (e: FormEvent) => { + e.preventDefault(); + advanceStep(); + }; + + const saveButton = ( + + Save configuration + + ); + + const deleteButton = ( + + Remove + + ); + + const backButton =  Go back; + const showSaveButton = hasPlatinumLicense || !accountContextOnly; + + const formActions = ( + + + {showSaveButton && {saveButton}} + + {goBackStep && backButton} + {onDeleteConfig && deleteButton} + + + + ); + + const publicKeyStep1 = ( + + + + + + + + + + + + + + ); + + const credentialsStep1 = ( + + ); + + const publicKeyStep2 = ( + <> + + setBaseUrlValue(e.target.value)} + name="base-uri" + /> + + + {formActions} + + ); + + const credentialsStep2 = ( + + + + + setClientIdValue(e.target.value)} + name="client-id" + /> + + + setClientSecretValue(e.target.value)} + name="client-secret" + /> + + {needsBaseUrl && ( + + setBaseUrlValue(e.target.value)} + name="base-uri" + /> + + )} + + {formActions} + + + + ); + + const oauthSteps = (sourceName: string) => [ + `Create an OAuth app in your organization's ${sourceName}\u00A0account`, + 'Provide the appropriate configuration information', + ]; + + const configSteps = [ + { + title: oauthSteps(name)[0], + children: isPublicKey ? publicKeyStep1 : credentialsStep1, + }, + { + title: oauthSteps(name)[1], + children: isPublicKey ? publicKeyStep2 : credentialsStep2, + }, + ]; + + return ( + <> + {header} +
+ + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx new file mode 100644 index 0000000000000..17510c3ece914 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.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 { Link } from 'react-router-dom'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiIcon, + EuiSpacer, + EuiText, + EuiTextAlign, + EuiTitle, + EuiLink, + EuiPanel, +} from '@elastic/eui'; + +import { CredentialItem } from '../../../../components/shared/credential_item'; +import { LicenseBadge } from '../../../../components/shared/license_badge'; + +import { CustomSource } from '../../../../types'; +import { + SOURCES_PATH, + SOURCE_DISPLAY_SETTINGS_PATH, + CUSTOM_API_DOCUMENT_PERMISSIONS_DOCS_URL, + ENT_SEARCH_LICENSE_MANAGEMENT, + getContentSourcePath, + getSourcesPath, +} from '../../../../routes'; + +interface SaveCustomProps { + documentationUrl: string; + newCustomSource: CustomSource; + isOrganization: boolean; + header: React.ReactNode; +} + +export const SaveCustom: React.FC = ({ + documentationUrl, + newCustomSource: { key, id, accessToken, name }, + isOrganization, + header, +}) => ( +
+ {header} + + + + + + + + + + +

{name} Created

+
+
+ + + Your endpoints are ready to accept requests. +
+ Be sure to copy your API keys below. +
+ + Return to Sources + +
+
+
+
+ + + + +

API Keys

+
+ +

You'll need these keys to sync documents for this custom source.

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

Visual Walkthrough

+
+ + +

+ + Check out the documentation + {' '} + to learn more about Custom API Sources. +

+
+
+ +
+ +

Styling Results

+
+ + +

+ Use{' '} + + Display Settings + {' '} + to customize how your documents will appear within your search results. Workplace + Search will use fields in alphabetical order by default. +

+
+
+ +
+ + + + +

Set document-level permissions

+
+ + +

+ + Document-level permissions + {' '} + manage content access content on individual or group attributes. Allow or deny + access to specific documents. +

+
+ + + + Learn about Platinum features + + +
+
+
+
+
+
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx new file mode 100644 index 0000000000000..6c92f3a9e13ff --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx @@ -0,0 +1,223 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { useValues } from 'kea'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { LicensingLogic } from '../../../../../../applications/shared/licensing'; + +import { AppLogic } from '../../../../app_logic'; +import { LicenseBadge } from '../../../../components/shared/license_badge'; +import { Features, FeatureIds } from '../../../../types'; +import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../../routes'; + +interface ConnectInstanceProps { + features?: Features; + objTypes?: string[]; + name: string; +} + +export const SourceFeatures: React.FC = ({ features, objTypes, name }) => { + const { hasPlatinumLicense } = useValues(LicensingLogic); + const { isOrganization } = useValues(AppLogic); + + const Feature = ({ title, children }: { title: string; children: React.ReactElement }) => ( + <> + + + {title} + + + {children} + + ); + + const SyncFrequencyFeature = ( + + +

+ This source gets new content from {name} every 2 hours (following the + initial sync). +

+
+
+ ); + + const SyncedItemsFeature = ( + + <> + +

The following items are searchable:

+
+ + +
    + {objTypes!.map((objType, i) => ( +
  • {objType}
  • + ))} +
+
+ +
+ ); + + const SearchableContentFeature = ( + + + +

The following items are searchable:

+
+ +
    + {objTypes!.map((objType, i) => ( +
  • {objType}
  • + ))} +
+
+
+ ); + + const RemoteFeature = ( + + +

+ Message data and other information is searchable in real-time from the Workplace Search + experience. +

+
+
+ ); + + const PrivateFeature = ( + + +

+ Results returned are specific and relevant to you. Connecting this source does not expose + your personal data to other search users - only you. +

+
+
+ ); + + const GlobalAccessPermissionsFeature = ( + + +

+ All documents accessible to the connecting service user will be synchronized and made + available to the organization’s users, or group’s users. Documents are immediately + available for search +

+
+
+ ); + + const DocumentLevelPermissionsFeature = ( + + +

+ Document-level permissions manage user content access based on defined rules. Allow or + deny access to certain documents for individuals and groups. +

+ + Explore Platinum features + +
+
+ ); + + const FeaturesRouter = ({ featureId }: { featureId: FeatureIds }) => + ({ + [FeatureIds.SyncFrequency]: SyncFrequencyFeature, + [FeatureIds.SearchableContent]: SearchableContentFeature, + [FeatureIds.SyncedItems]: SyncedItemsFeature, + [FeatureIds.Remote]: RemoteFeature, + [FeatureIds.Private]: PrivateFeature, + [FeatureIds.GlobalAccessPermissions]: GlobalAccessPermissionsFeature, + [FeatureIds.DocumentLevelPermissions]: DocumentLevelPermissionsFeature, + }[featureId]); + + const IncludedFeatures = () => { + let includedFeatures: FeatureIds[] | undefined; + + if (!hasPlatinumLicense && isOrganization) { + includedFeatures = features?.basicOrgContext; + } + if (hasPlatinumLicense && isOrganization) { + includedFeatures = features?.platinumOrgContext; + } + if (hasPlatinumLicense && !isOrganization) { + includedFeatures = features?.platinumPrivateContext; + } + + if (!includedFeatures?.length) { + return null; + } + + return ( + + +

Included features

+
+ {includedFeatures.map((featureId, i) => ( + + ))} +
+ ); + }; + + const ExcludedFeatures = () => { + let excludedFeatures: FeatureIds[] | undefined; + + if (!hasPlatinumLicense && isOrganization) { + excludedFeatures = features?.basicOrgContextExcludedFeatures; + } + + if (!excludedFeatures?.length) { + return null; + } + + return ( + + + {excludedFeatures.map((featureId, i) => ( + + ))} + + ); + }; + + return ( + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx new file mode 100644 index 0000000000000..0155c07f4e0bb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx @@ -0,0 +1,532 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { useValues } from 'kea'; +import { Link } from 'react-router-dom'; + +import { + EuiAvatar, + EuiButtonEmpty, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiIconTip, + EuiLink, + EuiPanel, + EuiSpacer, + EuiTable, + EuiTableBody, + EuiTableHeader, + EuiTableHeaderCell, + EuiTableRow, + EuiTableRowCell, + EuiText, + EuiTextColor, + EuiTitle, +} from '@elastic/eui'; + +import { + CUSTOM_SOURCE_DOCS_URL, + DOCUMENT_PERMISSIONS_DOCS_URL, + ENT_SEARCH_LICENSE_MANAGEMENT, + EXTERNAL_IDENTITIES_DOCS_URL, + SOURCE_CONTENT_PATH, + getContentSourcePath, + getGroupPath, +} from '../../../routes'; + +import { AppLogic } from '../../../app_logic'; +import { User } from '../../../types'; + +import { ComponentLoader } from '../../../components/shared/component_loader'; +import { CredentialItem } from '../../../components/shared/credential_item'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; +import { LicenseBadge } from '../../../components/shared/license_badge'; +import { Loading } from '../../../../../applications/shared/loading'; + +import aclImage from '../../../assets/supports_acl.svg'; +import { SourceLogic } from '../source_logic'; + +export const Overview: React.FC = () => { + const { contentSource, dataLoading } = useValues(SourceLogic); + const { isOrganization } = useValues(AppLogic); + + const { + id, + summary, + documentCount, + activities, + groups, + details, + custom, + accessToken, + key, + licenseSupportsPermissions, + serviceTypeSupportsPermissions, + indexPermissions, + hasPermissions, + isFederatedSource, + } = contentSource; + + if (dataLoading) return ; + + const DocumentSummary = () => { + let totalDocuments = 0; + const tableContent = + summary && + summary.map((item, index) => { + totalDocuments += item.count; + return ( + item.count > 0 && ( + + {item.type} + {item.count.toLocaleString('en-US')} + + ) + ); + }); + + const emptyState = ( + <> + + + No content yet} + iconType="documents" + iconColor="subdued" + /> + + + ); + + return ( +
+
+ + + +

Content summary

+
+
+ {totalDocuments > 0 && ( + + + + Manage + + + + )} +
+
+ + {!summary && } + {!!summary && + (totalDocuments === 0 ? ( + emptyState + ) : ( + + + Content Type + Items + + + {tableContent} + + + {summary ? Total documents : 'Documents'} + + + {summary ? ( + {totalDocuments.toLocaleString('en-US')} + ) : ( + parseInt(documentCount, 10).toLocaleString('en-US') + )} + + + + + ))} +
+ ); + }; + + const ActivitySummary = () => { + const emptyState = ( + <> + + + There is no recent activity} + iconType="clock" + iconColor="subdued" + /> + + + ); + + const activitiesTable = ( + + + Event + {!custom && Status} + Time + + + {activities.map(({ details: activityDetails, event, time, status }, i) => ( + + + {event} + + {!custom && ( + + + {status}{' '} + {activityDetails && ( + ( +
{detail}
+ ))} + /> + )} +
+
+ )} + + {time} + +
+ ))} +
+
+ ); + + return ( +
+
+ +

Recent activity

+
+
+ + {activities.length === 0 ? emptyState : activitiesTable} +
+ ); + }; + + const GroupsSummary = () => { + const GroupAvatars = ({ users }: { users: User[] }) => { + const MAX_USERS = 4; + return ( + + {users.slice(0, MAX_USERS).map((user) => ( + + + + ))} + {users.slice(MAX_USERS).length > 0 && ( + + + +{users.slice(MAX_USERS).length} + + + )} + + ); + }; + + return !groups.length ? null : ( + + +
+ Group Access +
+
+ + + {groups.map((group, index) => ( + + + + + + + {group.name} + + + + + + + + + + ))} + +
+ ); + }; + + const detailsSummary = ( + + +
+ Configuration +
+
+ + + {details.map((detail, index) => ( + + + {detail.title} + + {detail.description} + + ))} + +
+ ); + + const documentPermissions = ( + <> + + +

Document-level permissions

+
+ + + + + + + + + Using document-level permissions + + + + + + ); + + const documentPermissionsDisabled = ( + <> + + +

Document-level permissions

+
+ + + + + + + + + + Disabled for this source + + + + Learn more + {' '} + about permissions + + + + + + + ); + + const sourceStatus = ( + + +
+ Status +
+
+ + + + + + + + Everything looks good + + +

Your endpoints are ready to accept requests.

+
+
+
+
+ ); + + const permissionsStatus = ( + + +
+ Status +
+
+ + + + + + + + Requires additional configuration + + +

+ The{' '} + + External Identities API + {' '} + must be used to configure user access mappings. Read the guide to learn more. +

+
+
+
+
+ ); + + const credentials = ( + + +
+ Credentials +
+
+ + + + +
+ ); + + const DocumentationCallout = ({ + title, + children, + }: { + title: string; + children: React.ReactNode; + }) => ( + + +
+ Documentation +
+
+ + +

{title}

+
+ {children} +
+ ); + + const documentPermssionsLicenseLocked = ( + + + + +

Document-level permissions

+
+ +

+ Document-level permissions manage content access content on individual or group + attributes. Allow or deny access to specific documents. +

+
+ + + + Learn about Platinum features + + +
+ ); + + return ( + <> + + + + + + + + + {!isFederatedSource && ( + + + + )} + + + + + + + + {details.length > 0 && {detailsSummary}} + {!custom && serviceTypeSupportsPermissions && ( + <> + {indexPermissions && !hasPermissions && ( + {permissionsStatus} + )} + {indexPermissions && {documentPermissions}} + {!indexPermissions && isOrganization && ( + {documentPermissionsDisabled} + )} + {indexPermissions && {credentials}} + + )} + {custom && ( + <> + {sourceStatus} + {credentials} + + +

+ + Learn more + {' '} + about custom sources. +

+
+
+ {!licenseSupportsPermissions && ( + {documentPermssionsLicenseLocked} + )} + + )} +
+
+ +
+ + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx new file mode 100644 index 0000000000000..16aceacbddcd5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.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 { Location } from 'history'; +import { useActions, useValues } from 'kea'; +import { Redirect, useLocation } from 'react-router-dom'; + +import { setErrorMessage } from '../../../../shared/flash_messages'; + +import { parseQueryParams } from '../../../../../applications/shared/query_params'; + +import { SOURCES_PATH, getSourcesPath } from '../../../routes'; + +import { AppLogic } from '../../../app_logic'; +import { SourcesLogic } from '../sources_logic'; + +interface SourceQueryParams { + name: string; + hasError: boolean; + errorMessages?: string[]; + serviceType: string; + indexPermissions: boolean; +} + +export const SourceAdded: React.FC = () => { + const { search } = useLocation() as Location; + const { name, hasError, errorMessages, serviceType, indexPermissions } = (parseQueryParams( + search + ) as unknown) as SourceQueryParams; + const { setAddedSource } = useActions(SourcesLogic); + const { isOrganization } = useValues(AppLogic); + const decodedName = decodeURIComponent(name); + + if (hasError) { + const defaultError = `${decodedName} failed to connect.`; + setErrorMessage(errorMessages ? errorMessages.join(' ') : defaultError); + } else { + setAddedSource(decodedName, indexPermissions, serviceType); + } + + return ; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx new file mode 100644 index 0000000000000..3f289a6394131 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx @@ -0,0 +1,209 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { useActions, useValues } from 'kea'; +import { startCase } from 'lodash'; +import moment from 'moment'; + +import { + EuiButton, + EuiButtonEmpty, + EuiEmptyPrompt, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiTable, + EuiTableBody, + EuiTableHeader, + EuiTableHeaderCell, + EuiTableRow, + EuiTableRowCell, + EuiLink, +} from '@elastic/eui'; + +import { CUSTOM_SOURCE_DOCS_URL } from '../../../routes'; +import { SourceContentItem } from '../../../types'; + +import { TruncatedContent } from '../../../../shared/truncate'; + +const MAX_LENGTH = 28; + +import { ComponentLoader } from '../../../components/shared/component_loader'; +import { Loading } from '../../../../../applications/shared/loading'; +import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; + +import { CUSTOM_SERVICE_TYPE } from '../../../constants'; + +import { SourceLogic } from '../source_logic'; + +export const SourceContent: React.FC = () => { + const [searchTerm, setSearchTerm] = useState(''); + + const { + setActivePage, + searchContentSourceDocuments, + resetSourceState, + setContentFilterValue, + } = useActions(SourceLogic); + + const { + contentSource: { id, serviceType, urlField, titleField, urlFieldIsLinkable, isFederatedSource }, + contentMeta: { + page: { total_pages: totalPages, total_results: totalItems, current: activePage }, + }, + contentItems, + contentFilterValue, + dataLoading, + sectionLoading, + } = useValues(SourceLogic); + + useEffect(() => { + return resetSourceState; + }, []); + + useEffect(() => { + searchContentSourceDocuments(id); + }, [contentFilterValue, activePage]); + + if (dataLoading) return ; + + const showPagination = totalPages > 1; + const hasItems = totalItems > 0; + const emptyMessage = contentFilterValue + ? `No results for '${contentFilterValue}'` + : "This source doesn't have any content yet"; + + const paginationOptions = { + totalPages, + totalItems, + activePage, + onChangePage: (page: number) => { + // EUI component starts page at 0. API starts at 1. + setActivePage(page + 1); + }, + }; + + const isCustomSource = serviceType === CUSTOM_SERVICE_TYPE; + + const emptyState = ( + + + + {emptyMessage}} + iconType="documents" + body={ + isCustomSource ? ( +

+ Learn more about adding content in our{' '} + + documentation + +

+ ) : null + } + /> +
+ +
+ ); + + const contentItem = (item: SourceContentItem) => { + const { id: itemId, last_updated: updated } = item; + const url = item[urlField] || ''; + const title = item[titleField] || ''; + + return ( + + + + + + {!urlFieldIsLinkable && ( + + )} + {urlFieldIsLinkable && ( + + + + )} + + {moment(updated).format('M/D/YYYY, h:mm:ss A')} + + ); + }; + + const contentTable = ( + <> + {showPagination && } + + + + Title + {startCase(urlField)} + Last Updated + + {contentItems.map(contentItem)} + + + {showPagination && } + + ); + + const resetFederatedSearchTerm = () => { + setContentFilterValue(''); + setSearchTerm(''); + }; + const federatedSearchControls = ( + <> + + setContentFilterValue(searchTerm)} + > + Go + + + + + Reset + + + + ); + + return ( + <> + + + + + setSearchTerm(e.target.value)} + /> + + {isFederatedSource && federatedSearchControls} + + + {sectionLoading && } + {!sectionLoading && (hasItems ? contentTable : emptyState)} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx new file mode 100644 index 0000000000000..e3c3e76311018 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiSpacer, +} from '@elastic/eui'; + +import { SourceIcon } from '../../../components/shared/source_icon'; + +interface SourceInfoCardProps { + sourceName: string; + sourceType: string; + dateCreated: string; + isFederatedSource: boolean; +} + +export const SourceInfoCard: React.FC = ({ + sourceName, + sourceType, + dateCreated, + isFederatedSource, +}) => ( + + + + + Connector + + + + + + + + + {sourceName} + + + + + + + + + + + + + Created + + + + {dateCreated} + + + + + {isFederatedSource && ( + <> + + + + + + + Status + + + + + Ready to search + + + + + + + )} + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx new file mode 100644 index 0000000000000..1f756115e3ae4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, ChangeEvent, FormEvent } from 'react'; + +import { History } from 'history'; +import { useActions, useValues } from 'kea'; +import { isEmpty } from 'lodash'; +import { Link, useHistory } from 'react-router-dom'; + +import { + EuiButton, + EuiButtonEmpty, + EuiConfirmModal, + EuiOverlayMask, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, +} from '@elastic/eui'; + +import { SOURCES_PATH, getSourcesPath } from '../../../routes'; + +import { ContentSection } from '../../../components/shared/content_section'; +import { SourceConfigFields } from '../../../components/shared/source_config_fields'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; + +import { SourceDataItem } from '../../../types'; +import { AppLogic } from '../../../app_logic'; +import { staticSourceData } from '../source_data'; + +import { SourceLogic } from '../source_logic'; + +export const SourceSettings: React.FC = () => { + const history = useHistory() as History; + const { + updateContentSource, + removeContentSource, + resetSourceState, + getSourceConfigData, + } = useActions(SourceLogic); + + const { + contentSource: { name, id, serviceType }, + buttonLoading, + sourceConfigData: { configuredFields }, + } = useValues(SourceLogic); + + const { isOrganization } = useValues(AppLogic); + + useEffect(() => { + getSourceConfigData(serviceType); + return resetSourceState; + }, []); + const { + configuration: { isPublicKey }, + editPath, + } = staticSourceData.find((source) => source.serviceType === serviceType) as SourceDataItem; + + const [inputValue, setValue] = useState(name); + const [confirmModalVisible, setModalVisibility] = useState(false); + const showConfirm = () => setModalVisibility(true); + const hideConfirm = () => setModalVisibility(false); + + const showConfig = isOrganization && !isEmpty(configuredFields); + + const { clientId, clientSecret, publicKey, consumerKey, baseUrl } = configuredFields || {}; + + const handleNameChange = (e: ChangeEvent) => setValue(e.target.value); + + const submitNameChange = (e: FormEvent) => { + e.preventDefault(); + updateContentSource(id, { name: inputValue }); + }; + + const handleSourceRemoval = () => { + /** + * The modal was just hanging while the UI waited for the server to respond. + * EuiModal doens't allow the button to have a loading state so we just hide the + * modal here and set the button that was clicked to delete to a loading state. + */ + setModalVisibility(false); + const onSourceRemoved = () => history.push(getSourcesPath(SOURCES_PATH, isOrganization)); + removeContentSource(id, onSourceRemoved); + }; + + const confirmModal = ( + + + Your source documents will be deleted from Workplace Search.
+ Are you sure you want to remove {name}? +
+
+ ); + + return ( + <> + + + +
+ + + + + + + + + Save changes + + + +
+
+ {showConfig && ( + + + + + Edit content source connector settings + + + + )} + + + Remove + + {confirmModalVisible && confirmModal} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index 889519b8a9985..0a11da02dc789 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -23,7 +23,13 @@ import { import { DEFAULT_META } from '../../../shared/constants'; import { AppLogic } from '../../app_logic'; import { NOT_FOUND_PATH } from '../../routes'; -import { ContentSourceFullData, CustomSource, Meta } from '../../types'; +import { + ContentSourceFullData, + CustomSource, + Meta, + DocumentSummaryItem, + SourceContentItem, +} from '../../types'; export interface SourceActions { onInitializeSource(contentSource: ContentSourceFullData): ContentSourceFullData; @@ -32,7 +38,7 @@ export interface SourceActions { setSourceConnectData(sourceConnectData: SourceConnectData): SourceConnectData; setSearchResults(searchResultsResponse: SearchResultsResponse): SearchResultsResponse; initializeFederatedSummary(sourceId: string): { sourceId: string }; - onUpdateSummary(summary: object[]): object[]; + onUpdateSummary(summary: DocumentSummaryItem[]): DocumentSummaryItem[]; setContentFilterValue(contentFilterValue: string): string; setActivePage(activePage: number): number; setClientIdValue(clientIdValue: string): string; @@ -108,7 +114,7 @@ interface SourceValues { dataLoading: boolean; sectionLoading: boolean; buttonLoading: boolean; - contentItems: object[]; + contentItems: SourceContentItem[]; contentMeta: Meta; contentFilterValue: string; customSourceNameValue: string; @@ -129,7 +135,7 @@ interface SourceValues { } interface SearchResultsResponse { - results: object[]; + results: SourceContentItem[]; meta: Meta; } diff --git a/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts b/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts index 11d4a387b533f..b9bd111a22ca6 100644 --- a/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts @@ -10,6 +10,19 @@ jest.mock('./enterprise_search_config_api', () => ({ import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api'; import { checkAccess } from './check_access'; +import { spacesMock } from '../../../spaces/server/mocks'; + +const enabledSpace = { + id: 'space', + name: 'space', + disabledFeatures: [], +}; + +const disabledSpace = { + id: 'space', + name: 'space', + disabledFeatures: ['enterpriseSearch'], +}; describe('checkAccess', () => { const mockSecurity = { @@ -29,100 +42,156 @@ describe('checkAccess', () => { }, }, }; + const mockSpaces = spacesMock.createStart(); const mockDependencies = { - request: {}, + request: { auth: { isAuthenticated: true } }, config: { host: 'http://localhost:3002' }, security: mockSecurity, + spaces: mockSpaces, } as any; - describe('when security is disabled', () => { - it('should allow all access', async () => { - const security = undefined; - expect(await checkAccess({ ...mockDependencies, security })).toEqual({ - hasAppSearchAccess: true, - hasWorkplaceSearchAccess: true, + describe('when the space is disabled', () => { + it('should deny all access', async () => { + mockSpaces.spacesService.getActiveSpace.mockResolvedValueOnce(disabledSpace); + expect(await checkAccess({ ...mockDependencies })).toEqual({ + hasAppSearchAccess: false, + hasWorkplaceSearchAccess: false, }); }); }); - describe('when the user is a superuser', () => { - it('should allow all access', async () => { - const security = { - ...mockSecurity, - authz: { - mode: { useRbacForRequest: () => true }, - checkPrivilegesWithRequest: () => ({ - globally: () => ({ - hasAllRequested: true, - }), - }), - actions: { ui: { get: () => {} } }, - }, - }; - expect(await checkAccess({ ...mockDependencies, security })).toEqual({ - hasAppSearchAccess: true, - hasWorkplaceSearchAccess: true, + describe('when the spaces plugin is unavailable', () => { + describe('when security is disabled', () => { + it('should allow all access', async () => { + const spaces = undefined; + const security = undefined; + expect(await checkAccess({ ...mockDependencies, spaces, security })).toEqual({ + hasAppSearchAccess: true, + hasWorkplaceSearchAccess: true, + }); }); }); - it('falls back to assuming a non-superuser role if auth credentials are missing', async () => { - const security = { - authz: { - ...mockSecurity.authz, - checkPrivilegesWithRequest: () => ({ - globally: () => Promise.reject({ statusCode: 403 }), - }), - }, - }; - expect(await checkAccess({ ...mockDependencies, security })).toEqual({ - hasAppSearchAccess: false, - hasWorkplaceSearchAccess: false, + describe('when getActiveSpace returns 403 forbidden', () => { + it('should deny all access', async () => { + mockSpaces.spacesService.getActiveSpace.mockReturnValueOnce( + Promise.reject({ output: { statusCode: 403 } }) + ); + expect(await checkAccess({ ...mockDependencies })).toEqual({ + hasAppSearchAccess: false, + hasWorkplaceSearchAccess: false, + }); }); }); - it('throws other authz errors', async () => { - const security = { - authz: { - ...mockSecurity.authz, - checkPrivilegesWithRequest: undefined, - }, - }; - await expect(checkAccess({ ...mockDependencies, security })).rejects.toThrow(); + describe('when getActiveSpace throws', () => { + it('should re-throw', async () => { + mockSpaces.spacesService.getActiveSpace.mockReturnValueOnce(Promise.reject('Error')); + let expectedError = ''; + try { + await checkAccess({ ...mockDependencies }); + } catch (e) { + expectedError = e; + } + expect(expectedError).toEqual('Error'); + }); }); }); - describe('when the user is a non-superuser', () => { - describe('when enterpriseSearch.host is not set in kibana.yml', () => { - it('should deny all access', async () => { - const config = { host: undefined }; - expect(await checkAccess({ ...mockDependencies, config })).toEqual({ - hasAppSearchAccess: false, - hasWorkplaceSearchAccess: false, + describe('when the space is enabled', () => { + beforeEach(() => { + mockSpaces.spacesService.getActiveSpace.mockResolvedValueOnce(enabledSpace); + }); + + describe('when security is disabled', () => { + it('should allow all access', async () => { + const security = undefined; + expect(await checkAccess({ ...mockDependencies, security })).toEqual({ + hasAppSearchAccess: true, + hasWorkplaceSearchAccess: true, }); }); }); - describe('when enterpriseSearch.host is set in kibana.yml', () => { - it('should make a http call and return the access response', async () => { - (callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => ({ - access: { - hasAppSearchAccess: false, - hasWorkplaceSearchAccess: true, + describe('when the user is a superuser', () => { + it('should allow all access when enabled at the space ', async () => { + const security = { + ...mockSecurity, + authz: { + mode: { useRbacForRequest: () => true }, + checkPrivilegesWithRequest: () => ({ + globally: () => ({ + hasAllRequested: true, + }), + }), + actions: { ui: { get: () => {} } }, }, - })); - expect(await checkAccess(mockDependencies)).toEqual({ - hasAppSearchAccess: false, + }; + expect(await checkAccess({ ...mockDependencies, security })).toEqual({ + hasAppSearchAccess: true, hasWorkplaceSearchAccess: true, }); }); - it('falls back to no access if no http response', async () => { - (callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => ({})); - expect(await checkAccess(mockDependencies)).toEqual({ + it('falls back to assuming a non-superuser role if auth credentials are missing', async () => { + const security = { + authz: { + ...mockSecurity.authz, + checkPrivilegesWithRequest: () => ({ + globally: () => Promise.reject({ statusCode: 403 }), + }), + }, + }; + expect(await checkAccess({ ...mockDependencies, security })).toEqual({ hasAppSearchAccess: false, hasWorkplaceSearchAccess: false, }); }); + + it('throws other authz errors', async () => { + const security = { + authz: { + ...mockSecurity.authz, + checkPrivilegesWithRequest: undefined, + }, + }; + await expect(checkAccess({ ...mockDependencies, security })).rejects.toThrow(); + }); + }); + + describe('when the user is a non-superuser', () => { + describe('when enterpriseSearch.host is not set in kibana.yml', () => { + it('should deny all access', async () => { + const config = { host: undefined }; + expect(await checkAccess({ ...mockDependencies, config })).toEqual({ + hasAppSearchAccess: false, + hasWorkplaceSearchAccess: false, + }); + }); + }); + + describe('when enterpriseSearch.host is set in kibana.yml', () => { + it('should make a http call and return the access response', async () => { + (callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => ({ + access: { + hasAppSearchAccess: false, + hasWorkplaceSearchAccess: true, + }, + })); + expect(await checkAccess(mockDependencies)).toEqual({ + hasAppSearchAccess: false, + hasWorkplaceSearchAccess: true, + }); + }); + + it('falls back to no access if no http response', async () => { + (callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => ({})); + expect(await checkAccess(mockDependencies)).toEqual({ + hasAppSearchAccess: false, + hasWorkplaceSearchAccess: false, + }); + }); + }); }); }); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/check_access.ts b/x-pack/plugins/enterprise_search/server/lib/check_access.ts index 8b32260bb7322..b5a05a57f5e93 100644 --- a/x-pack/plugins/enterprise_search/server/lib/check_access.ts +++ b/x-pack/plugins/enterprise_search/server/lib/check_access.ts @@ -5,6 +5,7 @@ */ import { KibanaRequest, Logger } from 'src/core/server'; +import { SpacesPluginStart } from '../../../spaces/server'; import { SecurityPluginSetup } from '../../../security/server'; import { ConfigType } from '../'; @@ -13,6 +14,7 @@ import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api'; interface CheckAccess { request: KibanaRequest; security?: SecurityPluginSetup; + spaces?: SpacesPluginStart; config: ConfigType; log: Logger; } @@ -38,20 +40,53 @@ const DENY_ALL_PLUGINS = { export const checkAccess = async ({ config, security, + spaces, request, log, }: CheckAccess): Promise => { + const isRbacEnabled = security?.authz.mode.useRbacForRequest(request) ?? false; + + // We can only retrieve the active space when either: + // 1) security is enabled, and the request has already been authenticated + // 2) security is disabled + const attemptSpaceRetrieval = !isRbacEnabled || request.auth.isAuthenticated; + + // If we can't retrieve the current space, then assume the feature is available + let allowedAtSpace = false; + + if (!spaces) { + allowedAtSpace = true; + } + + if (spaces && attemptSpaceRetrieval) { + try { + const space = await spaces.spacesService.getActiveSpace(request); + allowedAtSpace = !space.disabledFeatures?.includes('enterpriseSearch'); + } catch (err) { + if (err?.output?.statusCode === 403) { + allowedAtSpace = false; + } else { + throw err; + } + } + } + + // Hide the plugin if turned off in the current space. + if (!allowedAtSpace) { + return DENY_ALL_PLUGINS; + } + // If security has been disabled, always show the plugin - if (!security?.authz.mode.useRbacForRequest(request)) { + if (!isRbacEnabled) { return ALLOW_ALL_PLUGINS; } // If the user is a "superuser" or has the base Kibana all privilege globally, always show the plugin const isSuperUser = async (): Promise => { try { - const { hasAllRequested } = await security.authz + const { hasAllRequested } = await security!.authz .checkPrivilegesWithRequest(request) - .globally({ kibana: security.authz.actions.ui.get('enterpriseSearch', 'all') }); + .globally({ kibana: security!.authz.actions.ui.get('enterpriseSearch', 'all') }); return hasAllRequested; } catch (err) { if (err.statusCode === 401 || err.statusCode === 403) { diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index d8f23674844b8..2d3b27783e3a1 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -16,6 +16,7 @@ import { KibanaRequest, } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { SpacesPluginStart } from '../../spaces/server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { SecurityPluginSetup } from '../../security/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; @@ -51,6 +52,10 @@ interface PluginsSetup { features: FeaturesPluginSetup; } +interface PluginsStart { + spaces?: SpacesPluginStart; +} + export interface RouteDependencies { router: IRouter; config: ConfigType; @@ -69,7 +74,7 @@ export class EnterpriseSearchPlugin implements Plugin { } public async setup( - { capabilities, http, savedObjects, getStartServices }: CoreSetup, + { capabilities, http, savedObjects, getStartServices }: CoreSetup, { usageCollection, security, features }: PluginsSetup ) { const config = await this.config.pipe(first()).toPromise(); @@ -97,7 +102,9 @@ export class EnterpriseSearchPlugin implements Plugin { * Register user access to the Enterprise Search plugins */ capabilities.registerSwitcher(async (request: KibanaRequest) => { - const dependencies = { config, security, request, log }; + const [, { spaces }] = await getStartServices(); + + const dependencies = { config, security, spaces, request, log }; const { hasAppSearchAccess, hasWorkplaceSearchAccess } = await checkAccess(dependencies); const showEnterpriseSearchOverview = hasAppSearchAccess || hasWorkplaceSearchAccess; diff --git a/x-pack/plugins/event_log/server/event_log_client.ts b/x-pack/plugins/event_log/server/event_log_client.ts index b7de4acb9428c..9b7d4e00b2761 100644 --- a/x-pack/plugins/event_log/server/event_log_client.ts +++ b/x-pack/plugins/event_log/server/event_log_client.ts @@ -7,7 +7,7 @@ import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; import { LegacyClusterClient, KibanaRequest } from 'src/core/server'; -import { SpacesServiceSetup } from '../../spaces/server'; +import { SpacesServiceStart } from '../../spaces/server'; import { EsContext } from './es'; import { IEventLogClient } from './types'; @@ -60,7 +60,7 @@ export type FindOptionsType = Pick< interface EventLogServiceCtorParams { esContext: EsContext; savedObjectGetter: SavedObjectGetter; - spacesService?: SpacesServiceSetup; + spacesService?: SpacesServiceStart; request: KibanaRequest; } @@ -68,7 +68,7 @@ interface EventLogServiceCtorParams { export class EventLogClient implements IEventLogClient { private esContext: EsContext; private savedObjectGetter: SavedObjectGetter; - private spacesService?: SpacesServiceSetup; + private spacesService?: SpacesServiceStart; private request: KibanaRequest; constructor({ esContext, savedObjectGetter, spacesService, request }: EventLogServiceCtorParams) { diff --git a/x-pack/plugins/event_log/server/event_log_start_service.ts b/x-pack/plugins/event_log/server/event_log_start_service.ts index 5cadab4df3ed7..51dd7d6e95d15 100644 --- a/x-pack/plugins/event_log/server/event_log_start_service.ts +++ b/x-pack/plugins/event_log/server/event_log_start_service.ts @@ -6,7 +6,7 @@ import { Observable } from 'rxjs'; import { LegacyClusterClient, KibanaRequest } from 'src/core/server'; -import { SpacesServiceSetup } from '../../spaces/server'; +import { SpacesServiceStart } from '../../spaces/server'; import { EsContext } from './es'; import { IEventLogClientService } from './types'; @@ -18,14 +18,14 @@ export type AdminClusterClient$ = Observable; interface EventLogServiceCtorParams { esContext: EsContext; savedObjectProviderRegistry: SavedObjectProviderRegistry; - spacesService?: SpacesServiceSetup; + spacesService?: SpacesServiceStart; } // note that clusterClient may be null, indicating we can't write to ES export class EventLogClientService implements IEventLogClientService { private esContext: EsContext; private savedObjectProviderRegistry: SavedObjectProviderRegistry; - private spacesService?: SpacesServiceSetup; + private spacesService?: SpacesServiceStart; constructor({ esContext, diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index 4439a4fb9fdbb..f69850f166aee 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -17,7 +17,7 @@ import { IContextProvider, RequestHandler, } from 'src/core/server'; -import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server'; +import { SpacesPluginStart } from '../../spaces/server'; import { IEventLogConfig, @@ -41,8 +41,8 @@ const ACTIONS = { stopping: 'stopping', }; -interface PluginSetupDeps { - spaces?: SpacesPluginSetup; +interface PluginStartDeps { + spaces?: SpacesPluginStart; } export class Plugin implements CorePlugin { @@ -53,7 +53,6 @@ export class Plugin implements CorePlugin; private eventLogClientService?: EventLogClientService; - private spacesService?: SpacesServiceSetup; private savedObjectProviderRegistry: SavedObjectProviderRegistry; constructor(private readonly context: PluginInitializerContext) { @@ -63,14 +62,13 @@ export class Plugin implements CorePlugin { + async setup(core: CoreSetup): Promise { const globalConfig = await this.globalConfig$.pipe(first()).toPromise(); const kibanaIndex = globalConfig.kibana.index; this.systemLogger.debug('setting up plugin'); const config = await this.config$.pipe(first()).toPromise(); - this.spacesService = spaces?.spacesService; this.esContext = createEsContext({ logger: this.systemLogger, @@ -105,7 +103,7 @@ export class Plugin implements CorePlugin { + async start(core: CoreStart, { spaces }: PluginStartDeps): Promise { this.systemLogger.debug('starting plugin'); if (!this.esContext) throw new Error('esContext not initialized'); @@ -131,7 +129,7 @@ export class Plugin implements CorePlugin = ({ ...suggestion, // For type onClick: () => {}, + descriptionDisplay: 'wrap', + labelWidth: '40', }; })} /> diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 47692d478b760..e4ed386802c3a 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -235,7 +235,7 @@ export class FleetPlugin if (isESOUsingEphemeralEncryptionKey) { if (this.logger) { this.logger.warn( - 'Fleet APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.' + 'Fleet APIs are disabled because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' ); } } else { diff --git a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts index 4574bcc64d4ce..2f08846642985 100644 --- a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts @@ -25,7 +25,6 @@ describe('test actions handlers schema', () => { NewAgentActionSchema.validate({ type: 'POLICY_CHANGE', data: 'data', - sent_at: '2020-03-14T19:45:02.620Z', }) ).toBeTruthy(); }); @@ -34,7 +33,6 @@ describe('test actions handlers schema', () => { expect(() => { NewAgentActionSchema.validate({ data: 'data', - sent_at: '2020-03-14T19:45:02.620Z', }); }).toThrowError(); }); @@ -55,7 +53,6 @@ describe('test actions handlers', () => { action: { type: 'POLICY_CHANGE', data: 'data', - sent_at: '2020-03-14T19:45:02.620Z', }, }, params: { diff --git a/x-pack/plugins/fleet/server/types/models/agent.ts b/x-pack/plugins/fleet/server/types/models/agent.ts index 98ed793604954..619c21d8bf5d9 100644 --- a/x-pack/plugins/fleet/server/types/models/agent.ts +++ b/x-pack/plugins/fleet/server/types/models/agent.ts @@ -62,14 +62,26 @@ export const AgentEventSchema = schema.object({ id: schema.string(), }); -export const NewAgentActionSchema = schema.object({ - type: schema.oneOf([ - schema.literal('POLICY_CHANGE'), - schema.literal('UNENROLL'), - schema.literal('UPGRADE'), - schema.literal('INTERNAL_POLICY_REASSIGN'), - ]), - data: schema.maybe(schema.any()), - ack_data: schema.maybe(schema.any()), - sent_at: schema.maybe(schema.string()), -}); +export const NewAgentActionSchema = schema.oneOf([ + schema.object({ + type: schema.oneOf([ + schema.literal('POLICY_CHANGE'), + schema.literal('UNENROLL'), + schema.literal('UPGRADE'), + schema.literal('INTERNAL_POLICY_REASSIGN'), + ]), + data: schema.maybe(schema.any()), + ack_data: schema.maybe(schema.any()), + }), + schema.object({ + type: schema.oneOf([schema.literal('SETTINGS')]), + data: schema.object({ + log_level: schema.oneOf([ + schema.literal('debug'), + schema.literal('info'), + schema.literal('warning'), + schema.literal('error'), + ]), + }), + }), +]); 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 00c7d705c1f44..68b2ac59d2a19 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 @@ -195,3 +195,41 @@ export const POLICY_WITH_NODE_ROLE_ALLOCATION: PolicyFromES = { }, name: POLICY_NAME, }; + +export const POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS = ({ + version: 1, + modified_date: Date.now().toString(), + policy: { + foo: 'bar', + phases: { + hot: { + min_age: '0ms', + actions: { + rollover: { + unknown_setting: 123, + max_size: '50gb', + }, + }, + }, + warm: { + actions: { + my_unfollow_action: {}, + set_priority: { + priority: 22, + unknown_setting: true, + }, + }, + }, + delete: { + wait_for_snapshot: { + policy: SNAPSHOT_POLICY_NAME, + }, + delete: { + delete_searchable_snapshot: true, + }, + }, + }, + name: POLICY_NAME, + }, + name: POLICY_NAME, +} as any) as PolicyFromES; 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 c91ee3e2a1c06..a203a434bb21a 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 @@ -19,6 +19,7 @@ import { POLICY_WITH_INCLUDE_EXCLUDE, POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION, POLICY_WITH_NODE_ROLE_ALLOCATION, + POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS, getDefaultHotPhasePolicy, } from './constants'; @@ -31,6 +32,70 @@ describe('', () => { server.restore(); }); + describe('serialization', () => { + /** + * We assume that policies that populate this form are loaded directly from ES and so + * are valid according to ES. There may be settings in the policy created through the ILM + * API that the UI does not cater for, like the unfollow action. We do not want to overwrite + * the configuration for these actions in the UI. + */ + it('preserves policy settings it did not configure', async () => { + httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + + // Set max docs to test whether we keep the unknown fields in that object after serializing + await actions.hot.setMaxDocs('1000'); + // Remove the delete phase to ensure that we also correctly remove data + await actions.delete.enable(false); + await actions.savePolicy(); + + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + + expect(entirePolicy).toEqual({ + foo: 'bar', // Made up value + name: 'my_policy', + phases: { + hot: { + actions: { + rollover: { + max_docs: 1000, + max_size: '50gb', + unknown_setting: 123, // Made up setting that should stay preserved + }, + set_priority: { + priority: 100, + }, + }, + min_age: '0ms', + }, + warm: { + actions: { + my_unfollow_action: {}, // Made up action + set_priority: { + priority: 22, + unknown_setting: true, + }, + }, + min_age: '0ms', + }, + }, + }); + }); + }); + describe('hot phase', () => { describe('serialization', () => { beforeEach(async () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx index 3e1577d8033ba..eb17402a46950 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx @@ -298,12 +298,12 @@ describe('edit policy', () => { phases: { hot: { actions: { - set_priority: { - priority: 100, - }, rollover: { - max_size: '50gb', max_age: '30d', + max_size: '50gb', + }, + set_priority: { + priority: 100, }, }, min_age: '0ms', diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts index 5af8807f2dec8..df5d6e2f80c15 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts @@ -22,13 +22,11 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => { const _meta: FormInternal['_meta'] = { hot: { useRollover: Boolean(hot?.actions?.rollover), - forceMergeEnabled: Boolean(hot?.actions?.forcemerge), bestCompression: hot?.actions?.forcemerge?.index_codec === 'best_compression', }, warm: { enabled: Boolean(warm), warmPhaseOnRollover: Boolean(warm?.min_age === '0ms'), - forceMergeEnabled: Boolean(warm?.actions?.forcemerge), bestCompression: warm?.actions?.forcemerge?.index_codec === 'best_compression', dataTierAllocationType: determineDataTierAllocationType(warm?.actions), }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts new file mode 100644 index 0000000000000..b379cb3956a02 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts @@ -0,0 +1,201 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { setAutoFreeze } from 'immer'; +import { cloneDeep } from 'lodash'; +import { SerializedPolicy } from '../../../../../common/types'; +import { deserializer } from './deserializer'; +import { createSerializer } from './serializer'; +import { FormInternal } from '../types'; + +const isObject = (v: unknown): v is { [key: string]: any } => + Object.prototype.toString.call(v) === '[object Object]'; + +const unknownValue = { some: 'value' }; + +const populateWithUnknownEntries = (v: unknown) => { + if (isObject(v)) { + for (const key of Object.keys(v)) { + if (['require', 'include', 'exclude'].includes(key)) continue; // this will generate an invalid policy + populateWithUnknownEntries(v[key]); + } + v.unknown = unknownValue; + return; + } + if (Array.isArray(v)) { + v.forEach(populateWithUnknownEntries); + } +}; + +const originalPolicy: SerializedPolicy = { + name: 'test', + phases: { + hot: { + actions: { + rollover: { + max_age: '1d', + max_size: '10gb', + max_docs: 1000, + }, + forcemerge: { + index_codec: 'best_compression', + max_num_segments: 22, + }, + set_priority: { + priority: 1, + }, + }, + min_age: '12ms', + }, + warm: { + min_age: '12ms', + actions: { + shrink: { number_of_shards: 12 }, + allocate: { + number_of_replicas: 3, + }, + set_priority: { + priority: 10, + }, + migrate: { enabled: false }, + }, + }, + cold: { + min_age: '30ms', + actions: { + allocate: { + number_of_replicas: 12, + require: { test: 'my_value' }, + include: { test: 'my_value' }, + exclude: { test: 'my_value' }, + }, + freeze: {}, + set_priority: { + priority: 12, + }, + }, + }, + delete: { + min_age: '33ms', + actions: { + delete: { + delete_searchable_snapshot: true, + }, + wait_for_snapshot: { + policy: 'test', + }, + }, + }, + }, +}; + +describe('deserializer and serializer', () => { + let policy: SerializedPolicy; + let serializer: ReturnType; + let formInternal: FormInternal; + + // So that we can modify produced form objects + beforeAll(() => setAutoFreeze(false)); + // This is the default in dev, so change back to true (https://github.com/immerjs/immer/blob/master/docs/freezing.md) + afterAll(() => setAutoFreeze(true)); + + beforeEach(() => { + policy = cloneDeep(originalPolicy); + formInternal = deserializer(policy); + // Because the policy object is not deepCloned by the form lib we + // clone here so that we can mutate the policy and preserve the + // original reference in the createSerializer + serializer = createSerializer(cloneDeep(policy)); + }); + + it('preserves any unknown policy settings', () => { + const thisTestPolicy = cloneDeep(originalPolicy); + // We populate all levels of the policy with entries our UI does not know about + populateWithUnknownEntries(thisTestPolicy); + serializer = createSerializer(thisTestPolicy); + + const copyOfThisTestPolicy = cloneDeep(thisTestPolicy); + + expect(serializer(deserializer(thisTestPolicy))).toEqual(thisTestPolicy); + + // Assert that the policy we passed in is unaltered after deserialization and serialization + expect(thisTestPolicy).not.toBe(copyOfThisTestPolicy); + expect(thisTestPolicy).toEqual(copyOfThisTestPolicy); + }); + + it('removes all phases if they were disabled in the form', () => { + formInternal._meta.warm.enabled = false; + formInternal._meta.cold.enabled = false; + formInternal._meta.delete.enabled = false; + + expect(serializer(formInternal)).toEqual({ + name: 'test', + phases: { + hot: policy.phases.hot, // We expect to see only the hot phase + }, + }); + }); + + it('removes the forcemerge action if it is disabled in the form', () => { + delete formInternal.phases.hot!.actions.forcemerge; + delete formInternal.phases.warm!.actions.forcemerge; + + const result = serializer(formInternal); + + expect(result.phases.hot!.actions.forcemerge).toBeUndefined(); + expect(result.phases.warm!.actions.forcemerge).toBeUndefined(); + }); + + it('removes set priority if it is disabled in the form', () => { + delete formInternal.phases.hot!.actions.set_priority; + delete formInternal.phases.warm!.actions.set_priority; + delete formInternal.phases.cold!.actions.set_priority; + + const result = serializer(formInternal); + + expect(result.phases.hot!.actions.set_priority).toBeUndefined(); + expect(result.phases.warm!.actions.set_priority).toBeUndefined(); + expect(result.phases.cold!.actions.set_priority).toBeUndefined(); + }); + + it('removes freeze setting in the cold phase if it is disabled in the form', () => { + formInternal._meta.cold.freezeEnabled = false; + + const result = serializer(formInternal); + + expect(result.phases.cold!.actions.freeze).toBeUndefined(); + }); + + it('removes node attribute allocation when it is not selected in the form', () => { + // Change from 'node_attrs' to 'node_roles' + formInternal._meta.cold.dataTierAllocationType = 'node_roles'; + + const result = serializer(formInternal); + + expect(result.phases.cold!.actions.allocate!.number_of_replicas).toBe(12); + expect(result.phases.cold!.actions.allocate!.require).toBeUndefined(); + expect(result.phases.cold!.actions.allocate!.include).toBeUndefined(); + expect(result.phases.cold!.actions.allocate!.exclude).toBeUndefined(); + }); + + it('removes forcemerge and rollover config when rollover is disabled in hot phase', () => { + formInternal._meta.hot.useRollover = false; + + const result = serializer(formInternal); + + expect(result.phases.hot!.actions.rollover).toBeUndefined(); + expect(result.phases.hot!.actions.forcemerge).toBeUndefined(); + }); + + it('removes min_age from warm when rollover is enabled', () => { + formInternal._meta.hot.useRollover = true; + formInternal._meta.warm.warmPhaseOnRollover = true; + + const result = serializer(formInternal); + + expect(result.phases.warm!.min_age).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index 4d20db4018740..0ad2d923117f4 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -23,7 +23,7 @@ import { i18nTexts } from '../i18n_texts'; const { emptyField, numberGreaterThanField } = fieldValidators; const serializers = { - stringToNumber: (v: string): any => (v ? parseInt(v, 10) : undefined), + stringToNumber: (v: string): any => (v != null ? parseInt(v, 10) : undefined), }; export const schema: FormSchema = { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer.ts deleted file mode 100644 index 2274efda426ad..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer.ts +++ /dev/null @@ -1,185 +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 { isEmpty, isNumber } from 'lodash'; - -import { SerializedPolicy, SerializedActionWithAllocation } from '../../../../../common/types'; - -import { FormInternal, DataAllocationMetaFields } from '../types'; - -const serializeAllocateAction = ( - { dataTierAllocationType, allocationNodeAttribute }: DataAllocationMetaFields, - newActions: SerializedActionWithAllocation = {}, - originalActions: SerializedActionWithAllocation = {} -): SerializedActionWithAllocation => { - const { allocate, migrate, ...rest } = newActions; - // First copy over all non-allocate and migrate actions. - const actions: SerializedActionWithAllocation = { allocate, migrate, ...rest }; - - switch (dataTierAllocationType) { - case 'node_attrs': - if (allocationNodeAttribute) { - const [name, value] = allocationNodeAttribute.split(':'); - actions.allocate = { - // copy over any other allocate details like "number_of_replicas" - ...actions.allocate, - require: { - [name]: value, - }, - }; - } else { - // The form has been configured to use node attribute based allocation but no node attribute - // was selected. We fall back to what was originally selected in this case. This might be - // migrate.enabled: "false" - actions.migrate = originalActions.migrate; - } - - // copy over the original include and exclude values until we can set them in the form. - if (!isEmpty(originalActions?.allocate?.include)) { - actions.allocate = { - ...actions.allocate, - include: { ...originalActions?.allocate?.include }, - }; - } - - if (!isEmpty(originalActions?.allocate?.exclude)) { - actions.allocate = { - ...actions.allocate, - exclude: { ...originalActions?.allocate?.exclude }, - }; - } - break; - case 'none': - actions.migrate = { enabled: false }; - break; - default: - } - return actions; -}; - -export const createSerializer = (originalPolicy?: SerializedPolicy) => ( - data: FormInternal -): SerializedPolicy => { - const { _meta, ...policy } = data; - - if (!policy.phases || !policy.phases.hot) { - policy.phases = { hot: { actions: {} } }; - } - - /** - * HOT PHASE SERIALIZATION - */ - if (policy.phases.hot) { - policy.phases.hot.min_age = originalPolicy?.phases.hot?.min_age ?? '0ms'; - } - - if (policy.phases.hot?.actions) { - if (policy.phases.hot.actions?.rollover && _meta.hot.useRollover) { - if (policy.phases.hot.actions.rollover.max_age) { - policy.phases.hot.actions.rollover.max_age = `${policy.phases.hot.actions.rollover.max_age}${_meta.hot.maxAgeUnit}`; - } - - if (policy.phases.hot.actions.rollover.max_size) { - policy.phases.hot.actions.rollover.max_size = `${policy.phases.hot.actions.rollover.max_size}${_meta.hot.maxStorageSizeUnit}`; - } - - if (_meta.hot.bestCompression && policy.phases.hot.actions?.forcemerge) { - policy.phases.hot.actions.forcemerge.index_codec = 'best_compression'; - } - } else { - delete policy.phases.hot.actions?.rollover; - } - } - - /** - * WARM PHASE SERIALIZATION - */ - if (policy.phases.warm) { - // 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 (_meta.hot.useRollover && _meta.warm.warmPhaseOnRollover) { - delete policy.phases.warm.min_age; - } else if ( - (!_meta.hot.useRollover || !_meta.warm.warmPhaseOnRollover) && - policy.phases.warm.min_age - ) { - policy.phases.warm.min_age = `${policy.phases.warm.min_age}${_meta.warm.minAgeUnit}`; - } - - policy.phases.warm.actions = serializeAllocateAction( - _meta.warm, - policy.phases.warm.actions, - originalPolicy?.phases.warm?.actions - ); - - if ( - policy.phases.warm.actions.allocate && - !policy.phases.warm.actions.allocate.require && - !isNumber(policy.phases.warm.actions.allocate.number_of_replicas) && - isEmpty(policy.phases.warm.actions.allocate.include) && - isEmpty(policy.phases.warm.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 policy.phases.warm.actions.allocate; - } - - if (_meta.warm.bestCompression && policy.phases.warm.actions?.forcemerge) { - policy.phases.warm.actions.forcemerge.index_codec = 'best_compression'; - } - } - - /** - * COLD PHASE SERIALIZATION - */ - if (policy.phases.cold) { - if (policy.phases.cold.min_age) { - policy.phases.cold.min_age = `${policy.phases.cold.min_age}${_meta.cold.minAgeUnit}`; - } - - policy.phases.cold.actions = serializeAllocateAction( - _meta.cold, - policy.phases.cold.actions, - originalPolicy?.phases.cold?.actions - ); - - if ( - policy.phases.cold.actions.allocate && - !policy.phases.cold.actions.allocate.require && - !isNumber(policy.phases.cold.actions.allocate.number_of_replicas) && - isEmpty(policy.phases.cold.actions.allocate.include) && - isEmpty(policy.phases.cold.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 policy.phases.cold.actions.allocate; - } - - if (_meta.cold.freezeEnabled) { - policy.phases.cold.actions.freeze = {}; - } - } - - /** - * DELETE PHASE SERIALIZATION - */ - if (policy.phases.delete) { - if (policy.phases.delete.min_age) { - policy.phases.delete.min_age = `${policy.phases.delete.min_age}${_meta.delete.minAgeUnit}`; - } - - if (originalPolicy?.phases.delete?.actions) { - const { wait_for_snapshot: __, ...rest } = originalPolicy.phases.delete.actions; - policy.phases.delete.actions = { - ...policy.phases.delete.actions, - ...rest, - }; - } - } - - return policy; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/index.ts new file mode 100644 index 0000000000000..f901bfcf4d49d --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/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 { createSerializer } from './serializer'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serialize_migrate_and_allocate_actions.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serialize_migrate_and_allocate_actions.ts new file mode 100644 index 0000000000000..d18a63d34c101 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serialize_migrate_and_allocate_actions.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 { isEmpty } from 'lodash'; + +import { SerializedActionWithAllocation } from '../../../../../../common/types'; + +import { DataAllocationMetaFields } from '../../types'; + +export const serializeMigrateAndAllocateActions = ( + { dataTierAllocationType, allocationNodeAttribute }: DataAllocationMetaFields, + newActions: SerializedActionWithAllocation = {}, + originalActions: SerializedActionWithAllocation = {} +): SerializedActionWithAllocation => { + const { allocate, migrate, ...otherActions } = newActions; + + // First copy over all non-allocate and migrate actions. + const actions: SerializedActionWithAllocation = { ...otherActions }; + + // The UI only knows about include, exclude and require, so copy over all other values. + if (allocate) { + const { include, exclude, require, ...otherSettings } = allocate; + if (!isEmpty(otherSettings)) { + actions.allocate = { ...otherSettings }; + } + } + + switch (dataTierAllocationType) { + case 'node_attrs': + if (allocationNodeAttribute) { + const [name, value] = allocationNodeAttribute.split(':'); + actions.allocate = { + // copy over any other allocate details like "number_of_replicas" + ...actions.allocate, + require: { + [name]: value, + }, + }; + } else { + // The form has been configured to use node attribute based allocation but no node attribute + // was selected. We fall back to what was originally selected in this case. This might be + // migrate.enabled: "false" + actions.migrate = originalActions.migrate; + } + + // copy over the original include and exclude values until we can set them in the form. + if (!isEmpty(originalActions?.allocate?.include)) { + actions.allocate = { + ...actions.allocate, + include: { ...originalActions?.allocate?.include }, + }; + } + + if (!isEmpty(originalActions?.allocate?.exclude)) { + actions.allocate = { + ...actions.allocate, + exclude: { ...originalActions?.allocate?.exclude }, + }; + } + break; + case 'none': + actions.migrate = { + ...originalActions?.migrate, + enabled: false, + }; + break; + default: + } + return actions; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts new file mode 100644 index 0000000000000..694f26abafe1d --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { produce } from 'immer'; + +import { merge } from 'lodash'; + +import { SerializedPolicy } from '../../../../../../common/types'; + +import { defaultPolicy } from '../../../../constants'; + +import { FormInternal } from '../../types'; + +import { serializeMigrateAndAllocateActions } from './serialize_migrate_and_allocate_actions'; + +export const createSerializer = (originalPolicy?: SerializedPolicy) => ( + data: FormInternal +): SerializedPolicy => { + const { _meta, ...updatedPolicy } = data; + + if (!updatedPolicy.phases || !updatedPolicy.phases.hot) { + updatedPolicy.phases = { hot: { actions: {} } }; + } + + return produce(originalPolicy ?? defaultPolicy, (draft) => { + // Copy over all updated fields + merge(draft, updatedPolicy); + + // Next copy over all meta fields and delete any fields that have been removed + // by fields exposed in the form. It is very important that we do not delete + // data that the form does not control! E.g., unfollow action in hot phase. + + /** + * HOT PHASE SERIALIZATION + */ + if (draft.phases.hot) { + draft.phases.hot.min_age = draft.phases.hot.min_age ?? '0ms'; + } + + if (draft.phases.hot?.actions) { + const hotPhaseActions = draft.phases.hot.actions; + if (hotPhaseActions.rollover && _meta.hot.useRollover) { + if (hotPhaseActions.rollover.max_age) { + hotPhaseActions.rollover.max_age = `${hotPhaseActions.rollover.max_age}${_meta.hot.maxAgeUnit}`; + } + + if (hotPhaseActions.rollover.max_size) { + hotPhaseActions.rollover.max_size = `${hotPhaseActions.rollover.max_size}${_meta.hot.maxStorageSizeUnit}`; + } + + if (!updatedPolicy.phases.hot!.actions?.forcemerge) { + delete hotPhaseActions.forcemerge; + } else if (_meta.hot.bestCompression) { + hotPhaseActions.forcemerge!.index_codec = 'best_compression'; + } + + if (_meta.hot.bestCompression && hotPhaseActions.forcemerge) { + hotPhaseActions.forcemerge.index_codec = 'best_compression'; + } + } else { + delete hotPhaseActions.rollover; + delete hotPhaseActions.forcemerge; + } + + if (!updatedPolicy.phases.hot!.actions?.set_priority) { + delete hotPhaseActions.set_priority; + } + } + + /** + * WARM PHASE SERIALIZATION + */ + if (_meta.warm.enabled) { + const warmPhase = draft.phases.warm!; + // 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 ( + (!_meta.hot.useRollover || !_meta.warm.warmPhaseOnRollover) && + updatedPolicy.phases.warm!.min_age + ) { + warmPhase.min_age = `${updatedPolicy.phases.warm!.min_age}${_meta.warm.minAgeUnit}`; + } else { + delete warmPhase.min_age; + } + + warmPhase.actions = serializeMigrateAndAllocateActions( + _meta.warm, + warmPhase.actions, + originalPolicy?.phases.warm?.actions + ); + + if (!updatedPolicy.phases.warm!.actions?.forcemerge) { + delete warmPhase.actions.forcemerge; + } else if (_meta.warm.bestCompression) { + warmPhase.actions.forcemerge!.index_codec = 'best_compression'; + } + + if (!updatedPolicy.phases.warm!.actions?.set_priority) { + delete warmPhase.actions.set_priority; + } + + if (!updatedPolicy.phases.warm!.actions?.shrink) { + delete warmPhase.actions.shrink; + } + } else { + delete draft.phases.warm; + } + + /** + * COLD PHASE SERIALIZATION + */ + if (_meta.cold.enabled) { + const coldPhase = draft.phases.cold!; + + if (updatedPolicy.phases.cold!.min_age) { + coldPhase.min_age = `${updatedPolicy.phases.cold!.min_age}${_meta.cold.minAgeUnit}`; + } + + coldPhase.actions = serializeMigrateAndAllocateActions( + _meta.cold, + coldPhase.actions, + originalPolicy?.phases.cold?.actions + ); + + if (_meta.cold.freezeEnabled) { + coldPhase.actions.freeze = coldPhase.actions.freeze ?? {}; + } else { + delete coldPhase.actions.freeze; + } + + if (!updatedPolicy.phases.cold!.actions?.set_priority) { + delete coldPhase.actions.set_priority; + } + } else { + delete draft.phases.cold; + } + + /** + * DELETE PHASE SERIALIZATION + */ + if (_meta.delete.enabled) { + const deletePhase = draft.phases.delete!; + if (updatedPolicy.phases.delete!.min_age) { + deletePhase.min_age = `${updatedPolicy.phases.delete!.min_age}${_meta.delete.minAgeUnit}`; + } + + if ( + !updatedPolicy.phases.delete!.actions?.wait_for_snapshot && + deletePhase.actions.wait_for_snapshot + ) { + delete deletePhase.actions.wait_for_snapshot; + } + } else { + delete draft.phases.delete; + } + }); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts index dc3d8a640e682..7d512936290af 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts @@ -18,7 +18,6 @@ export interface MinAgeField { } export interface ForcemergeFields { - forceMergeEnabled: boolean; bestCompression: boolean; } diff --git a/x-pack/plugins/infra/common/http_api/snapshot_api.ts b/x-pack/plugins/infra/common/http_api/snapshot_api.ts index a6273fa967baf..32fad61011c92 100644 --- a/x-pack/plugins/infra/common/http_api/snapshot_api.ts +++ b/x-pack/plugins/infra/common/http_api/snapshot_api.ts @@ -26,7 +26,7 @@ const SnapshotNodeMetricOptionalRT = rt.partial({ }); const SnapshotNodeMetricRequiredRT = rt.type({ - name: SnapshotMetricTypeRT, + name: rt.union([SnapshotMetricTypeRT, rt.string]), }); export const SnapshotNodeMetricRT = rt.intersection([ diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx index 8b2140aa196b3..0943ced5e5be0 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx @@ -11,7 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { euiStyled } from '../../../../../../../observability/public'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib'; import { InventoryItemType } from '../../../../../../common/inventory_models/types'; -import { MetricsTab } from './tabs/metrics'; +import { MetricsTab } from './tabs/metrics/metrics'; import { LogsTab } from './tabs/logs'; import { ProcessesTab } from './tabs/processes'; import { PropertiesTab } from './tabs/properties'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx index 1a8bc374e79a3..ce800a7d73700 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx @@ -4,14 +4,86 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { EuiFieldSearch } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiButtonEmpty } from '@elastic/eui'; import { TabContent, TabProps } from './shared'; +import { LogStream } from '../../../../../../components/log_stream'; +import { useWaffleOptionsContext } from '../../../hooks/use_waffle_options'; +import { findInventoryFields } from '../../../../../../../common/inventory_models'; +import { euiStyled } from '../../../../../../../../observability/public'; +import { useLinkProps } from '../../../../../../hooks/use_link_props'; +import { getNodeLogsUrl } from '../../../../../link_to'; const TabComponent = (props: TabProps) => { - return Logs Placeholder; + const [textQuery, setTextQuery] = useState(''); + const endTimestamp = props.currentTime; + const startTimestamp = endTimestamp - 60 * 60 * 1000; // 60 minutes + const { nodeType } = useWaffleOptionsContext(); + const { options, node } = props; + + const filter = useMemo(() => { + let query = options.fields + ? `${findInventoryFields(nodeType, options.fields).id}: "${node.id}"` + : ``; + + if (textQuery) { + query += ` and message: ${textQuery}`; + } + return query; + }, [options, nodeType, node.id, textQuery]); + + const onQueryChange = useCallback((e: React.ChangeEvent) => { + setTextQuery(e.target.value); + }, []); + + const nodeLogsMenuItemLinkProps = useLinkProps( + getNodeLogsUrl({ + nodeType, + nodeId: node.id, + time: startTimestamp, + }) + ); + + return ( + + + + + + + + + + + + + + + + ); }; +const QueryWrapper = euiStyled.div` + padding: ${(props) => props.theme.eui.paddingSizes.m}; + padding-right: 0; +`; + export const LogsTab = { id: 'logs', name: i18n.translate('xpack.infra.nodeDetails.tabs.logs', { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics.tsx deleted file mode 100644 index e329a5771c41d..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics.tsx +++ /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 React from 'react'; -import { i18n } from '@kbn/i18n'; -import { TabContent, TabProps } from './shared'; - -const TabComponent = (props: TabProps) => { - return Metrics Placeholder; -}; - -export const MetricsTab = { - id: 'metrics', - name: i18n.translate('xpack.infra.nodeDetails.tabs.metrics', { - defaultMessage: 'Metrics', - }), - content: TabComponent, -}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/chart_header.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/chart_header.tsx new file mode 100644 index 0000000000000..63004072c08d0 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/chart_header.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 { EuiText } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiIcon } from '@elastic/eui'; +import { colorTransformer } from '../../../../../../../../common/color_palette'; +import { MetricsExplorerOptionsMetric } from '../../../../../metrics_explorer/hooks/use_metrics_explorer_options'; +import { euiStyled } from '../../../../../../../../../observability/public'; + +interface Props { + title: string; + metrics: MetricsExplorerOptionsMetric[]; +} + +export const ChartHeader = ({ title, metrics }: Props) => { + return ( + + + + {title} + + + + + {metrics.map((chartMetric) => ( + + + + + + {chartMetric.label} + + + ))} + + + + ); +}; + +const ChartHeaderWrapper = euiStyled.div` + display: flex; + width: 100%; + padding: ${(props) => props.theme.eui.paddingSizes.s} ${(props) => + props.theme.eui.paddingSizes.m}; +`; diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/index.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/index.tsx similarity index 83% rename from x-pack/plugins/spaces/server/lib/spaces_client/index.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/index.tsx index 54c778ae3839e..88b76eb0ef775 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/index.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/index.tsx @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { SpacesClient } from './spaces_client'; +export * from './metrics'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx new file mode 100644 index 0000000000000..b5628b0a7c9b4 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx @@ -0,0 +1,476 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, useEffect, useMemo, useRef, useState } from 'react'; +import { first, last } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { + Axis, + Chart, + niceTimeFormatter, + Position, + Settings, + TooltipValue, + PointerEvent, +} from '@elastic/charts'; +import moment from 'moment'; +import { TabContent, TabProps } from '../shared'; +import { useSnapshot } from '../../../../hooks/use_snaphot'; +import { useWaffleOptionsContext } from '../../../../hooks/use_waffle_options'; +import { useSourceContext } from '../../../../../../../containers/source'; +import { findInventoryFields } from '../../../../../../../../common/inventory_models'; +import { convertKueryToElasticSearchQuery } from '../../../../../../../utils/kuery'; +import { SnapshotMetricType } from '../../../../../../../../common/inventory_models/types'; +import { + MetricsExplorerChartType, + MetricsExplorerOptionsMetric, +} from '../../../../../metrics_explorer/hooks/use_metrics_explorer_options'; +import { Color } from '../../../../../../../../common/color_palette'; +import { + MetricsExplorerAggregation, + MetricsExplorerSeries, +} from '../../../../../../../../common/http_api'; +import { MetricExplorerSeriesChart } from '../../../../../metrics_explorer/components/series_chart'; +import { createInventoryMetricFormatter } from '../../../../lib/create_inventory_metric_formatter'; +import { calculateDomain } from '../../../../../metrics_explorer/components/helpers/calculate_domain'; +import { getTimelineChartTheme } from '../../../../../metrics_explorer/components/helpers/get_chart_theme'; +import { useUiSetting } from '../../../../../../../../../../../src/plugins/kibana_react/public'; +import { euiStyled } from '../../../../../../../../../observability/public'; +import { ChartHeader } from './chart_header'; +import { + SYSTEM_METRIC_NAME, + USER_METRIC_NAME, + INBOUND_METRIC_NAME, + OUTBOUND_METRIC_NAME, + USED_MEMORY_METRIC_NAME, + FREE_MEMORY_METRIC_NAME, + CPU_CHART_TITLE, + LOAD_CHART_TITLE, + MEMORY_CHART_TITLE, + NETWORK_CHART_TITLE, +} from './translations'; +import { TimeDropdown } from './time_dropdown'; + +const ONE_HOUR = 60 * 60 * 1000; +const TabComponent = (props: TabProps) => { + const cpuChartRef = useRef(null); + const networkChartRef = useRef(null); + const memoryChartRef = useRef(null); + const loadChartRef = useRef(null); + const [time, setTime] = useState(ONE_HOUR); + const chartRefs = useMemo(() => [cpuChartRef, networkChartRef, memoryChartRef, loadChartRef], [ + cpuChartRef, + networkChartRef, + memoryChartRef, + loadChartRef, + ]); + const { sourceId, createDerivedIndexPattern } = useSourceContext(); + const { nodeType, accountId, region } = useWaffleOptionsContext(); + const { currentTime, options, node } = props; + const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ + createDerivedIndexPattern, + ]); + let filter = options.fields + ? `${findInventoryFields(nodeType, options.fields).id}: "${node.id}"` + : ''; + + if (filter) { + filter = convertKueryToElasticSearchQuery(filter, derivedIndexPattern); + } + + const buildCustomMetric = useCallback( + (field: string, id: string) => ({ + type: 'custom' as SnapshotMetricType, + aggregation: 'avg', + field, + id, + }), + [] + ); + + const updateTime = useCallback( + (e: React.ChangeEvent) => { + setTime(Number(e.currentTarget.value)); + }, + [setTime] + ); + + const { nodes, reload } = useSnapshot( + filter, + [ + { type: 'rx' }, + { type: 'tx' }, + buildCustomMetric('system.cpu.user.pct', 'user'), + buildCustomMetric('system.cpu.system.pct', 'system'), + buildCustomMetric('system.load.1', 'load1m'), + buildCustomMetric('system.load.5', 'load5m'), + buildCustomMetric('system.load.15', 'load15m'), + buildCustomMetric('system.memory.actual.used.bytes', 'usedMemory'), + buildCustomMetric('system.memory.actual.free', 'freeMemory'), + ], + [], + nodeType, + sourceId, + currentTime, + accountId, + region, + false, + { + interval: '1m', + to: currentTime, + from: currentTime - time, + ignoreLookback: true, + } + ); + + const getDomain = useCallback( + (timeseries: MetricsExplorerSeries, ms: MetricsExplorerOptionsMetric[]) => { + const dataDomain = timeseries ? calculateDomain(timeseries, ms, false) : null; + return dataDomain + ? { + max: dataDomain.max * 1.1, // add 10% headroom. + min: dataDomain.min, + } + : { max: 0, min: 0 }; + }, + [] + ); + + const dateFormatter = useCallback((timeseries: MetricsExplorerSeries) => { + if (!timeseries) return () => ''; + const firstTimestamp = first(timeseries.rows)?.timestamp; + const lastTimestamp = last(timeseries.rows)?.timestamp; + + if (firstTimestamp == null || lastTimestamp == null) { + return (value: number) => `${value}`; + } + + return niceTimeFormatter([firstTimestamp, lastTimestamp]); + }, []); + + const networkFormatter = useMemo(() => createInventoryMetricFormatter({ type: 'rx' }), []); + const cpuFormatter = useMemo(() => createInventoryMetricFormatter({ type: 'cpu' }), []); + const memoryFormatter = useMemo( + () => createInventoryMetricFormatter({ type: 's3BucketSize' }), + [] + ); + const loadFormatter = useMemo(() => createInventoryMetricFormatter({ type: 'load' }), []); + + const mergeTimeseries = useCallback((...series: MetricsExplorerSeries[]) => { + const base = series[0]; + const otherSeries = series.slice(1); + base.rows = base.rows.map((b, rowIdx) => { + const newRow = { ...b }; + otherSeries.forEach((o, idx) => { + newRow[`metric_${idx + 1}`] = o.rows[rowIdx].metric_0; + }); + return newRow; + }); + return base; + }, []); + + const buildChartMetricLabels = useCallback( + (labels: string[], aggregation: MetricsExplorerAggregation) => { + const baseMetric = { + color: Color.color0, + aggregation, + label: 'System', + }; + + return labels.map((label, idx) => { + return { ...baseMetric, color: Color[`color${idx}` as Color], label }; + }); + }, + [] + ); + + const pointerUpdate = useCallback( + (event: PointerEvent) => { + chartRefs.forEach((ref) => { + if (ref.current) { + ref.current.dispatchExternalPointerEvent(event); + } + }); + }, + [chartRefs] + ); + + const isDarkMode = useUiSetting('theme:darkMode'); + const tooltipProps = { + headerFormatter: (tooltipValue: TooltipValue) => + moment(tooltipValue.value).format('Y-MM-DD HH:mm:ss.SSS'), + }; + + const getTimeseries = useCallback( + (metricName: string) => { + if (!nodes || !nodes.length) { + return null; + } + return nodes[0].metrics.find((m) => m.name === metricName)!.timeseries!; + }, + [nodes] + ); + + const systemMetricsTs = useMemo(() => getTimeseries('system'), [getTimeseries]); + const userMetricsTs = useMemo(() => getTimeseries('user'), [getTimeseries]); + const rxMetricsTs = useMemo(() => getTimeseries('rx'), [getTimeseries]); + const txMetricsTs = useMemo(() => getTimeseries('tx'), [getTimeseries]); + const load1mMetricsTs = useMemo(() => getTimeseries('load1m'), [getTimeseries]); + const load5mMetricsTs = useMemo(() => getTimeseries('load5m'), [getTimeseries]); + const load15mMetricsTs = useMemo(() => getTimeseries('load15m'), [getTimeseries]); + const usedMemoryMetricsTs = useMemo(() => getTimeseries('usedMemory'), [getTimeseries]); + const freeMemoryMetricsTs = useMemo(() => getTimeseries('freeMemory'), [getTimeseries]); + + useEffect(() => { + reload(); + }, [time, reload]); + + if ( + !systemMetricsTs || + !userMetricsTs || + !rxMetricsTs || + !txMetricsTs || + !load1mMetricsTs || + !load5mMetricsTs || + !load15mMetricsTs || + !usedMemoryMetricsTs || + !freeMemoryMetricsTs + ) { + return
; + } + + const cpuChartMetrics = buildChartMetricLabels([SYSTEM_METRIC_NAME, USER_METRIC_NAME], 'avg'); + const networkChartMetrics = buildChartMetricLabels( + [INBOUND_METRIC_NAME, OUTBOUND_METRIC_NAME], + 'rate' + ); + const loadChartMetrics = buildChartMetricLabels(['1m', '5m', '15m'], 'avg'); + const memoryChartMetrics = buildChartMetricLabels( + [USED_MEMORY_METRIC_NAME, FREE_MEMORY_METRIC_NAME], + 'rate' + ); + + const cpuTimeseries = mergeTimeseries(systemMetricsTs, userMetricsTs); + const networkTimeseries = mergeTimeseries(rxMetricsTs, txMetricsTs); + const loadTimeseries = mergeTimeseries(load1mMetricsTs, load5mMetricsTs, load15mMetricsTs); + const memoryTimeseries = mergeTimeseries(usedMemoryMetricsTs, freeMemoryMetricsTs); + + const formatter = dateFormatter(rxMetricsTs); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const ChartsContainer = euiStyled.div` + display: flex; + flex-direction: row; + flex-wrap: wrap; +`; + +const ChartContainerWrapper = euiStyled.div` + width: 50% +`; + +const TimepickerWrapper = euiStyled.div` + padding: ${(props) => props.theme.eui.paddingSizes.m}; + width: 50%; +`; + +const ChartContainer: React.FC = ({ children }) => ( +
+ {children} +
+); + +export const MetricsTab = { + id: 'metrics', + name: i18n.translate('xpack.infra.nodeDetails.tabs.metrics', { + defaultMessage: 'Metrics', + }), + content: TabComponent, +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/time_dropdown.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/time_dropdown.tsx new file mode 100644 index 0000000000000..00441e520c90a --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/time_dropdown.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { EuiSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface Props { + value: number; + onChange(event: React.ChangeEvent): void; +} + +export const TimeDropdown = (props: Props) => ( + +); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/translations.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/translations.tsx new file mode 100644 index 0000000000000..90589fc71d9a4 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/translations.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 SYSTEM_METRIC_NAME = i18n.translate('xpack.infra.nodeDetails.metrics.system', { + defaultMessage: 'System', +}); + +export const USER_METRIC_NAME = i18n.translate('xpack.infra.nodeDetails.metrics.user', { + defaultMessage: 'User', +}); + +export const INBOUND_METRIC_NAME = i18n.translate('xpack.infra.nodeDetails.metrics.inbound', { + defaultMessage: 'Inbound', +}); + +export const OUTBOUND_METRIC_NAME = i18n.translate('xpack.infra.nodeDetails.metrics.outbound', { + defaultMessage: 'Outbound', +}); + +export const USED_MEMORY_METRIC_NAME = i18n.translate('xpack.infra.nodeDetails.metrics.used', { + defaultMessage: 'Used', +}); + +export const CACHED_MEMORY_METRIC_NAME = i18n.translate('xpack.infra.nodeDetails.metrics.cached', { + defaultMessage: 'Cached', +}); + +export const FREE_MEMORY_METRIC_NAME = i18n.translate('xpack.infra.nodeDetails.metrics.free', { + defaultMessage: 'Free', +}); + +export const NETWORK_CHART_TITLE = i18n.translate( + 'xpack.infra.nodeDetails.metrics.charts.networkTitle', + { + defaultMessage: 'Network', + } +); +export const MEMORY_CHART_TITLE = i18n.translate( + 'xpack.infra.nodeDetails.metrics.charts.memoryTitle', + { + defaultMessage: 'Memory', + } +); +export const CPU_CHART_TITLE = i18n.translate('xpack.infra.nodeDetails.metrics.fcharts.cpuTitle', { + defaultMessage: 'CPU', +}); +export const LOAD_CHART_TITLE = i18n.translate('xpack.infra.nodeDetails.metrics.charts.loadTitle', { + defaultMessage: 'Load', +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx index 11f27f6401a31..8082752a88b7f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx @@ -12,6 +12,7 @@ import { findInventoryModel } from '../../../../../../common/inventory_models'; import { InventoryItemType, SnapshotMetricType, + SnapshotMetricTypeRT, } from '../../../../../../common/inventory_models/types'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib'; import { useSnapshot } from '../../hooks/use_snaphot'; @@ -88,8 +89,9 @@ export const ConditionalToolTip = withTheme( {node.name}
{metrics.map((metric) => { - const name = SNAPSHOT_METRIC_TRANSLATIONS[metric.name] || metric.name; - const formatter = createInventoryMetricFormatter({ type: metric.name }); + const metricName = SnapshotMetricTypeRT.is(metric.name) ? metric.name : 'custom'; + const name = SNAPSHOT_METRIC_TRANSLATIONS[metricName] || metricName; + const formatter = createInventoryMetricFormatter({ type: metricName }); return ( {name} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts index eec46b0486287..4cfa8871b0dcc 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts @@ -31,7 +31,8 @@ export function useSnapshot( currentTime: number, accountId: string, region: string, - sendRequestImmediatly = true + sendRequestImmediatly = true, + timerange?: InfraTimerangeInput ) { const decodeResponse = (response: any) => { return pipe( @@ -40,7 +41,7 @@ export function useSnapshot( ); }; - const timerange: InfraTimerangeInput = { + timerange = timerange || { interval: '1m', to: currentTime, from: currentTime - 1200 * 1000, diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_snapshot_metrics_to_metrics_api_metrics.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_snapshot_metrics_to_metrics_api_metrics.ts index 6f7c88eda5d7a..50c53b27cd50f 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_snapshot_metrics_to_metrics_api_metrics.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_snapshot_metrics_to_metrics_api_metrics.ts @@ -18,7 +18,10 @@ export const transformSnapshotMetricsToMetricsAPIMetrics = ( return snapshotRequest.metrics.map((metric, index) => { const inventoryModel = findInventoryModel(snapshotRequest.nodeType); if (SnapshotCustomMetricInputRT.is(metric)) { - const customId = `custom_${index}`; + const isUniqueId = snapshotRequest.metrics.findIndex((m) => + SnapshotCustomMetricInputRT.is(m) ? m.id === metric.id : false + ); + const customId = isUniqueId ? metric.id : `custom_${index}`; if (metric.aggregation === 'rate') { return { id: customId, aggregations: networkTraffic(customId, metric.field) }; } diff --git a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts index 958d5ae250185..7eef86869b9e5 100644 --- a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts @@ -16,7 +16,7 @@ export const JOB_MAP_NODE_TYPES = { ANALYTICS: 'analytics', TRANSFORM: 'transform', INDEX: 'index', - INFERENCE_MODEL: 'inferenceModel', + TRAINED_MODEL: 'trainedModel', } as const; export type JobMapNodeTypes = typeof JOB_MAP_NODE_TYPES[keyof typeof JOB_MAP_NODE_TYPES]; diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts index 9a3d8fc4a4f02..b5a78ee746efe 100644 --- a/x-pack/plugins/ml/common/types/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts @@ -156,6 +156,7 @@ export type TimeSeriesExplorerUrlState = MLPageState< export interface DataFrameAnalyticsQueryState { jobId?: JobId | JobId[]; + modelId?: string; groupIds?: string[]; globalState?: MlCommonGlobalState; } @@ -170,6 +171,7 @@ export interface DataFrameAnalyticsExplorationQueryState { jobId: JobId; analysisType: DataFrameAnalysisConfigType; defaultIsTraining?: boolean; + modelId?: string; }; } @@ -180,6 +182,7 @@ export type DataFrameAnalyticsExplorationUrlState = MLPageState< analysisType: DataFrameAnalysisConfigType; globalState?: MlCommonGlobalState; defaultIsTraining?: boolean; + modelId?: string; } >; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx index a5d3555fcc278..bf90ce58fb85d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx @@ -15,10 +15,11 @@ interface Tab { path: string; } -export const AnalyticsNavigationBar: FC<{ selectedTabId?: string; jobId?: string }> = ({ - jobId, - selectedTabId, -}) => { +export const AnalyticsNavigationBar: FC<{ + selectedTabId?: string; + jobId?: string; + modelId?: string; +}> = ({ jobId, modelId, selectedTabId }) => { const navigateToPath = useNavigateToPath(); const tabs = useMemo(() => { @@ -38,7 +39,7 @@ export const AnalyticsNavigationBar: FC<{ selectedTabId?: string; jobId?: string path: '/data_frame_analytics/models', }, ]; - if (jobId !== undefined) { + if (jobId !== undefined || modelId !== undefined) { navTabs.push({ id: 'map', name: i18n.translate('xpack.ml.dataframe.mapTabLabel', { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx index 2d74d08c4550c..cde29d357b1c6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx @@ -342,7 +342,7 @@ export const ModelsList: FC = () => { onClick: async (item) => { const path = await mlUrlGenerator.createUrl({ page: ML_PAGES.DATA_FRAME_ANALYTICS_MAP, - pageState: { jobId: item.metadata?.analytics_config.id }, + pageState: { modelId: item.model_id }, }); await navigateToPath(path, false); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx index 5a17b91818a1c..38b7088690e12 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx @@ -59,6 +59,7 @@ export const Page: FC = () => { const location = useLocation(); const selectedTabId = useMemo(() => location.pathname.split('/').pop(), [location]); const mapJobId = globalState?.ml?.jobId; + const mapModelId = globalState?.ml?.modelId; return ( @@ -106,8 +107,14 @@ export const Page: FC = () => { - - {selectedTabId === 'map' && mapJobId && } + + {selectedTabId === 'map' && (mapJobId || mapModelId) && ( + + )} {selectedTabId === 'data_frame_analytics' && ( = ({ analyticsId, details, getNodeData }) => { +export const Controls: FC = ({ analyticsId, modelId, details, getNodeData }) => { const [showFlyout, setShowFlyout] = useState(false); const [selectedNode, setSelectedNode] = useState(); @@ -98,10 +99,12 @@ export const Controls: FC = ({ analyticsId, details, getNodeData }) => { } const nodeDataButton = - analyticsId !== nodeLabel && nodeType === JOB_MAP_NODE_TYPES.ANALYTICS ? ( + analyticsId !== nodeLabel && + modelId !== nodeLabel && + (nodeType === JOB_MAP_NODE_TYPES.ANALYTICS || nodeType === JOB_MAP_NODE_TYPES.INDEX) ? ( { - getNodeData(nodeLabel); + getNodeData({ id: nodeLabel, type: nodeType }); setShowFlyout(false); }} iconType="branch" diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx index 85d10aa897415..18be614afb5c3 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx @@ -80,7 +80,8 @@ export const cytoscapeOptions: cytoscape.CytoscapeOptions = { { selector: 'node', style: { - 'background-color': theme.euiColorGhost, + 'background-color': (el: cytoscape.NodeSingular) => + el.data('isRoot') ? theme.euiColorLightShade : theme.euiColorGhost, 'background-height': '60%', 'background-width': '60%', 'border-color': (el: cytoscape.NodeSingular) => borderColorForNode(el), diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx index c29b6aca804d7..04e415eca1691 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx @@ -6,6 +6,7 @@ import React, { FC } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { JOB_MAP_NODE_TYPES } from '../../../../../../common/constants/data_frame_analytics'; export const JobMapLegend: FC = () => ( @@ -17,7 +18,10 @@ export const JobMapLegend: FC = () => (
- {JOB_MAP_NODE_TYPES.INDEX} +
@@ -41,7 +45,10 @@ export const JobMapLegend: FC = () => ( - {JOB_MAP_NODE_TYPES.ANALYTICS} + @@ -49,11 +56,29 @@ export const JobMapLegend: FC = () => ( - + - {'inference model'} + + + + + + + + + + + + + diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx index 53d47937409d8..6395d491d5e6b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx @@ -15,6 +15,7 @@ import { Cytoscape, Controls, JobMapLegend } from './components'; import { ml } from '../../../services/ml_api_service'; import { useMlKibana } from '../../../contexts/kibana'; import { useRefDimensions } from './components/use_ref_dimensions'; +import { JOB_MAP_NODE_TYPES } from '../../../../../common/constants/data_frame_analytics'; const cytoscapeDivStyle = { background: `linear-gradient( @@ -36,22 +37,36 @@ ${theme.euiColorLightShade}`, marginTop: 0, }; -export const JobMapTitle: React.FC<{ analyticsId: string }> = ({ analyticsId }) => ( +export const JobMapTitle: React.FC<{ analyticsId?: string; modelId?: string }> = ({ + analyticsId, + modelId, +}) => ( - {i18n.translate('xpack.ml.dataframe.analyticsMap.analyticsIdTitle', { - defaultMessage: 'Map for analytics ID {analyticsId}', - values: { analyticsId }, - })} + {analyticsId + ? i18n.translate('xpack.ml.dataframe.analyticsMap.analyticsIdTitle', { + defaultMessage: 'Map for analytics ID {analyticsId}', + values: { analyticsId }, + }) + : i18n.translate('xpack.ml.dataframe.analyticsMap.modelIdTitle', { + defaultMessage: 'Map for trained model ID {modelId}', + values: { modelId }, + })} ); +interface GetDataObjectParameter { + id: string; + type: string; +} + interface Props { - analyticsId: string; + analyticsId?: string; + modelId?: string; } -export const JobMap: FC = ({ analyticsId }) => { +export const JobMap: FC = ({ analyticsId, modelId }) => { const [elements, setElements] = useState([]); const [nodeDetails, setNodeDetails] = useState({}); const [error, setError] = useState(undefined); @@ -60,14 +75,33 @@ export const JobMap: FC = ({ analyticsId }) => { services: { notifications }, } = useMlKibana(); - const getData = async (id?: string) => { + const getDataWrapper = async (params?: GetDataObjectParameter) => { + const { id, type } = params ?? {}; const treatAsRoot = id !== undefined; - const idToUse = treatAsRoot ? id : analyticsId; - // Pass in treatAsRoot flag - endpoint will take job destIndex to grab jobs created from it + let idToUse: string; + + if (id !== undefined) { + idToUse = id; + } else if (modelId !== undefined) { + idToUse = modelId; + } else { + idToUse = analyticsId as string; + } + + await getData( + idToUse, + treatAsRoot, + modelId !== undefined && treatAsRoot === false ? JOB_MAP_NODE_TYPES.TRAINED_MODEL : type + ); + }; + + const getData = async (idToUse: string, treatAsRoot: boolean, type?: string) => { + // Pass in treatAsRoot flag - endpoint will take job or index to grab jobs created from it // TODO: update analyticsMap return type here const analyticsMap: any = await ml.dataFrameAnalytics.getDataFrameAnalyticsMap( idToUse, - treatAsRoot + treatAsRoot, + type ); const { elements: nodeElements, details, error: fetchError } = analyticsMap; @@ -86,7 +120,7 @@ export const JobMap: FC = ({ analyticsId }) => { } if (nodeElements && nodeElements.length > 0) { - if (id === undefined) { + if (treatAsRoot === false) { setElements(nodeElements); setNodeDetails(details); } else { @@ -98,8 +132,8 @@ export const JobMap: FC = ({ analyticsId }) => { }; useEffect(() => { - getData(); - }, [analyticsId]); + getDataWrapper(); + }, [analyticsId, modelId]); if (error !== undefined) { notifications.toasts.addDanger( @@ -119,14 +153,19 @@ export const JobMap: FC = ({ analyticsId }) => {
- + - +
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts index 21556a4702b4e..8e541443c34a1 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts @@ -83,12 +83,12 @@ export const dataFrameAnalytics = { body, }); }, - getDataFrameAnalyticsMap(analyticsId?: string, treatAsRoot?: boolean) { - const analyticsIdString = analyticsId !== undefined ? `/${analyticsId}` : ''; + getDataFrameAnalyticsMap(id: string, treatAsRoot: boolean, type?: string) { + const idString = id !== undefined ? `/${id}` : ''; return http({ - path: `${basePath()}/data_frame/analytics/map${analyticsIdString}`, + path: `${basePath()}/data_frame/analytics/map${idString}`, method: 'GET', - query: { treatAsRoot }, + query: { treatAsRoot, type }, }); }, evaluateDataFrameAnalytics(evaluateConfig: any) { diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx index 8e26a912a6051..78c0cb97cb889 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx @@ -38,12 +38,14 @@ export const PlotByFunctionControls = ({ selectedDetectorIndex, selectedJobId, selectedEntities, + entityControlsCount, }: { functionDescription: undefined | string; setFunctionDescription: (func: string) => void; selectedDetectorIndex: number; selectedJobId: string; selectedEntities: Record; + entityControlsCount: number; }) => { const toastNotificationService = useToastNotificationService(); @@ -73,9 +75,12 @@ export const PlotByFunctionControls = ({ return; } const selectedJob = mlJobService.getJob(selectedJobId); + // if no controls, it's okay to fetch + // if there are series controls, only fetch if user has selected something + const validEntities = + entityControlsCount === 0 || (entityControlsCount > 0 && selectedEntities !== undefined); if ( - // set if only entity controls are picked - selectedEntities !== undefined && + validEntities && functionDescription === undefined && isMetricDetector(selectedJob, selectedDetectorIndex) ) { @@ -95,6 +100,7 @@ export const PlotByFunctionControls = ({ selectedEntities, selectedJobId, functionDescription, + entityControlsCount, ]); if (functionDescription === undefined) return null; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx index 37a637e2c1446..c1f35e68e43c6 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx @@ -28,6 +28,7 @@ import { useStorage } from '../../../contexts/ml/use_storage'; import { EntityFieldType } from '../../../../../common/types/anomalies'; import { FieldDefinition } from '../../../services/results_service/result_service_rx'; import { getViewableDetectors } from '../../timeseriesexplorer_utils/get_viewable_detectors'; +import { PlotByFunctionControls } from '../plot_function_controls'; function getEntityControlOptions(fieldValues: FieldDefinition['values']): ComboBoxOption[] { if (!Array.isArray(fieldValues)) { @@ -67,6 +68,8 @@ interface SeriesControlsProps { bounds: any; appStateHandler: Function; selectedEntities: Record; + functionDescription: string; + setFunctionDescription: (func: string) => void; } /** @@ -79,6 +82,8 @@ export const SeriesControls: FC = ({ appStateHandler, children, selectedEntities, + functionDescription, + setFunctionDescription, }) => { const { services: { @@ -306,6 +311,15 @@ export const SeriesControls: FC = ({ /> ); })} + + {children} diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index f22cc191ef844..47d0f25857b03 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -81,7 +81,6 @@ import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/ import { getControlsForDetector } from './get_controls_for_detector'; import { SeriesControls } from './components/series_controls'; import { TimeSeriesChartWithTooltips } from './components/timeseries_chart/timeseries_chart_with_tooltip'; -import { PlotByFunctionControls } from './components/plot_function_controls'; import { aggregationTypeTransform } from '../../../common/util/anomaly_utils'; import { isMetricDetector } from './get_function_description'; import { getViewableDetectors } from './timeseriesexplorer_utils/get_viewable_detectors'; @@ -1013,15 +1012,9 @@ export class TimeSeriesExplorer extends React.Component { selectedDetectorIndex={selectedDetectorIndex} selectedEntities={this.props.selectedEntities} bounds={bounds} + functionDescription={this.props.functionDescription} + setFunctionDescription={this.setFunctionDescription} > - - {arePartitioningFieldsProvided && ( diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts index dc9c3bd86cc63..10764022a3ce7 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts @@ -104,11 +104,12 @@ export function createDataFrameAnalyticsMapUrl( let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_MAP}`; if (mlUrlGeneratorState) { - const { jobId, analysisType, defaultIsTraining, globalState } = mlUrlGeneratorState; + const { jobId, modelId, analysisType, defaultIsTraining, globalState } = mlUrlGeneratorState; const queryState: DataFrameAnalyticsExplorationQueryState = { ml: { jobId, + modelId, analysisType, defaultIsTraining, }, diff --git a/x-pack/plugins/ml/server/lib/spaces_utils.ts b/x-pack/plugins/ml/server/lib/spaces_utils.ts index e1606061b0ff1..b96fe6f2d1eb6 100644 --- a/x-pack/plugins/ml/server/lib/spaces_utils.ts +++ b/x-pack/plugins/ml/server/lib/spaces_utils.ts @@ -5,29 +5,33 @@ */ import { Legacy } from 'kibana'; -import { KibanaRequest } from 'kibana/server'; -import { SpacesPluginSetup } from '../../../spaces/server'; +import { KibanaRequest } from '../../../../../src/core/server'; +import { SpacesPluginStart } from '../../../spaces/server'; export type RequestFacade = KibanaRequest | Legacy.Request; export function spacesUtilsProvider( - spacesPlugin: SpacesPluginSetup | undefined, + getSpacesPlugin: (() => Promise) | undefined, request: RequestFacade ) { async function isMlEnabledInSpace(): Promise { - if (spacesPlugin === undefined) { + if (getSpacesPlugin === undefined) { // if spaces is disabled force isMlEnabledInSpace to be true return true; } - const space = await spacesPlugin.spacesService.getActiveSpace(request); + const space = await (await getSpacesPlugin()).spacesService.getActiveSpace( + request instanceof KibanaRequest ? request : KibanaRequest.from(request) + ); return space.disabledFeatures.includes('ml') === false; } async function getAllSpaces(): Promise { - if (spacesPlugin === undefined) { + if (getSpacesPlugin === undefined) { return null; } - const client = await spacesPlugin.spacesService.scopedClient(request); + const client = (await getSpacesPlugin()).spacesService.createSpacesClient( + request instanceof KibanaRequest ? request : KibanaRequest.from(request) + ); const spaces = await client.getAll(); return spaces.map((s) => s.id); } diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts index f1f0b352ca920..769ec09a6b911 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts @@ -10,12 +10,17 @@ import { JOB_MAP_NODE_TYPES, JobMapNodeTypes, } from '../../../common/constants/data_frame_analytics'; +import { TrainedModelConfigResponse } from '../../../common/types/trained_models'; import { INDEX_META_DATA_CREATED_BY } from '../../../common/constants/file_datavisualizer'; import { getAnalysisType } from '../../../common/util/analytics_utils'; import { AnalyticsMapEdgeElement, AnalyticsMapReturnType, AnalyticsMapNodeElement, + ExtendAnalyticsMapArgs, + GetAnalyticsMapArgs, + InitialElementsReturnType, + isCompleteInitialReturnType, isAnalyticsMapEdgeElement, isAnalyticsMapNodeElement, isIndexPatternLinkReturnType, @@ -29,7 +34,7 @@ import type { MlClient } from '../../lib/ml_client'; export class AnalyticsManager { private _client: IScopedClusterClient['asInternalUser']; private _mlClient: MlClient; - public _inferenceModels: any; // TODO: update types + public _inferenceModels: TrainedModelConfigResponse[]; constructor(mlClient: MlClient, client: IScopedClusterClient['asInternalUser']) { this._client = client; @@ -37,11 +42,11 @@ export class AnalyticsManager { this._inferenceModels = []; } - public set inferenceModels(models: any) { + public set inferenceModels(models) { this._inferenceModels = models; } - public get inferenceModels(): any { + public get inferenceModels() { return this._inferenceModels; } @@ -56,16 +61,20 @@ export class AnalyticsManager { } } - private isDuplicateElement(analyticsId: string, elements: any[]): boolean { + private isDuplicateElement(analyticsId: string, elements: MapElements[]): boolean { let isDuplicate = false; - elements.forEach((elem: any) => { - if (elem.data.label === analyticsId && elem.data.type === JOB_MAP_NODE_TYPES.ANALYTICS) { + elements.forEach((elem) => { + if ( + isAnalyticsMapNodeElement(elem) && + elem.data.label === analyticsId && + elem.data.type === JOB_MAP_NODE_TYPES.ANALYTICS + ) { isDuplicate = true; } }); return isDuplicate; } - // @ts-ignore // TODO: is this needed? + private async getAnalyticsModelData(modelId: string) { const resp = await this._mlClient.getTrainedModels({ model_id: modelId, @@ -80,11 +89,17 @@ export class AnalyticsManager { return models; } - private async getAnalyticsJobData(analyticsId: string) { - const resp = await this._mlClient.getDataFrameAnalytics({ - id: analyticsId, - }); - const jobData = resp?.body?.data_frame_analytics[0]; + private async getAnalyticsData(analyticsId?: string) { + const options = analyticsId + ? { + id: analyticsId, + } + : undefined; + const resp = await this._mlClient.getDataFrameAnalytics(options); + const jobData = analyticsId + ? resp?.body?.data_frame_analytics[0] + : resp?.body?.data_frame_analytics; + return jobData; } @@ -130,7 +145,7 @@ export class AnalyticsManager { return { isWildcardIndexPattern, isIndexPattern: true, indexData, meta }; } else if (type.includes(JOB_MAP_NODE_TYPES.ANALYTICS)) { // fetch job associated with this index - const jobData = await this.getAnalyticsJobData(id); + const jobData = await this.getAnalyticsData(id); return { jobData, isJob: true }; } else if (type === JOB_MAP_NODE_TYPES.TRANSFORM) { // fetch transform so we can get original index pattern @@ -155,12 +170,12 @@ export class AnalyticsManager { let edgeElement; if (analyticsModel !== undefined) { - const modelId = `${analyticsModel.model_id}-${JOB_MAP_NODE_TYPES.INFERENCE_MODEL}`; + const modelId = `${analyticsModel.model_id}-${JOB_MAP_NODE_TYPES.TRAINED_MODEL}`; modelElement = { data: { id: modelId, label: analyticsModel.model_id, - type: JOB_MAP_NODE_TYPES.INFERENCE_MODEL, + type: JOB_MAP_NODE_TYPES.TRAINED_MODEL, }, }; // Create edge for job and corresponding model @@ -201,29 +216,41 @@ export class AnalyticsManager { } /** - * Works backward from jobId to return related jobs from source indices - * @param jobId + * Prepares the initial elements for incoming modelId + * @param modelId */ - async getAnalyticsMap(analyticsId: string): Promise { - const result: any = { elements: [], details: {}, error: null }; - const modelElements: MapElements[] = []; - const indexPatternElements: MapElements[] = []; + async getInitialElementsModelRoot(modelId: string): Promise { + const resultElements = []; + const modelElements = []; + const details: any = {}; + // fetch model data and create model elements + let data = await this.getAnalyticsModelData(modelId); + const modelNodeId = `${data.model_id}-${JOB_MAP_NODE_TYPES.TRAINED_MODEL}`; + const sourceJobId = data?.metadata?.analytics_config?.id; + let nextLinkId: string | undefined; + let nextType: JobMapNodeTypes | undefined; + let previousNodeId: string | undefined; + + modelElements.push({ + data: { + id: modelNodeId, + label: data.model_id, + type: JOB_MAP_NODE_TYPES.TRAINED_MODEL, + isRoot: true, + }, + }); - try { - await this.setInferenceModels(); - // Create first node for incoming analyticsId - let data = await this.getAnalyticsJobData(analyticsId); - let nextLinkId = data?.source?.index[0]; - let nextType: JobMapNodeTypes = JOB_MAP_NODE_TYPES.INDEX; - let complete = false; - let link: NextLinkReturnType; - let count = 0; - let rootTransform; - let rootIndexPattern; - - let previousNodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + details[modelNodeId] = data; + // fetch source job data and create elements + if (sourceJobId !== undefined) { + data = await this.getAnalyticsData(sourceJobId); - result.elements.push({ + nextLinkId = data?.source?.index[0]; + nextType = JOB_MAP_NODE_TYPES.INDEX; + + previousNodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + + resultElements.push({ data: { id: previousNodeId, label: data.id, @@ -231,167 +258,178 @@ export class AnalyticsManager { analysisType: getAnalysisType(data?.analysis), }, }); - result.details[previousNodeId] = data; + // Create edge between job and model + modelElements.push({ + data: { + id: `${previousNodeId}~${modelNodeId}`, + source: previousNodeId, + target: modelNodeId, + }, + }); - let { modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements(analyticsId); - if (isAnalyticsMapNodeElement(modelElement)) { - modelElements.push(modelElement); - result.details[modelElement.data.id] = modelDetails; - } - if (isAnalyticsMapEdgeElement(edgeElement)) { - modelElements.push(edgeElement); - } - // Add a safeguard against infinite loops. - while (complete === false) { - count++; - if (count >= 100) { - break; - } + details[previousNodeId] = data; + } - try { - link = await this.getNextLink({ - id: nextLinkId, - type: nextType, - }); - } catch (error) { - result.error = error.message || 'Something went wrong'; - break; - } - // If it's index pattern, check meta data to see what to fetch next - if (isIndexPatternLinkReturnType(link) && link.isIndexPattern === true) { - if (link.isWildcardIndexPattern === true) { - // Create index nodes for each of the indices included in the index pattern then break - const { details, elements } = this.getIndexPatternElements( - link.indexData, - previousNodeId - ); - - indexPatternElements.push(...elements); - result.details = { ...result.details, ...details }; - complete = true; - } else { - const nodeId = `${nextLinkId}-${JOB_MAP_NODE_TYPES.INDEX}`; - result.elements.unshift({ - data: { id: nodeId, label: nextLinkId, type: JOB_MAP_NODE_TYPES.INDEX }, - }); - result.details[nodeId] = link.indexData; - } + return { data, details, resultElements, modelElements, nextLinkId, nextType, previousNodeId }; + } - // Check meta data - if ( - link.isWildcardIndexPattern === false && - (link.meta === undefined || link.meta?.created_by === INDEX_META_DATA_CREATED_BY) - ) { - rootIndexPattern = nextLinkId; - complete = true; - break; - } + /** + * Prepares the initial elements for incoming jobId + * @param jobId + */ + async getInitialElementsJobRoot(jobId: string): Promise { + const resultElements = []; + const modelElements = []; + const details: any = {}; + const data = await this.getAnalyticsData(jobId); + const nextLinkId = data?.source?.index[0]; + const nextType: JobMapNodeTypes = JOB_MAP_NODE_TYPES.INDEX; + + const previousNodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + + resultElements.push({ + data: { + id: previousNodeId, + label: data.id, + type: JOB_MAP_NODE_TYPES.ANALYTICS, + analysisType: getAnalysisType(data?.analysis), + isRoot: true, + }, + }); - if (link.meta?.created_by === 'data-frame-analytics') { - nextLinkId = link.meta.analytics; - nextType = JOB_MAP_NODE_TYPES.ANALYTICS; - } + details[previousNodeId] = data; - if (link.meta?.created_by === JOB_MAP_NODE_TYPES.TRANSFORM) { - nextLinkId = link.meta._transform?.transform; - nextType = JOB_MAP_NODE_TYPES.TRANSFORM; - } - } else if (isJobDataLinkReturnType(link) && link.isJob === true) { - data = link.jobData; - const nodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; - previousNodeId = nodeId; + const { modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements(jobId); + if (isAnalyticsMapNodeElement(modelElement)) { + modelElements.push(modelElement); + details[modelElement.data.id] = modelDetails; + } + if (isAnalyticsMapEdgeElement(edgeElement)) { + modelElements.push(edgeElement); + } - result.elements.unshift({ - data: { - id: nodeId, - label: data.id, - type: JOB_MAP_NODE_TYPES.ANALYTICS, - analysisType: getAnalysisType(data?.analysis), - }, - }); - result.details[nodeId] = data; - nextLinkId = data?.source?.index[0]; - nextType = JOB_MAP_NODE_TYPES.INDEX; - - // Get inference model for analytics job and create model node - ({ modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements(data.id)); - if (isAnalyticsMapNodeElement(modelElement)) { - modelElements.push(modelElement); - result.details[modelElement.data.id] = modelDetails; - } - if (isAnalyticsMapEdgeElement(edgeElement)) { - modelElements.push(edgeElement); + return { data, details, resultElements, modelElements, nextLinkId, nextType, previousNodeId }; + } + + /** + * Works backward from jobId or modelId to return related jobs, indices, models, and transforms + * @param jobId (optional) + * @param modelId (optional) + */ + async getAnalyticsMap({ + analyticsId, + modelId, + }: GetAnalyticsMapArgs): Promise { + const result: AnalyticsMapReturnType = { elements: [], details: {}, error: null }; + const modelElements: MapElements[] = []; + const indexPatternElements: MapElements[] = []; + + try { + await this.setInferenceModels(); + // Create first node for incoming analyticsId or modelId + let initialData: InitialElementsReturnType = {} as InitialElementsReturnType; + if (analyticsId !== undefined) { + initialData = await this.getInitialElementsJobRoot(analyticsId); + } else if (modelId !== undefined) { + initialData = await this.getInitialElementsModelRoot(modelId); + } + + const { + resultElements, + details: initialDetails, + modelElements: initialModelElements, + } = initialData; + + result.elements.push(...resultElements); + result.details = initialDetails; + modelElements.push(...initialModelElements); + + if (isCompleteInitialReturnType(initialData)) { + let { data, nextLinkId, nextType, previousNodeId } = initialData; + + let complete = false; + let link: NextLinkReturnType; + let count = 0; + let rootTransform; + let rootIndexPattern; + let modelElement; + let modelDetails; + let edgeElement; + + // Add a safeguard against infinite loops. + while (complete === false) { + count++; + if (count >= 100) { + break; } - } else if (isTransformLinkReturnType(link) && link.isTransform === true) { - data = link.transformData; - const nodeId = `${data.id}-${JOB_MAP_NODE_TYPES.TRANSFORM}`; - previousNodeId = nodeId; - rootTransform = data.dest.index; + try { + link = await this.getNextLink({ + id: nextLinkId, + type: nextType, + }); + } catch (error) { + result.error = error.message || 'Something went wrong'; + break; + } + // If it's index pattern, check meta data to see what to fetch next + if (isIndexPatternLinkReturnType(link) && link.isIndexPattern === true) { + if (link.isWildcardIndexPattern === true) { + // Create index nodes for each of the indices included in the index pattern then break + const { details, elements } = this.getIndexPatternElements( + link.indexData, + previousNodeId + ); + + indexPatternElements.push(...elements); + result.details = { ...result.details, ...details }; + complete = true; + } else { + const nodeId = `${nextLinkId}-${JOB_MAP_NODE_TYPES.INDEX}`; + result.elements.unshift({ + data: { id: nodeId, label: nextLinkId, type: JOB_MAP_NODE_TYPES.INDEX }, + }); + result.details[nodeId] = link.indexData; + } - result.elements.unshift({ - data: { id: nodeId, label: data.id, type: JOB_MAP_NODE_TYPES.TRANSFORM }, - }); - result.details[nodeId] = data; - nextLinkId = data?.source?.index[0]; - nextType = JOB_MAP_NODE_TYPES.INDEX; - } - } // end while + // Check meta data + if ( + link.isWildcardIndexPattern === false && + (link.meta === undefined || link.meta?.created_by === INDEX_META_DATA_CREATED_BY) + ) { + rootIndexPattern = nextLinkId; + complete = true; + break; + } - // create edge elements - const elemLength = result.elements.length - 1; - for (let i = 0; i < elemLength; i++) { - const currentElem = result.elements[i]; - const nextElem = result.elements[i + 1]; - if ( - currentElem !== undefined && - nextElem !== undefined && - currentElem?.data?.id.includes('*') === false && - nextElem?.data?.id.includes('*') === false - ) { - result.elements.push({ - data: { - id: `${currentElem.data.id}~${nextElem.data.id}`, - source: currentElem.data.id, - target: nextElem.data.id, - }, - }); - } - } + if (link.meta?.created_by === 'data-frame-analytics') { + nextLinkId = link.meta.analytics; + nextType = JOB_MAP_NODE_TYPES.ANALYTICS; + } - // fetch all jobs associated with root transform if defined, otherwise check root index - if (rootTransform !== undefined || rootIndexPattern !== undefined) { - const analyticsJobs = await this._mlClient.getDataFrameAnalytics(); - const jobs = analyticsJobs?.body?.data_frame_analytics || []; - const comparator = rootTransform !== undefined ? rootTransform : rootIndexPattern; + if (link.meta?.created_by === JOB_MAP_NODE_TYPES.TRANSFORM) { + nextLinkId = link.meta._transform?.transform; + nextType = JOB_MAP_NODE_TYPES.TRANSFORM; + } + } else if (isJobDataLinkReturnType(link) && link.isJob === true) { + data = link.jobData; + const nodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + previousNodeId = nodeId; - for (let i = 0; i < jobs.length; i++) { - if ( - jobs[i]?.source?.index[0] === comparator && - this.isDuplicateElement(jobs[i].id, result.elements) === false - ) { - const nodeId = `${jobs[i].id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; - result.elements.push({ + result.elements.unshift({ data: { id: nodeId, - label: jobs[i].id, + label: data.id, type: JOB_MAP_NODE_TYPES.ANALYTICS, - analysisType: getAnalysisType(jobs[i]?.analysis), - }, - }); - result.details[nodeId] = jobs[i]; - const source = `${comparator}-${JOB_MAP_NODE_TYPES.INDEX}`; - result.elements.push({ - data: { - id: `${source}~${nodeId}`, - source, - target: nodeId, + analysisType: getAnalysisType(data?.analysis), }, }); + result.details[nodeId] = data; + nextLinkId = data?.source?.index[0]; + nextType = JOB_MAP_NODE_TYPES.INDEX; + // Get inference model for analytics job and create model node - ({ modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements( - jobs[i].id - )); + ({ modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements(data.id)); if (isAnalyticsMapNodeElement(modelElement)) { modelElements.push(modelElement); result.details[modelElement.data.id] = modelDetails; @@ -399,12 +437,88 @@ export class AnalyticsManager { if (isAnalyticsMapEdgeElement(edgeElement)) { modelElements.push(edgeElement); } + } else if (isTransformLinkReturnType(link) && link.isTransform === true) { + data = link.transformData; + + const nodeId = `${data.id}-${JOB_MAP_NODE_TYPES.TRANSFORM}`; + previousNodeId = nodeId; + rootTransform = data.dest.index; + + result.elements.unshift({ + data: { id: nodeId, label: data.id, type: JOB_MAP_NODE_TYPES.TRANSFORM }, + }); + result.details[nodeId] = data; + nextLinkId = data?.source?.index[0]; + nextType = JOB_MAP_NODE_TYPES.INDEX; + } + } // end while + + // create edge elements + const elemLength = result.elements.length - 1; + for (let i = 0; i < elemLength; i++) { + const currentElem = result.elements[i]; + const nextElem = result.elements[i + 1]; + if ( + currentElem !== undefined && + nextElem !== undefined && + currentElem?.data?.id.includes('*') === false && + nextElem?.data?.id.includes('*') === false + ) { + result.elements.push({ + data: { + id: `${currentElem.data.id}~${nextElem.data.id}`, + source: currentElem.data.id, + target: nextElem.data.id, + }, + }); + } + } + + // fetch all jobs associated with root transform if defined, otherwise check root index + if (rootTransform !== undefined || rootIndexPattern !== undefined) { + const jobs = await this.getAnalyticsData(); + const comparator = rootTransform !== undefined ? rootTransform : rootIndexPattern; + + for (let i = 0; i < jobs.length; i++) { + if ( + jobs[i]?.source?.index[0] === comparator && + this.isDuplicateElement(jobs[i].id, result.elements) === false + ) { + const nodeId = `${jobs[i].id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + result.elements.push({ + data: { + id: nodeId, + label: jobs[i].id, + type: JOB_MAP_NODE_TYPES.ANALYTICS, + analysisType: getAnalysisType(jobs[i]?.analysis), + }, + }); + result.details[nodeId] = jobs[i]; + const source = `${comparator}-${JOB_MAP_NODE_TYPES.INDEX}`; + result.elements.push({ + data: { + id: `${source}~${nodeId}`, + source, + target: nodeId, + }, + }); + // Get inference model for analytics job and create model node + ({ modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements( + jobs[i].id + )); + if (isAnalyticsMapNodeElement(modelElement)) { + modelElements.push(modelElement); + result.details[modelElement.data.id] = modelDetails; + } + if (isAnalyticsMapEdgeElement(edgeElement)) { + modelElements.push(edgeElement); + } + } } } } // Include model and index pattern nodes in result elements now that all other nodes have been created result.elements.push(...modelElements, ...indexPatternElements); - return result; } catch (error) { result.error = error.message || 'An error occurred fetching map'; @@ -412,56 +526,64 @@ export class AnalyticsManager { } } - async extendAnalyticsMapForAnalyticsJob(analyticsId: string): Promise { - const result: any = { elements: [], details: {}, error: null }; - + async extendAnalyticsMapForAnalyticsJob({ + analyticsId, + index, + }: ExtendAnalyticsMapArgs): Promise { + const result: AnalyticsMapReturnType = { elements: [], details: {}, error: null }; try { await this.setInferenceModels(); + const jobs = await this.getAnalyticsData(); + let rootIndex; + let rootIndexNodeId; + + if (analyticsId !== undefined) { + const jobData = await this.getAnalyticsData(analyticsId); + const currentJobNodeId = `${jobData.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + rootIndex = Array.isArray(jobData?.dest?.index) + ? jobData?.dest?.index[0] + : jobData?.dest?.index; + rootIndexNodeId = `${rootIndex}-${JOB_MAP_NODE_TYPES.INDEX}`; + + // Fetch inference model for incoming job id and add node and edge + const { modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements( + analyticsId + ); + if (isAnalyticsMapNodeElement(modelElement)) { + result.elements.push(modelElement); + result.details[modelElement.data.id] = modelDetails; + } + if (isAnalyticsMapEdgeElement(edgeElement)) { + result.elements.push(edgeElement); + } - const jobData = await this.getAnalyticsJobData(analyticsId); - const currentJobNodeId = `${jobData.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; - const destIndex = Array.isArray(jobData?.dest?.index) - ? jobData?.dest?.index[0] - : jobData?.dest?.index; - const destIndexNodeId = `${destIndex}-${JOB_MAP_NODE_TYPES.INDEX}`; - const analyticsJobs = await this._mlClient.getDataFrameAnalytics(); - const jobs = analyticsJobs?.body?.data_frame_analytics || []; - - // Fetch inference model for incoming job id and add node and edge - const { modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements( - analyticsId - ); - if (isAnalyticsMapNodeElement(modelElement)) { - result.elements.push(modelElement); - result.details[modelElement.data.id] = modelDetails; - } - if (isAnalyticsMapEdgeElement(edgeElement)) { - result.elements.push(edgeElement); + // If rootIndex node has not been created, create it + const rootIndexDetails = await this.getIndexData(rootIndex); + result.elements.push({ + data: { + id: rootIndexNodeId, + label: rootIndex, + type: JOB_MAP_NODE_TYPES.INDEX, + }, + }); + result.details[rootIndexNodeId] = rootIndexDetails; + + // Connect incoming job to rootIndex + result.elements.push({ + data: { + id: `${currentJobNodeId}~${rootIndexNodeId}`, + source: currentJobNodeId, + target: rootIndexNodeId, + }, + }); + } else { + rootIndex = index; + rootIndexNodeId = `${rootIndex}-${JOB_MAP_NODE_TYPES.INDEX}`; } - // If destIndex node has not been created, create it - const destIndexDetails = await this.getIndexData(destIndex); - result.elements.push({ - data: { - id: destIndexNodeId, - label: destIndex, - type: JOB_MAP_NODE_TYPES.INDEX, - }, - }); - result.details[destIndexNodeId] = destIndexDetails; - - // Connect incoming job to destIndex - result.elements.push({ - data: { - id: `${currentJobNodeId}~${destIndexNodeId}`, - source: currentJobNodeId, - target: destIndexNodeId, - }, - }); - for (let i = 0; i < jobs.length; i++) { if ( - jobs[i]?.source?.index[0] === destIndex && + jobs[i]?.source?.index[0] === rootIndex && this.isDuplicateElement(jobs[i].id, result.elements) === false ) { // Create node for associated job @@ -478,8 +600,8 @@ export class AnalyticsManager { result.elements.push({ data: { - id: `${destIndexNodeId}~${nodeId}`, - source: destIndexNodeId, + id: `${rootIndexNodeId}~${nodeId}`, + source: rootIndexNodeId, target: nodeId, }, }); diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/types.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/types.ts index 5d6cec8cdfa61..e34d68ec7840c 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/types.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/types.ts @@ -4,6 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ +import { JobMapNodeTypes } from '../../../common/constants/data_frame_analytics'; + +interface AnalyticsMapArg { + analyticsId: string; +} +interface GetAnalyticsJobIdArg extends AnalyticsMapArg { + modelId?: never; +} +interface GetAnalyticsModelIdArg { + analyticsId?: never; + modelId: string; +} +interface ExtendAnalyticsJobIdArg extends AnalyticsMapArg { + index?: never; +} +interface ExtendAnalyticsIndexArg { + analyticsId?: never; + index: string; +} + +export type GetAnalyticsMapArgs = GetAnalyticsJobIdArg | GetAnalyticsModelIdArg; +export type ExtendAnalyticsMapArgs = ExtendAnalyticsJobIdArg | ExtendAnalyticsIndexArg; + export interface IndexPatternLinkReturnType { isWildcardIndexPattern: boolean; isIndexPattern: boolean; @@ -26,9 +49,27 @@ export type NextLinkReturnType = export type MapElements = AnalyticsMapNodeElement | AnalyticsMapEdgeElement; export interface AnalyticsMapReturnType { elements: MapElements[]; - details: object; // transform, job, or index details + details: Record; // transform, job, or index details error: null | any; } + +interface BasicInitialElementsReturnType { + data: any; + details: object; + resultElements: MapElements[]; + modelElements: MapElements[]; +} + +export interface InitialElementsReturnType extends BasicInitialElementsReturnType { + nextLinkId?: string; + nextType?: JobMapNodeTypes; + previousNodeId?: string; +} +interface CompleteInitialElementsReturnType extends BasicInitialElementsReturnType { + nextLinkId: string; + nextType: JobMapNodeTypes; + previousNodeId: string; +} export interface AnalyticsMapNodeElement { data: { id: string; @@ -44,6 +85,16 @@ export interface AnalyticsMapEdgeElement { target: string; }; } +export const isCompleteInitialReturnType = (arg: any): arg is CompleteInitialElementsReturnType => { + if (typeof arg !== 'object' || arg === null) return false; + const keys = Object.keys(arg); + return ( + keys.length > 0 && + keys.includes('nextLinkId') && + keys.includes('nextType') && + keys.includes('previousNodeId') + ); +}; export const isAnalyticsMapNodeElement = (arg: any): arg is AnalyticsMapNodeElement => { if (typeof arg !== 'object' || arg === null) return false; const keys = Object.keys(arg); diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 669fc9a1d92e4..5e103dbc1806a 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -18,8 +18,8 @@ import { } from 'kibana/server'; import type { SecurityPluginSetup } from '../../security/server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; +import { PluginsSetup, PluginsStart, RouteInitialization } from './types'; import { SpacesPluginSetup } from '../../spaces/server'; -import { PluginsSetup, RouteInitialization } from './types'; import { PLUGIN_ID } from '../common/constants/app'; import { MlCapabilities } from '../common/types/capabilities'; @@ -61,7 +61,8 @@ import { RouteGuard } from './lib/route_guard'; export type MlPluginSetup = SharedServices; export type MlPluginStart = void; -export class MlServerPlugin implements Plugin { +export class MlServerPlugin + implements Plugin { private log: Logger; private version: string; private mlLicense: MlLicense; @@ -80,7 +81,7 @@ export class MlServerPlugin implements Plugin (this.setMlReady = resolve)); } - public setup(coreSetup: CoreSetup, plugins: PluginsSetup): MlPluginSetup { + public setup(coreSetup: CoreSetup, plugins: PluginsSetup): MlPluginSetup { this.spacesPlugin = plugins.spaces; this.security = plugins.security; const { admin, user, apmUser } = getPluginPrivileges(); @@ -157,6 +158,10 @@ export class MlServerPlugin implements Plugin coreSetup.getStartServices().then(([, { spaces }]) => spaces!) + : undefined; + annotationRoutes(routeInit, plugins.security); calendars(routeInit); dataFeedRoutes(routeInit); @@ -175,7 +180,7 @@ export class MlServerPlugin implements Plugin { try { - const { isMlEnabledInSpace } = spacesUtilsProvider(spaces, (request as unknown) as Request); + const { isMlEnabledInSpace } = spacesUtilsProvider(getSpaces, request); const mlCapabilities = await resolveMlCapabilities(request); if (mlCapabilities === null) { 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 c7c50eb74595e..b1494546c89f4 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/system.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/system.ts @@ -10,7 +10,7 @@ import { RequestParams } from '@elastic/elasticsearch'; import { MlLicense } from '../../../common/license'; import { CloudSetup } from '../../../../cloud/server'; import { spacesUtilsProvider } from '../../lib/spaces_utils'; -import { SpacesPluginSetup } from '../../../../spaces/server'; +import { SpacesPluginStart } from '../../../../spaces/server'; import { capabilitiesProvider } from '../../lib/capabilities'; import { MlInfoResponse } from '../../../common/types/ml_server_info'; import { MlCapabilitiesResponse, ResolveMlCapabilities } from '../../../common/types/capabilities'; @@ -33,7 +33,7 @@ export interface MlSystemProvider { export function getMlSystemProvider( getGuards: GetGuards, mlLicense: MlLicense, - spaces: SpacesPluginSetup | undefined, + getSpaces: (() => Promise) | undefined, cloud: CloudSetup | undefined, resolveMlCapabilities: ResolveMlCapabilities ): MlSystemProvider { @@ -44,7 +44,7 @@ export function getMlSystemProvider( return await getGuards(request, savedObjectsClient) .isMinimumLicense() .ok(async ({ mlClient }) => { - const { isMlEnabledInSpace } = spacesUtilsProvider(spaces, request); + const { isMlEnabledInSpace } = spacesUtilsProvider(getSpaces, request); const mlCapabilities = await resolveMlCapabilities(request); if (mlCapabilities === null) { 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 dc7bc06fde7d5..0699c1af3086a 100644 --- a/x-pack/plugins/ml/server/shared_services/shared_services.ts +++ b/x-pack/plugins/ml/server/shared_services/shared_services.ts @@ -5,11 +5,8 @@ */ import { IClusterClient, IScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; -import { SpacesPluginSetup } from '../../../spaces/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 { SpacesPluginStart } from '../../../spaces/server'; +import { KibanaRequest } from '../../.././../../src/core/server'; import { MlLicense } from '../../common/license'; import type { CloudSetup } from '../../../cloud/server'; @@ -61,7 +58,7 @@ type OkCallback = (okParams: OkParams) => any; export function createSharedServices( mlLicense: MlLicense, - spacesPlugin: SpacesPluginSetup | undefined, + getSpaces: (() => Promise) | undefined, cloud: CloudSetup, authorization: SecurityPluginSetup['authz'] | undefined, resolveMlCapabilities: ResolveMlCapabilities, @@ -84,7 +81,7 @@ export function createSharedServices( savedObjectsClient, internalSavedObjectsClient, authorization, - spacesPlugin !== undefined, + getSpaces !== undefined, isMlReady ); @@ -119,7 +116,7 @@ export function createSharedServices( ...getAnomalyDetectorsProvider(getGuards), ...getModulesProvider(getGuards), ...getResultsServiceProvider(getGuards), - ...getMlSystemProvider(getGuards, mlLicense, spacesPlugin, cloud, resolveMlCapabilities), + ...getMlSystemProvider(getGuards, mlLicense, getSpaces, cloud, resolveMlCapabilities), }; } diff --git a/x-pack/plugins/ml/server/types.ts b/x-pack/plugins/ml/server/types.ts index 4a43a3e3f173c..df40f5a26b0f3 100644 --- a/x-pack/plugins/ml/server/types.ts +++ b/x-pack/plugins/ml/server/types.ts @@ -11,7 +11,7 @@ import type { CloudSetup } from '../../cloud/server'; import type { SecurityPluginSetup } from '../../security/server'; import type { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import type { LicensingPluginSetup } from '../../licensing/server'; -import type { SpacesPluginSetup } from '../../spaces/server'; +import type { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server'; import type { MlLicense } from '../common/license'; import type { ResolveMlCapabilities } from '../common/types/capabilities'; import type { RouteGuard } from './lib/route_guard'; @@ -27,7 +27,7 @@ export interface LicenseCheckResult { export interface SystemRouteDeps { cloud: CloudSetup; - spaces?: SpacesPluginSetup; + getSpaces?: () => Promise; resolveMlCapabilities: ResolveMlCapabilities; } @@ -41,6 +41,10 @@ export interface PluginsSetup { usageCollection: UsageCollectionSetup; } +export interface PluginsStart { + spaces?: SpacesPluginStart; +} + export interface RouteInitialization { router: IRouter; mlLicense: MlLicense; diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js deleted file mode 100644 index 5d8af8d71b7fc..0000000000000 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js +++ /dev/null @@ -1,275 +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 { defaultsDeep, uniq, compact } from 'lodash'; -import { ServiceStatusLevels } from '../../../../../src/core/server'; -import { - TELEMETRY_COLLECTION_INTERVAL, - KIBANA_STATS_TYPE_MONITORING, -} from '../../common/constants'; - -import { sendBulkPayload, monitoringBulk } from './lib'; - -/* - * Handles internal Kibana stats collection and uploading data to Monitoring - * bulk endpoint. - * - * NOTE: internal collection will be removed in 7.0 - * - * Depends on - * - 'monitoring.kibana.collection.enabled' config - * - monitoring enabled in ES (checked against xpack_main.info license info change) - * The dependencies are handled upstream - * - Ops Events - essentially Kibana's /api/status - * - Usage Stats - essentially Kibana's /api/stats - * - Kibana Settings - select uiSettings - * @param {Object} server HapiJS server instance - * @param {Object} xpackInfo server.plugins.xpack_main.info object - */ -export class BulkUploader { - constructor({ log, interval, elasticsearch, statusGetter$, kibanaStats }) { - if (typeof interval !== 'number') { - throw new Error('interval number of milliseconds is required'); - } - - this._timer = null; - // Hold sending and fetching usage until monitoring.bulk is successful. This means that we - // send usage data on the second tick. But would save a lot of bandwidth fetching usage on - // every tick when ES is failing or monitoring is disabled. - this._holdSendingUsage = false; - this._interval = interval; - this._lastFetchUsageTime = null; - // Limit sending and fetching usage to once per day once usage is successfully stored - // into the monitoring indices. - this._usageInterval = TELEMETRY_COLLECTION_INTERVAL; - this._log = log; - - this._cluster = elasticsearch.legacy.createClient('admin', { - plugins: [monitoringBulk], - }); - - this.kibanaStats = kibanaStats; - - this.kibanaStatus = null; - this.kibanaStatusGetter$ = statusGetter$.subscribe((nextStatus) => { - this.kibanaStatus = nextStatus.level; - }); - } - - filterCollectorSet(usageCollection) { - const successfulUploadInLastDay = - this._lastFetchUsageTime && this._lastFetchUsageTime + this._usageInterval > Date.now(); - - return usageCollection.getFilteredCollectorSet((c) => { - // this is internal bulk upload, so filter out API-only collectors - if (c.ignoreForInternalUploader) { - return false; - } - // Only collect usage data at the same interval as telemetry would (default to once a day) - if (usageCollection.isUsageCollector(c)) { - if (this._holdSendingUsage) { - return false; - } - if (successfulUploadInLastDay) { - return false; - } - } - - return true; - }); - } - - /* - * Start the interval timer - * @param {usageCollection} usageCollection object to use for initial the fetch/upload and fetch/uploading on interval - * @return undefined - */ - start(usageCollection) { - this._log.info('Starting monitoring stats collection'); - - if (this._timer) { - clearInterval(this._timer); - } else { - this._fetchAndUpload(this.filterCollectorSet(usageCollection)); // initial fetch - } - - this._timer = setInterval(() => { - this._fetchAndUpload(this.filterCollectorSet(usageCollection)); - }, this._interval); - } - - /* - * start() and stop() are lifecycle event handlers for - * xpackMainPlugin license changes - * @param {String} logPrefix help give context to the reason for stopping - */ - stop(logPrefix) { - clearInterval(this._timer); - this._timer = null; - - const prefix = logPrefix ? logPrefix + ':' : ''; - this._log.info(prefix + 'Monitoring stats collection is stopped'); - } - - handleNotEnabled() { - this.stop('Monitoring status upload endpoint is not enabled in Elasticsearch'); - } - handleConnectionLost() { - this.stop('Connection issue detected'); - } - - /* - * @param {usageCollection} usageCollection - * @return {Promise} - resolves to undefined - */ - async _fetchAndUpload(usageCollection) { - const collectorsReady = await usageCollection.areAllCollectorsReady(); - const hasUsageCollectors = usageCollection.some(usageCollection.isUsageCollector); - if (!collectorsReady) { - this._log.debug('Skipping bulk uploading because not all collectors are ready'); - if (hasUsageCollectors) { - this._lastFetchUsageTime = null; - this._log.debug('Resetting lastFetchWithUsage because not all collectors are ready'); - } - return; - } - - const data = await usageCollection.bulkFetch(this._cluster.callAsInternalUser); - const payload = this.toBulkUploadFormat(compact(data), usageCollection); - if (payload && payload.length > 0) { - try { - this._log.debug(`Uploading bulk stats payload to the local cluster`); - const result = await this._onPayload(payload); - const sendSuccessful = !result.ignored && !result.errors; - if (!sendSuccessful && hasUsageCollectors) { - this._lastFetchUsageTime = null; - this._holdSendingUsage = true; - this._log.debug( - 'Resetting lastFetchWithUsage because uploading to the cluster was not successful.' - ); - } - - if (sendSuccessful) { - this._holdSendingUsage = false; - if (hasUsageCollectors) { - this._lastFetchUsageTime = Date.now(); - } - } - this._log.debug(`Uploaded bulk stats payload to the local cluster`); - } catch (err) { - this._log.warn(err.stack); - this._log.warn(`Unable to bulk upload the stats payload to the local cluster`); - } - } else { - this._log.debug(`Skipping bulk uploading of an empty stats payload`); - } - } - - async _onPayload(payload) { - return await sendBulkPayload(this._cluster, this._interval, payload, this._log); - } - - getConvertedKibanaStatuss() { - if (this.kibanaStatus === ServiceStatusLevels.available) { - return 'green'; - } - if (this.kibanaStatus === ServiceStatusLevels.critical) { - return 'red'; - } - if (this.kibanaStatus === ServiceStatusLevels.degraded) { - return 'yellow'; - } - return 'unknown'; - } - - getKibanaStats(type) { - const stats = { - ...this.kibanaStats, - status: this.getConvertedKibanaStatuss(), - }; - - if (type === KIBANA_STATS_TYPE_MONITORING) { - delete stats.port; - delete stats.locale; - } - - return stats; - } - - /* - * Bulk stats are transformed into a bulk upload format - * Non-legacy transformation is done in CollectorSet.toApiStats - * - * Example: - * Before: - * [ - * { - * "type": "kibana_stats", - * "result": { - * "process": { ... }, - * "requests": { ... }, - * ... - * } - * }, - * ] - * - * After: - * [ - * { - * "index": { - * "_type": "kibana_stats" - * } - * }, - * { - * "kibana": { - * "host": "localhost", - * "uuid": "d619c5d1-4315-4f35-b69d-a3ac805489fb", - * "version": "7.0.0-alpha1", - * ... - * }, - * "process": { ... }, - * "requests": { ... }, - * ... - * } - * ] - */ - toBulkUploadFormat(rawData, usageCollection) { - if (rawData.length === 0) { - return []; - } - - // convert the raw data to a nested object by taking each payload through - // its formatter, organizing it per-type - const typesNested = rawData.reduce((accum, { type, result }) => { - const { type: uploadType, payload: uploadData } = usageCollection - .getCollectorByType(type) - .formatForBulkUpload(result); - return defaultsDeep(accum, { [uploadType]: uploadData }); - }, {}); - // convert the nested object into a flat array, with each payload prefixed - // with an 'index' instruction, for bulk upload - const flat = Object.keys(typesNested).reduce((accum, type) => { - return [ - ...accum, - { index: { _type: type } }, - { - kibana: this.getKibanaStats(type), - ...typesNested[type], - }, - ]; - }, []); - - return flat; - } - - static checkPayloadTypesUnique(payload) { - const ids = payload.map((item) => item[0].index._type); - const uniques = uniq(ids); - if (ids.length !== uniques.length) { - throw new Error('Duplicate collector type identifiers found in payload! ' + ids.join(',')); - } - } -} diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.ts new file mode 100644 index 0000000000000..e17d3e58e859c --- /dev/null +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.ts @@ -0,0 +1,274 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Observable, Subscription } from 'rxjs'; +import { take } from 'rxjs/operators'; +import moment from 'moment'; +import { + ElasticsearchServiceSetup, + ILegacyCustomClusterClient, + Logger, + OpsMetrics, + ServiceStatus, + ServiceStatusLevel, + ServiceStatusLevels, +} from '../../../../../src/core/server'; +import { KIBANA_STATS_TYPE_MONITORING, KIBANA_SETTINGS_TYPE } from '../../common/constants'; + +import { sendBulkPayload, monitoringBulk } from './lib'; +import { getKibanaSettings } from './collectors'; +import { MonitoringConfig } from '../config'; + +export interface BulkUploaderOptions { + log: Logger; + config: MonitoringConfig; + interval: number; + elasticsearch: ElasticsearchServiceSetup; + statusGetter$: Observable; + opsMetrics$: Observable; + kibanaStats: KibanaStats; +} + +export interface KibanaStats { + uuid: string; + name: string; + index: string; + host: string; + locale: string; + port: string; + transport_address: string; + version: string; + snapshot: boolean; +} + +/* + * Handles internal Kibana stats collection and uploading data to Monitoring + * bulk endpoint. + * + * NOTE: internal collection will be removed in 7.0 + * + * Depends on + * - 'monitoring.kibana.collection.enabled' config + * - monitoring enabled in ES (checked against xpack_main.info license info change) + * The dependencies are handled upstream + * - Ops Events - essentially Kibana's /api/status + * - Usage Stats - essentially Kibana's /api/stats + * - Kibana Settings - select uiSettings + * @param {Object} server HapiJS server instance + * @param {Object} xpackInfo server.plugins.xpack_main.info object + */ +export class BulkUploader { + private readonly _log: Logger; + private readonly _cluster: ILegacyCustomClusterClient; + private readonly kibanaStats: KibanaStats; + private readonly kibanaStatusGetter$: Subscription; + private readonly opsMetrics$: Observable; + private kibanaStatus: ServiceStatusLevel | null; + private _timer: NodeJS.Timer | null; + private readonly _interval: number; + private readonly config: MonitoringConfig; + constructor({ + log, + config, + interval, + elasticsearch, + statusGetter$, + opsMetrics$, + kibanaStats, + }: BulkUploaderOptions) { + if (typeof interval !== 'number') { + throw new Error('interval number of milliseconds is required'); + } + + this.opsMetrics$ = opsMetrics$; + this.config = config; + + this._timer = null; + this._interval = interval; + this._log = log; + + this._cluster = elasticsearch.legacy.createClient('admin', { + plugins: [monitoringBulk], + }); + + this.kibanaStats = kibanaStats; + + this.kibanaStatus = null; + this.kibanaStatusGetter$ = statusGetter$.subscribe((nextStatus) => { + this.kibanaStatus = nextStatus.level; + }); + } + + /* + * Start the interval timer + * @param {usageCollection} usageCollection object to use for initial the fetch/upload and fetch/uploading on interval + * @return undefined + */ + public start() { + this._log.info('Starting monitoring stats collection'); + + if (this._timer) { + clearInterval(this._timer); + } else { + this._fetchAndUpload(); // initial fetch + } + + this._timer = setInterval(() => { + this._fetchAndUpload(); + }, this._interval); + } + + /* + * start() and stop() are lifecycle event handlers for + * xpackMainPlugin license changes + * @param {String} logPrefix help give context to the reason for stopping + */ + public stop(logPrefix?: string) { + if (this._timer) clearInterval(this._timer); + this._timer = null; + + this.kibanaStatusGetter$.unsubscribe(); + this._cluster.close(); + + const prefix = logPrefix ? logPrefix + ':' : ''; + this._log.info(prefix + 'Monitoring stats collection is stopped'); + } + + public handleNotEnabled() { + this.stop('Monitoring status upload endpoint is not enabled in Elasticsearch'); + } + public handleConnectionLost() { + this.stop('Connection issue detected'); + } + + /** + * Retrieves the OpsMetrics in the same format as the `kibana_stats` collector + * @private + */ + private async getOpsMetrics() { + const { + process: { pid, ...process }, + collected_at: collectedAt, + requests: { statusCodes, ...requests }, + ...lastMetrics + } = await this.opsMetrics$.pipe(take(1)).toPromise(); + return { + ...lastMetrics, + process, + requests, + response_times: { + average: lastMetrics.response_times.avg_in_millis, + max: lastMetrics.response_times.max_in_millis, + }, + timestamp: moment.utc(collectedAt).toISOString(), + }; + } + + private async _fetchAndUpload() { + const data = await Promise.all([ + { type: KIBANA_STATS_TYPE_MONITORING, result: await this.getOpsMetrics() }, + { type: KIBANA_SETTINGS_TYPE, result: await getKibanaSettings(this._log, this.config) }, + ]); + + const payload = this.toBulkUploadFormat(data); + if (payload && payload.length > 0) { + try { + this._log.debug(`Uploading bulk stats payload to the local cluster`); + await this._onPayload(payload); + this._log.debug(`Uploaded bulk stats payload to the local cluster`); + } catch (err) { + this._log.warn(err.stack); + this._log.warn(`Unable to bulk upload the stats payload to the local cluster`); + } + } else { + this._log.debug(`Skipping bulk uploading of an empty stats payload`); + } + } + + private async _onPayload(payload: object[]) { + return await sendBulkPayload(this._cluster, this._interval, payload); + } + + private getConvertedKibanaStatus() { + if (this.kibanaStatus === ServiceStatusLevels.available) { + return 'green'; + } + if (this.kibanaStatus === ServiceStatusLevels.critical) { + return 'red'; + } + if (this.kibanaStatus === ServiceStatusLevels.degraded) { + return 'yellow'; + } + return 'unknown'; + } + + public getKibanaStats(type?: string) { + const stats = { + ...this.kibanaStats, + status: this.getConvertedKibanaStatus(), + }; + + if (type === KIBANA_STATS_TYPE_MONITORING) { + // Do not report the keys `port` and `locale` + const { port, locale, ...rest } = stats; + return rest; + } + + return stats; + } + + /* + * Bulk stats are transformed into a bulk upload format + * Non-legacy transformation is done in CollectorSet.toApiStats + * + * Example: + * Before: + * [ + * { + * "type": "kibana_stats", + * "result": { + * "process": { ... }, + * "requests": { ... }, + * ... + * } + * }, + * ] + * + * After: + * [ + * { + * "index": { + * "_type": "kibana_stats" + * } + * }, + * { + * "kibana": { + * "host": "localhost", + * "uuid": "d619c5d1-4315-4f35-b69d-a3ac805489fb", + * "version": "7.0.0-alpha1", + * ... + * }, + * "process": { ... }, + * "requests": { ... }, + * ... + * } + * ] + */ + private toBulkUploadFormat(rawData: Array<{ type: string; result: any }>) { + // convert the raw data into a flat array, with each payload prefixed + // with an 'index' instruction, for bulk upload + return rawData.reduce((accum, { type, result }) => { + return [ + ...accum, + { index: { _type: type } }, + { + kibana: this.getKibanaStats(type), + ...result, + }, + ]; + }, [] as object[]); + } +} diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts index 2b81f1078ad0a..858c50790fc2e 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Logger } from 'src/core/server'; import { Collector, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { KIBANA_SETTINGS_TYPE } from '../../../common/constants'; @@ -51,6 +52,37 @@ export interface KibanaSettingsCollectorExtraOptions { export type KibanaSettingsCollector = Collector & KibanaSettingsCollectorExtraOptions; +export function getEmailValueStructure(email: string | null) { + return { + xpack: { + default_admin_email: email, + }, + }; +} + +export async function getKibanaSettings(logger: Logger, config: MonitoringConfig) { + let kibanaSettingsData; + const defaultAdminEmail = await checkForEmailValue(config); + + // skip everything if defaultAdminEmail === undefined + if (defaultAdminEmail || (defaultAdminEmail === null && shouldUseNull)) { + kibanaSettingsData = getEmailValueStructure(defaultAdminEmail); + logger.debug( + `[${defaultAdminEmail}] default admin email setting found, sending [${KIBANA_SETTINGS_TYPE}] monitoring document.` + ); + } else { + logger.debug( + `not sending [${KIBANA_SETTINGS_TYPE}] monitoring document because [${defaultAdminEmail}] is null or invalid.` + ); + } + + // remember the current email so that we can mark it as successful if the bulk does not error out + shouldUseNull = !!defaultAdminEmail; + + // returns undefined if there was no result + return kibanaSettingsData; +} + export function getSettingsCollector( usageCollection: UsageCollectionSetup, config: MonitoringConfig @@ -69,33 +101,10 @@ export function getSettingsCollector( }, }, async fetch() { - let kibanaSettingsData; - const defaultAdminEmail = await checkForEmailValue(config); - - // skip everything if defaultAdminEmail === undefined - if (defaultAdminEmail || (defaultAdminEmail === null && shouldUseNull)) { - kibanaSettingsData = this.getEmailValueStructure(defaultAdminEmail); - this.log.debug( - `[${defaultAdminEmail}] default admin email setting found, sending [${KIBANA_SETTINGS_TYPE}] monitoring document.` - ); - } else { - this.log.debug( - `not sending [${KIBANA_SETTINGS_TYPE}] monitoring document because [${defaultAdminEmail}] is null or invalid.` - ); - } - - // remember the current email so that we can mark it as successful if the bulk does not error out - shouldUseNull = !!defaultAdminEmail; - - // returns undefined if there was no result - return kibanaSettingsData; + return getKibanaSettings(this.log, config); }, getEmailValueStructure(email: string | null) { - return { - xpack: { - default_admin_email: email, - }, - }; + return getEmailValueStructure(email); }, }); } diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts index 25e243656898c..5fb1583a5c0db 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts @@ -10,7 +10,7 @@ import { getSettingsCollector } from './get_settings_collector'; import { getMonitoringUsageCollector } from './get_usage_collector'; import { MonitoringConfig } from '../../config'; -export { KibanaSettingsCollector } from './get_settings_collector'; +export { KibanaSettingsCollector, getKibanaSettings } from './get_settings_collector'; export function registerCollectors( usageCollection: UsageCollectionSetup, diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/index.js b/x-pack/plugins/monitoring/server/kibana_monitoring/index.ts similarity index 100% rename from x-pack/plugins/monitoring/server/kibana_monitoring/index.js rename to x-pack/plugins/monitoring/server/kibana_monitoring/index.ts diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/init.js b/x-pack/plugins/monitoring/server/kibana_monitoring/init.ts similarity index 76% rename from x-pack/plugins/monitoring/server/kibana_monitoring/init.js rename to x-pack/plugins/monitoring/server/kibana_monitoring/init.ts index 79aafb8f361f3..c8c5fabb65db0 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/init.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/init.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BulkUploader } from './bulk_uploader'; +import { BulkUploader, BulkUploaderOptions } from './bulk_uploader'; + +export type InitBulkUploaderOptions = Omit; /** * Initialize different types of Kibana Monitoring @@ -15,7 +17,7 @@ import { BulkUploader } from './bulk_uploader'; * @param {Object} kbnServer manager of Kibana services - see `src/legacy/server/kbn_server` in Kibana core * @param {Object} server HapiJS server instance */ -export function initBulkUploader({ config, ...params }) { +export function initBulkUploader({ config, ...params }: InitBulkUploaderOptions) { const interval = config.kibana.collection.interval; return new BulkUploader({ interval, diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/lib/index.js b/x-pack/plugins/monitoring/server/kibana_monitoring/lib/index.ts similarity index 96% rename from x-pack/plugins/monitoring/server/kibana_monitoring/lib/index.js rename to x-pack/plugins/monitoring/server/kibana_monitoring/lib/index.ts index c5fdd29d4306d..a6c5583329861 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/lib/index.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/lib/index.ts @@ -5,4 +5,5 @@ */ export { sendBulkPayload } from './send_bulk_payload'; +// @ts-ignore export { monitoringBulk } from './monitoring_bulk'; diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.js b/x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.ts similarity index 78% rename from x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.js rename to x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.ts index 66799e4aa651a..78d689fe9f182 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.ts @@ -3,12 +3,17 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { ILegacyClusterClient } from 'src/core/server'; import { MONITORING_SYSTEM_API_VERSION, KIBANA_SYSTEM_ID } from '../../../common/constants'; /* * Send the Kibana usage data to the ES Monitoring Bulk endpoint */ -export async function sendBulkPayload(cluster, interval, payload) { +export async function sendBulkPayload( + cluster: ILegacyClusterClient, + interval: number, + payload: object[] +) { return cluster.callAsInternalUser('monitoring.bulk', { system_id: KIBANA_SYSTEM_ID, system_api_version: MONITORING_SYSTEM_API_VERSION, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts index 49307764e9f01..0fa90e1d6fb39 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts @@ -101,7 +101,7 @@ export async function fetchMissingMonitoringData( 'kibana_stats.kibana.name', 'logstash_stats.logstash.host', 'beats_stats.beat.name', - 'beat_stats.beat.type', + 'beats_stats.beat.type', ]; const subAggs = { most_recent: { diff --git a/x-pack/plugins/monitoring/server/plugin.test.ts b/x-pack/plugins/monitoring/server/plugin.test.ts index 3fc494d6c3706..b376fc2eec60b 100644 --- a/x-pack/plugins/monitoring/server/plugin.test.ts +++ b/x-pack/plugins/monitoring/server/plugin.test.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 { coreMock } from 'src/core/server/mocks'; import { Plugin } from './plugin'; import { combineLatest } from 'rxjs'; import { AlertsFactory } from './alerts'; @@ -53,31 +54,9 @@ describe('Monitoring plugin', () => { }, }; - const coreSetup = { - http: { - createRouter: jest.fn(), - getServerInfo: jest.fn().mockImplementation(() => ({ - port: 5601, - })), - basePath: { - serverBasePath: '', - }, - }, - elasticsearch: { - legacy: { - client: {}, - createClient: jest.fn(), - }, - }, - status: { - overall$: { - subscribe: jest.fn(), - }, - }, - savedObjects: { - registerType: jest.fn(), - }, - }; + const coreSetup = coreMock.createSetup(); + coreSetup.http.getServerInfo.mockReturnValue({ port: 5601 } as any); + coreSetup.status.overall$.subscribe = jest.fn(); const setupPlugins = { usageCollection: { @@ -124,7 +103,7 @@ describe('Monitoring plugin', () => { it('always create the bulk uploader', async () => { const plugin = new Plugin(initializerContext as any); - await plugin.setup(coreSetup as any, setupPlugins as any); + await plugin.setup(coreSetup, setupPlugins as any); expect(coreSetup.status.overall$.subscribe).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 8a8e6a867c2e2..af5e1fca76308 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -30,11 +30,8 @@ import { SAVED_OBJECT_TELEMETRY, } from '../common/constants'; import { MonitoringConfig, createConfig, configSchema } from './config'; -// @ts-ignore import { requireUIRoutes } from './routes'; -// @ts-ignore import { initBulkUploader } from './kibana_monitoring'; -// @ts-ignore import { initInfraSource } from './lib/logs/init_infra_source'; import { mbSafeQuery } from './lib/mb_safe_query'; import { instantiateClient } from './es_client/instantiate_client'; @@ -73,7 +70,7 @@ export class Plugin { private licenseService = {} as MonitoringLicenseService; private monitoringCore = {} as MonitoringCore; private legacyShimDependencies = {} as LegacyShimDependencies; - private bulkUploader: IBulkUploader = {} as IBulkUploader; + private bulkUploader: IBulkUploader | undefined; constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; @@ -170,6 +167,7 @@ export class Plugin { elasticsearch: core.elasticsearch, config, log: kibanaMonitoringLog, + opsMetrics$: core.metrics.getOpsMetrics$(), statusGetter$: core.status.overall$, kibanaStats: { uuid: this.initializerContext.env.instanceUuid, @@ -196,7 +194,7 @@ export class Plugin { const monitoringBulkEnabled = mainMonitoring && mainMonitoring.isAvailable && mainMonitoring.isEnabled; if (monitoringBulkEnabled) { - bulkUploader.start(plugins.usageCollection); + bulkUploader.start(); } else { bulkUploader.handleNotEnabled(); } @@ -237,7 +235,7 @@ export class Plugin { return { // OSS stats api needs to call this in order to centralize how // we fetch kibana specific stats - getKibanaStats: () => this.bulkUploader.getKibanaStats(), + getKibanaStats: () => bulkUploader.getKibanaStats(), }; } @@ -250,6 +248,7 @@ export class Plugin { if (this.licenseService) { this.licenseService.stop(); } + this.bulkUploader?.stop(); } registerPluginInUI(plugins: PluginsSetup) { diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index b25daced50b73..a5d7051105797 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -72,6 +72,7 @@ export interface LegacyShimDependencies { export interface IBulkUploader { getKibanaStats: () => any; + stop: () => void; } export interface LegacyRequest { diff --git a/x-pack/plugins/observability/public/pages/landing/index.tsx b/x-pack/plugins/observability/public/pages/landing/index.tsx index 7377a1ca0ea52..b5302d5f17f5c 100644 --- a/x-pack/plugins/observability/public/pages/landing/index.tsx +++ b/x-pack/plugins/observability/public/pages/landing/index.tsx @@ -30,8 +30,8 @@ const EuiCardWithoutPadding = styled(EuiCard)` `; export function LandingPage() { - useTrackPageview({ app: 'observability', path: 'landing' }); - useTrackPageview({ app: 'observability', path: 'landing', delay: 15000 }); + useTrackPageview({ app: 'observability-overview', path: 'landing' }); + useTrackPageview({ app: 'observability-overview', path: 'landing', delay: 15000 }); const { core } = usePluginContext(); const theme = useContext(ThemeContext); diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx index ec00a5b416034..d85bd1a624d7a 100644 --- a/x-pack/plugins/observability/public/pages/overview/index.tsx +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -56,8 +56,8 @@ export function OverviewPage({ routeParams }: Props) { end: getAbsoluteTime(relativeTime.end, { roundUp: true }) as number, }; - useTrackPageview({ app: 'observability', path: 'overview' }); - useTrackPageview({ app: 'observability', path: 'overview', delay: 15000 }); + useTrackPageview({ app: 'observability-overview', path: 'overview' }); + useTrackPageview({ app: 'observability-overview', path: 'overview', delay: 15000 }); const { data: alerts = [], status: alertStatus } = useFetcher(() => { return getObservabilityAlerts({ core }); diff --git a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts index a64e6fc55b85a..70c1eb1859ee3 100644 --- a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts +++ b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts @@ -47,7 +47,7 @@ export type HasData = (params?: HasDataParams) => Promise; export type ObservabilityFetchDataPlugins = Exclude< ObservabilityApp, - 'observability' | 'stack_monitoring' + 'observability-overview' | 'stack_monitoring' >; export interface DataHandler< diff --git a/x-pack/plugins/observability/typings/common.ts b/x-pack/plugins/observability/typings/common.ts index c86eb924a051e..8093d6077148e 100644 --- a/x-pack/plugins/observability/typings/common.ts +++ b/x-pack/plugins/observability/typings/common.ts @@ -9,7 +9,7 @@ export type ObservabilityApp = | 'infra_logs' | 'apm' | 'uptime' - | 'observability' + | 'observability-overview' | 'stack_monitoring' | 'ux'; diff --git a/x-pack/plugins/reporting/server/config/create_config.test.ts b/x-pack/plugins/reporting/server/config/create_config.test.ts index f1257f51f4910..154a05742d747 100644 --- a/x-pack/plugins/reporting/server/config/create_config.test.ts +++ b/x-pack/plugins/reporting/server/config/create_config.test.ts @@ -70,7 +70,7 @@ describe('Reporting server createConfig$', () => { `); expect((mockLogger.warn as any).mock.calls.length).toBe(1); expect((mockLogger.warn as any).mock.calls[0]).toMatchObject([ - 'Generating a random key for xpack.reporting.encryptionKey. To prevent sessions from being invalidated on restart, please set xpack.reporting.encryptionKey in kibana.yml', + 'Generating a random key for xpack.reporting.encryptionKey. To prevent sessions from being invalidated on restart, please set xpack.reporting.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.', ]); }); diff --git a/x-pack/plugins/reporting/server/config/create_config.ts b/x-pack/plugins/reporting/server/config/create_config.ts index 315ac8e8549a7..2e07478c1663c 100644 --- a/x-pack/plugins/reporting/server/config/create_config.ts +++ b/x-pack/plugins/reporting/server/config/create_config.ts @@ -35,7 +35,7 @@ export function createConfig$( i18n.translate('xpack.reporting.serverConfig.randomEncryptionKey', { defaultMessage: 'Generating a random key for xpack.reporting.encryptionKey. To prevent sessions from being invalidated on ' + - 'restart, please set xpack.reporting.encryptionKey in kibana.yml', + 'restart, please set xpack.reporting.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.', }) ); encryptionKey = crypto.randomBytes(16).toString('hex'); diff --git a/x-pack/plugins/saved_objects_tagging/README.md b/x-pack/plugins/saved_objects_tagging/README.md index 5e4281a8c4e7d..0da16746f6494 100644 --- a/x-pack/plugins/saved_objects_tagging/README.md +++ b/x-pack/plugins/saved_objects_tagging/README.md @@ -1,3 +1,53 @@ # SavedObjectsTagging -Add tagging capability to saved objects \ No newline at end of file +Add tagging capability to saved objects + +## Integrating tagging on a new object type + +In addition to use the UI api to plug the tagging feature in your application, there is a couple +things that needs to be done on the server: + +### Add read-access to the `tag` SO type to your feature's capabilities + +In order to be able to fetch the tags assigned to an object, the user must have read permission +for the `tag` saved object type. Which is why all features relying on SO tagging must update +their capabilities. + +```typescript +features.registerKibanaFeature({ + id: 'myFeature', + // ... + privileges: { + all: { + // ... + savedObject: { + all: ['some-type'], + read: ['tag'], // <-- HERE + }, + }, + read: { + // ... + savedObject: { + all: [], + read: ['some-type', 'tag'], // <-- AND HERE + }, + }, + }, +}); +``` + +### Update the SOT telemetry collector schema to add the new type + +The schema is located here: `x-pack/plugins/saved_objects_tagging/server/usage/schema.ts`. You +just need to add the name of the SO type you are adding. + +```ts +export const tagUsageCollectorSchema: MakeSchemaFrom = { + // ... + types: { + dashboard: perTypeSchema, + visualization: perTypeSchema, + // <-- add your type here + }, +}; +``` diff --git a/x-pack/plugins/saved_objects_tagging/kibana.json b/x-pack/plugins/saved_objects_tagging/kibana.json index 89c5e7a134339..134e48a671f28 100644 --- a/x-pack/plugins/saved_objects_tagging/kibana.json +++ b/x-pack/plugins/saved_objects_tagging/kibana.json @@ -6,5 +6,6 @@ "ui": true, "configPath": ["xpack", "saved_object_tagging"], "requiredPlugins": ["features", "management", "savedObjectsTaggingOss"], - "requiredBundles": ["kibanaReact"] + "requiredBundles": ["kibanaReact"], + "optionalPlugins": ["usageCollection"] } diff --git a/x-pack/plugins/saved_objects_tagging/server/plugin.test.mocks.ts b/x-pack/plugins/saved_objects_tagging/server/plugin.test.mocks.ts index 1223b1ec20389..f0c3285667817 100644 --- a/x-pack/plugins/saved_objects_tagging/server/plugin.test.mocks.ts +++ b/x-pack/plugins/saved_objects_tagging/server/plugin.test.mocks.ts @@ -8,3 +8,8 @@ export const registerRoutesMock = jest.fn(); jest.doMock('./routes', () => ({ registerRoutes: registerRoutesMock, })); + +export const createTagUsageCollectorMock = jest.fn(); +jest.doMock('./usage', () => ({ + createTagUsageCollector: createTagUsageCollectorMock, +})); diff --git a/x-pack/plugins/saved_objects_tagging/server/plugin.test.ts b/x-pack/plugins/saved_objects_tagging/server/plugin.test.ts index 1a3e4071f5e09..0730b29cde4a8 100644 --- a/x-pack/plugins/saved_objects_tagging/server/plugin.test.ts +++ b/x-pack/plugins/saved_objects_tagging/server/plugin.test.ts @@ -4,20 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import { registerRoutesMock } from './plugin.test.mocks'; +import { registerRoutesMock, createTagUsageCollectorMock } from './plugin.test.mocks'; import { coreMock } from '../../../../src/core/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; +import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks'; import { SavedObjectTaggingPlugin } from './plugin'; import { savedObjectsTaggingFeature } from './features'; describe('SavedObjectTaggingPlugin', () => { let plugin: SavedObjectTaggingPlugin; let featuresPluginSetup: ReturnType; + let usageCollectionSetup: ReturnType; beforeEach(() => { plugin = new SavedObjectTaggingPlugin(coreMock.createPluginInitializerContext()); featuresPluginSetup = featuresPluginMock.createSetup(); + usageCollectionSetup = usageCollectionPluginMock.createSetupContract(); + // `usageCollection` 'mocked' implementation use the real `CollectorSet` implementation + // that throws when registering things that are not collectors. + // We just want to assert that it was called here, so jest.fn is fine. + usageCollectionSetup.registerCollector = jest.fn(); + }); + + afterEach(() => { + registerRoutesMock.mockReset(); + createTagUsageCollectorMock.mockReset(); }); describe('#setup', () => { @@ -43,5 +55,18 @@ describe('SavedObjectTaggingPlugin', () => { savedObjectsTaggingFeature ); }); + + it('registers the usage collector if `usageCollection` is present', async () => { + const tagUsageCollector = Symbol('saved_objects_tagging'); + createTagUsageCollectorMock.mockReturnValue(tagUsageCollector); + + await plugin.setup(coreMock.createSetup(), { + features: featuresPluginSetup, + usageCollection: usageCollectionSetup, + }); + + expect(usageCollectionSetup.registerCollector).toHaveBeenCalledTimes(1); + expect(usageCollectionSetup.registerCollector).toHaveBeenCalledWith(tagUsageCollector); + }); }); }); diff --git a/x-pack/plugins/saved_objects_tagging/server/plugin.ts b/x-pack/plugins/saved_objects_tagging/server/plugin.ts index 8347fb1f8ef20..6eb8080793d0e 100644 --- a/x-pack/plugins/saved_objects_tagging/server/plugin.ts +++ b/x-pack/plugins/saved_objects_tagging/server/plugin.ts @@ -4,22 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, CoreStart, PluginInitializerContext, Plugin } from 'src/core/server'; +import { Observable } from 'rxjs'; +import { + CoreSetup, + CoreStart, + PluginInitializerContext, + Plugin, + SharedGlobalConfig, +} from 'src/core/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { savedObjectsTaggingFeature } from './features'; import { tagType } from './saved_objects'; import { ITagsRequestHandlerContext } from './types'; -import { registerRoutes } from './routes'; import { TagsRequestHandlerContext } from './request_handler_context'; +import { registerRoutes } from './routes'; +import { createTagUsageCollector } from './usage'; interface SetupDeps { features: FeaturesPluginSetup; + usageCollection?: UsageCollectionSetup; } export class SavedObjectTaggingPlugin implements Plugin<{}, {}, SetupDeps, {}> { - constructor(context: PluginInitializerContext) {} + private readonly legacyConfig$: Observable; - public setup({ savedObjects, http }: CoreSetup, { features }: SetupDeps) { + constructor(context: PluginInitializerContext) { + this.legacyConfig$ = context.config.legacy.globalConfig$; + } + + public setup({ savedObjects, http }: CoreSetup, { features, usageCollection }: SetupDeps) { savedObjects.registerType(tagType); const router = http.createRouter(); @@ -34,6 +48,15 @@ export class SavedObjectTaggingPlugin implements Plugin<{}, {}, SetupDeps, {}> { features.registerKibanaFeature(savedObjectsTaggingFeature); + if (usageCollection) { + usageCollection.registerCollector( + createTagUsageCollector({ + usageCollection, + legacyConfig$: this.legacyConfig$, + }) + ); + } + return {}; } diff --git a/x-pack/plugins/saved_objects_tagging/server/usage/fetch_tag_usage_data.ts b/x-pack/plugins/saved_objects_tagging/server/usage/fetch_tag_usage_data.ts new file mode 100644 index 0000000000000..692088e66003e --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/server/usage/fetch_tag_usage_data.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { ElasticsearchClient } from 'src/core/server'; +import { TaggingUsageData, ByTypeTaggingUsageData } from './types'; + +/** + * Manual type reflection of the `tagDataAggregations` resulting payload + */ +interface AggregatedTagUsageResponseBody { + aggregations: { + by_type: { + buckets: Array<{ + key: string; + doc_count: number; + nested_ref: { + tag_references: { + doc_count: number; + tag_id: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; + }; + }; + }>; + }; + }; +} + +export const fetchTagUsageData = async ({ + esClient, + kibanaIndex, +}: { + esClient: ElasticsearchClient; + kibanaIndex: string; +}): Promise => { + const { body } = await esClient.search({ + index: [kibanaIndex], + ignore_unavailable: true, + filter_path: 'aggregations', + body: { + size: 0, + query: { + bool: { + must: [hasTagReferenceClause], + }, + }, + aggs: tagDataAggregations, + }, + }); + + const byTypeUsages: Record = {}; + const allUsedTags = new Set(); + let totalTaggedObjects = 0; + + const typeBuckets = body.aggregations.by_type.buckets; + typeBuckets.forEach((bucket) => { + const type = bucket.key; + const taggedDocCount = bucket.doc_count; + const usedTagIds = bucket.nested_ref.tag_references.tag_id.buckets.map( + (tagBucket) => tagBucket.key + ); + + totalTaggedObjects += taggedDocCount; + usedTagIds.forEach((tagId) => allUsedTags.add(tagId)); + + byTypeUsages[type] = { + taggedObjects: taggedDocCount, + usedTags: usedTagIds.length, + }; + }); + + return { + usedTags: allUsedTags.size, + taggedObjects: totalTaggedObjects, + types: byTypeUsages, + }; +}; + +const hasTagReferenceClause = { + nested: { + path: 'references', + query: { + bool: { + must: [ + { + term: { + 'references.type': 'tag', + }, + }, + ], + }, + }, + }, +}; + +const tagDataAggregations = { + by_type: { + terms: { + field: 'type', + }, + aggs: { + nested_ref: { + nested: { + path: 'references', + }, + aggs: { + tag_references: { + filter: { + term: { + 'references.type': 'tag', + }, + }, + aggs: { + tag_id: { + terms: { + field: 'references.id', + }, + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/x-pack/plugins/saved_objects_tagging/server/usage/index.ts b/x-pack/plugins/saved_objects_tagging/server/usage/index.ts new file mode 100644 index 0000000000000..023295ab19aef --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/server/usage/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 { createTagUsageCollector } from './tag_usage_collector'; diff --git a/x-pack/plugins/saved_objects_tagging/server/usage/schema.ts b/x-pack/plugins/saved_objects_tagging/server/usage/schema.ts new file mode 100644 index 0000000000000..8132c60daf964 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/server/usage/schema.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 { MakeSchemaFrom } from '../../../../../src/plugins/usage_collection/server'; +import { TaggingUsageData, ByTypeTaggingUsageData } from './types'; + +const perTypeSchema: MakeSchemaFrom = { + usedTags: { type: 'integer' }, + taggedObjects: { type: 'integer' }, +}; + +export const tagUsageCollectorSchema: MakeSchemaFrom = { + usedTags: { type: 'integer' }, + taggedObjects: { type: 'integer' }, + + types: { + dashboard: perTypeSchema, + visualization: perTypeSchema, + map: perTypeSchema, + }, +}; diff --git a/x-pack/plugins/saved_objects_tagging/server/usage/tag_usage_collector.ts b/x-pack/plugins/saved_objects_tagging/server/usage/tag_usage_collector.ts new file mode 100644 index 0000000000000..a38dc46193332 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/server/usage/tag_usage_collector.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { SharedGlobalConfig } from 'src/core/server'; +import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/server'; +import { TaggingUsageData } from './types'; +import { fetchTagUsageData } from './fetch_tag_usage_data'; +import { tagUsageCollectorSchema } from './schema'; + +export const createTagUsageCollector = ({ + usageCollection, + legacyConfig$, +}: { + usageCollection: UsageCollectionSetup; + legacyConfig$: Observable; +}) => { + return usageCollection.makeUsageCollector({ + type: 'saved_objects_tagging', + isReady: () => true, + schema: tagUsageCollectorSchema, + fetch: async ({ esClient }) => { + const { kibana } = await legacyConfig$.pipe(take(1)).toPromise(); + return fetchTagUsageData({ esClient, kibanaIndex: kibana.index }); + }, + }); +}; diff --git a/x-pack/plugins/saved_objects_tagging/server/usage/types.ts b/x-pack/plugins/saved_objects_tagging/server/usage/types.ts new file mode 100644 index 0000000000000..3f6ebb752de13 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/server/usage/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. + */ + +/** + * @internal + */ +export interface TaggingUsageData { + usedTags: number; + taggedObjects: number; + types: Record; +} + +/** + * @internal + */ +export interface ByTypeTaggingUsageData { + usedTags: number; + taggedObjects: number; +} diff --git a/x-pack/plugins/security/kibana.json b/x-pack/plugins/security/kibana.json index 40629dbe4f3b3..f6e7b8bf46a39 100644 --- a/x-pack/plugins/security/kibana.json +++ b/x-pack/plugins/security/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "configPath": ["xpack", "security"], "requiredPlugins": ["data", "features", "licensing", "taskManager", "securityOss"], - "optionalPlugins": ["home", "management", "usageCollection"], + "optionalPlugins": ["home", "management", "usageCollection", "spaces"], "server": true, "ui": true, "requiredBundles": [ diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index e75c0d1c4085f..76a6586e5af80 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -985,12 +985,12 @@ describe('createConfig()', () => { expect(config.encryptionKey).toEqual('ab'.repeat(16)); expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(` - Array [ - Array [ - "Generating a random key for xpack.security.encryptionKey. To prevent sessions from being invalidated on restart, please set xpack.security.encryptionKey in kibana.yml", - ], - ] - `); + Array [ + Array [ + "Generating a random key for xpack.security.encryptionKey. To prevent sessions from being invalidated on restart, please set xpack.security.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.", + ], + ] + `); }); it('should log a warning if SSL is not configured', async () => { diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index 4da0a8598309a..f44c68588fd61 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -247,7 +247,7 @@ export function createConfig( if (encryptionKey === undefined) { logger.warn( 'Generating a random key for xpack.security.encryptionKey. To prevent sessions from being invalidated on ' + - 'restart, please set xpack.security.encryptionKey in kibana.yml' + 'restart, please set xpack.security.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' ); encryptionKey = crypto.randomBytes(16).toString('hex'); diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index cf9a30b0b3857..65f9e76c4ee09 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -114,7 +114,6 @@ describe('Security Plugin', () => { "isEnabled": [Function], "isLicenseAvailable": [Function], }, - "registerSpacesService": [Function], } `); }); diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 52283290ba7b7..17f2480026cc7 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -16,7 +16,7 @@ import { Logger, PluginInitializerContext, } from '../../../../src/core/server'; -import { SpacesPluginSetup } from '../../spaces/server'; +import { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server'; import { PluginSetupContract as FeaturesSetupContract } from '../../features/server'; import { PluginSetupContract as FeaturesPluginSetup, @@ -37,6 +37,7 @@ import { securityFeatures } from './features'; import { ElasticsearchService } from './elasticsearch'; import { SessionManagementService } from './session_management'; import { registerSecurityUsageCollector } from './usage_collector'; +import { setupSpacesClient } from './spaces'; export type SpacesService = Pick< SpacesPluginSetup['spacesService'], @@ -68,16 +69,6 @@ export interface SecurityPluginSetup { >; license: SecurityLicense; audit: AuditServiceSetup; - - /** - * If Spaces plugin is available it's supposed to register its SpacesService with Security plugin - * so that Security can get space ID from the URL or namespace. We can't declare optional dependency - * to Spaces since it'd result into circular dependency between these two plugins and circular - * dependencies aren't supported by the Core. In the future we have to get rid of this implicit - * dependency. - * @param service Spaces service exposed by the Spaces plugin. - */ - registerSpacesService: (service: SpacesService) => void; } export interface PluginSetupDependencies { @@ -86,12 +77,14 @@ export interface PluginSetupDependencies { taskManager: TaskManagerSetupContract; usageCollection?: UsageCollectionSetup; securityOss?: SecurityOssPluginSetup; + spaces?: SpacesPluginSetup; } export interface PluginStartDependencies { features: FeaturesPluginStart; licensing: LicensingPluginStart; taskManager: TaskManagerStartContract; + spaces?: SpacesPluginStart; } /** @@ -99,7 +92,6 @@ export interface PluginStartDependencies { */ export class Plugin { private readonly logger: Logger; - private spacesService?: SpacesService | symbol = Symbol('not accessed'); private securityLicenseService?: SecurityLicenseService; private authc?: Authentication; @@ -121,22 +113,20 @@ export class Plugin { this.initializerContext.logger.get('session') ); - private readonly getSpacesService = () => { - // Changing property value from Symbol to undefined denotes the fact that property was accessed. - if (!this.wasSpacesServiceAccessed()) { - this.spacesService = undefined; - } - - return this.spacesService as SpacesService | undefined; - }; - constructor(private readonly initializerContext: PluginInitializerContext) { this.logger = this.initializerContext.logger.get(); } public async setup( core: CoreSetup, - { features, licensing, taskManager, usageCollection, securityOss }: PluginSetupDependencies + { + features, + licensing, + taskManager, + usageCollection, + securityOss, + spaces, + }: PluginSetupDependencies ) { const [config, legacyConfig] = await combineLatest([ this.initializerContext.config.create>().pipe( @@ -182,7 +172,7 @@ export class Plugin { config: config.audit, logging: core.logging, http: core.http, - getSpaceId: (request) => this.getSpacesService()?.getSpaceId(request), + getSpaceId: (request) => spaces?.spacesService.getSpaceId(request), getCurrentUser: (request) => this.authc?.getCurrentUser(request), }); const legacyAuditLogger = new SecurityAuditLogger(audit.getLogger()); @@ -216,17 +206,23 @@ export class Plugin { kibanaIndexName: legacyConfig.kibana.index, packageVersion: this.initializerContext.env.packageInfo.version, buildNumber: this.initializerContext.env.packageInfo.buildNum, - getSpacesService: this.getSpacesService, + getSpacesService: () => spaces?.spacesService, features, getCurrentUser: this.authc.getCurrentUser, }); + setupSpacesClient({ + spaces, + audit, + authz, + }); + setupSavedObjects({ legacyAuditLogger, audit, authz, savedObjects: core.savedObjects, - getSpacesService: this.getSpacesService, + getSpacesService: () => spaces?.spacesService, }); defineRoutes({ @@ -271,14 +267,6 @@ export class Plugin { }, license, - - registerSpacesService: (service) => { - if (this.wasSpacesServiceAccessed()) { - throw new Error('Spaces service has been accessed before registration.'); - } - - this.spacesService = service; - }, }); } @@ -312,8 +300,4 @@ export class Plugin { this.elasticsearchService.stop(); this.sessionManagementService.stop(); } - - private wasSpacesServiceAccessed() { - return typeof this.spacesService !== 'symbol'; - } } diff --git a/x-pack/plugins/security/server/routes/authorization/index.ts b/x-pack/plugins/security/server/routes/authorization/index.ts index 699ffb5e81ffc..75bfcf65b3965 100644 --- a/x-pack/plugins/security/server/routes/authorization/index.ts +++ b/x-pack/plugins/security/server/routes/authorization/index.ts @@ -7,10 +7,12 @@ import { definePrivilegesRoutes } from './privileges'; import { defineRolesRoutes } from './roles'; import { resetSessionPageRoutes } from './reset_session_page'; +import { defineShareSavedObjectPermissionRoutes } from './spaces'; import { RouteDefinitionParams } from '..'; export function defineAuthorizationRoutes(params: RouteDefinitionParams) { defineRolesRoutes(params); definePrivilegesRoutes(params); resetSessionPageRoutes(params); + defineShareSavedObjectPermissionRoutes(params); } diff --git a/x-pack/plugins/security/server/routes/authorization/spaces/index.ts b/x-pack/plugins/security/server/routes/authorization/spaces/index.ts new file mode 100644 index 0000000000000..eb72a13fd7a15 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/spaces/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 { defineShareSavedObjectPermissionRoutes } from './share_saved_object_permissions'; diff --git a/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.test.ts b/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.test.ts new file mode 100644 index 0000000000000..ccdee8b100039 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.test.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 { + IRouter, + kibanaResponseFactory, + RequestHandler, + RequestHandlerContext, + RouteConfig, +} from '../../../../../../../src/core/server'; +import { defineShareSavedObjectPermissionRoutes } from './share_saved_object_permissions'; + +import { httpServerMock } from '../../../../../../../src/core/server/mocks'; +import { routeDefinitionParamsMock } from '../../index.mock'; +import { RouteDefinitionParams } from '../..'; +import { DeeplyMockedKeys } from '@kbn/utility-types/target/jest'; +import { CheckPrivileges } from '../../../authorization/types'; + +describe('Share Saved Object Permissions', () => { + let router: jest.Mocked; + let routeParamsMock: DeeplyMockedKeys; + + const mockContext = ({ + licensing: { + license: { check: jest.fn().mockReturnValue({ state: 'valid' }) }, + }, + } as unknown) as RequestHandlerContext; + + beforeEach(() => { + routeParamsMock = routeDefinitionParamsMock.create(); + router = routeParamsMock.router as jest.Mocked; + + defineShareSavedObjectPermissionRoutes(routeParamsMock); + }); + + describe('GET /internal/security/_share_saved_object_permissions', () => { + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + beforeEach(() => { + const [shareRouteConfig, shareRouteHandler] = router.get.mock.calls.find( + ([{ path }]) => path === '/internal/security/_share_saved_object_permissions' + )!; + + routeConfig = shareRouteConfig; + routeHandler = shareRouteHandler; + }); + + it('correctly defines route.', () => { + expect(routeConfig.options).toBeUndefined(); + expect(routeConfig.validate).toHaveProperty('query'); + }); + + it('returns `true` when the user is authorized globally', async () => { + const checkPrivilegesWithRequest = jest.fn().mockResolvedValue({ hasAllRequested: true }); + + routeParamsMock.authz.checkPrivilegesWithRequest.mockReturnValue(({ + globally: checkPrivilegesWithRequest, + } as unknown) as CheckPrivileges); + + const request = httpServerMock.createKibanaRequest({ + query: { + type: 'foo-type', + }, + }); + + await expect( + routeHandler(mockContext, request, kibanaResponseFactory) + ).resolves.toMatchObject({ + status: 200, + payload: { + shareToAllSpaces: true, + }, + }); + + expect(routeParamsMock.authz.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + + expect(checkPrivilegesWithRequest).toHaveBeenCalledTimes(1); + expect(checkPrivilegesWithRequest).toHaveBeenCalledWith({ + kibana: routeParamsMock.authz.actions.savedObject.get('foo-type', 'share-to-space'), + }); + }); + + it('returns `false` when the user is not authorized globally', async () => { + const checkPrivilegesWithRequest = jest.fn().mockResolvedValue({ hasAllRequested: false }); + + routeParamsMock.authz.checkPrivilegesWithRequest.mockReturnValue(({ + globally: checkPrivilegesWithRequest, + } as unknown) as CheckPrivileges); + + const request = httpServerMock.createKibanaRequest({ + query: { + type: 'foo-type', + }, + }); + + await expect( + routeHandler(mockContext, request, kibanaResponseFactory) + ).resolves.toMatchObject({ + status: 200, + payload: { + shareToAllSpaces: false, + }, + }); + + expect(routeParamsMock.authz.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + + expect(checkPrivilegesWithRequest).toHaveBeenCalledTimes(1); + expect(checkPrivilegesWithRequest).toHaveBeenCalledWith({ + kibana: routeParamsMock.authz.actions.savedObject.get('foo-type', 'share-to-space'), + }); + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.ts b/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.ts new file mode 100644 index 0000000000000..edfdef34b7fbf --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.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 { schema } from '@kbn/config-schema'; +import { RouteDefinitionParams } from '../../index'; +import { createLicensedRouteHandler } from '../../licensed_route_handler'; +import { wrapIntoCustomErrorResponse } from '../../../errors'; + +export function defineShareSavedObjectPermissionRoutes({ router, authz }: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/_share_saved_object_permissions', + validate: { query: schema.object({ type: schema.string() }) }, + }, + createLicensedRouteHandler(async (context, request, response) => { + let shareToAllSpaces = true; + const { type } = request.query; + + try { + const checkPrivileges = authz.checkPrivilegesWithRequest(request); + shareToAllSpaces = ( + await checkPrivileges.globally({ + kibana: authz.actions.savedObject.get(type, 'share_to_space'), + }) + ).hasAllRequested; + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + return response.ok({ body: { shareToAllSpaces } }); + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/index.mock.ts b/x-pack/plugins/security/server/routes/index.mock.ts index b4698708f86fe..fab4a71df0cb0 100644 --- a/x-pack/plugins/security/server/routes/index.mock.ts +++ b/x-pack/plugins/security/server/routes/index.mock.ts @@ -15,23 +15,26 @@ import { authorizationMock } from '../authorization/index.mock'; import { ConfigSchema, createConfig } from '../config'; import { licenseMock } from '../../common/licensing/index.mock'; import { sessionMock } from '../session_management/session.mock'; +import { RouteDefinitionParams } from '.'; +import { DeeplyMockedKeys } from '@kbn/utility-types/jest'; export const routeDefinitionParamsMock = { - create: (config: Record = {}) => ({ - router: httpServiceMock.createRouter(), - basePath: httpServiceMock.createBasePath(), - csp: httpServiceMock.createSetupContract().csp, - logger: loggingSystemMock.create().get(), - clusterClient: elasticsearchServiceMock.createLegacyClusterClient(), - config: createConfig(ConfigSchema.validate(config), loggingSystemMock.create().get(), { - isTLSEnabled: false, - }), - authc: authenticationMock.create(), - authz: authorizationMock.create(), - license: licenseMock.create(), - httpResources: httpResourcesMock.createRegistrar(), - getFeatures: jest.fn(), - getFeatureUsageService: jest.fn(), - session: sessionMock.create(), - }), + create: (config: Record = {}) => + (({ + router: httpServiceMock.createRouter(), + basePath: httpServiceMock.createBasePath(), + csp: httpServiceMock.createSetupContract().csp, + logger: loggingSystemMock.create().get(), + clusterClient: elasticsearchServiceMock.createLegacyClusterClient(), + config: createConfig(ConfigSchema.validate(config), loggingSystemMock.create().get(), { + isTLSEnabled: false, + }), + authc: authenticationMock.create(), + authz: authorizationMock.create(), + license: licenseMock.create(), + httpResources: httpResourcesMock.createRegistrar(), + getFeatures: jest.fn(), + getFeatureUsageService: jest.fn(), + session: sessionMock.create(), + } as unknown) as DeeplyMockedKeys), }; diff --git a/x-pack/plugins/security/server/spaces/index.ts b/x-pack/plugins/security/server/spaces/index.ts new file mode 100644 index 0000000000000..264cc55a777ca --- /dev/null +++ b/x-pack/plugins/security/server/spaces/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 { setupSpacesClient } from './setup_spaces_client'; diff --git a/x-pack/plugins/spaces/server/lib/audit_logger.test.ts b/x-pack/plugins/security/server/spaces/legacy_audit_logger.test.ts similarity index 87% rename from x-pack/plugins/spaces/server/lib/audit_logger.test.ts rename to x-pack/plugins/security/server/spaces/legacy_audit_logger.test.ts index 94e9a6a35be64..bbd91f0fa8d41 100644 --- a/x-pack/plugins/spaces/server/lib/audit_logger.test.ts +++ b/x-pack/plugins/security/server/spaces/legacy_audit_logger.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 { SpacesAuditLogger } from './audit_logger'; +import { LegacySpacesAuditLogger } from './legacy_audit_logger'; const createMockAuditLogger = () => { return { @@ -14,7 +14,7 @@ const createMockAuditLogger = () => { describe(`#savedObjectsAuthorizationFailure`, () => { test('logs auth failure with spaceIds via auditLogger', () => { const auditLogger = createMockAuditLogger(); - const securityAuditLogger = new SpacesAuditLogger(auditLogger); + const securityAuditLogger = new LegacySpacesAuditLogger(auditLogger); const username = 'foo-user'; const action = 'foo-action'; const spaceIds = ['foo-space-1', 'foo-space-2']; @@ -34,7 +34,7 @@ describe(`#savedObjectsAuthorizationFailure`, () => { test('logs auth failure without spaceIds via auditLogger', () => { const auditLogger = createMockAuditLogger(); - const securityAuditLogger = new SpacesAuditLogger(auditLogger); + const securityAuditLogger = new LegacySpacesAuditLogger(auditLogger); const username = 'foo-user'; const action = 'foo-action'; @@ -54,7 +54,7 @@ describe(`#savedObjectsAuthorizationFailure`, () => { describe(`#savedObjectsAuthorizationSuccess`, () => { test('logs auth success with spaceIds via auditLogger', () => { const auditLogger = createMockAuditLogger(); - const securityAuditLogger = new SpacesAuditLogger(auditLogger); + const securityAuditLogger = new LegacySpacesAuditLogger(auditLogger); const username = 'foo-user'; const action = 'foo-action'; const spaceIds = ['foo-space-1', 'foo-space-2']; @@ -74,7 +74,7 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { test('logs auth success without spaceIds via auditLogger', () => { const auditLogger = createMockAuditLogger(); - const securityAuditLogger = new SpacesAuditLogger(auditLogger); + const securityAuditLogger = new LegacySpacesAuditLogger(auditLogger); const username = 'foo-user'; const action = 'foo-action'; diff --git a/x-pack/plugins/spaces/server/lib/audit_logger.ts b/x-pack/plugins/security/server/spaces/legacy_audit_logger.ts similarity index 78% rename from x-pack/plugins/spaces/server/lib/audit_logger.ts rename to x-pack/plugins/security/server/spaces/legacy_audit_logger.ts index 8110e3fbc6624..88cb30c751045 100644 --- a/x-pack/plugins/spaces/server/lib/audit_logger.ts +++ b/x-pack/plugins/security/server/spaces/legacy_audit_logger.ts @@ -4,14 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAuditLogger } from '../../../security/server'; +import { LegacyAuditLogger } from '../audit'; -export class SpacesAuditLogger { +/** + * @deprecated will be removed in 8.0 + */ +export class LegacySpacesAuditLogger { private readonly auditLogger: LegacyAuditLogger; + /** + * @deprecated will be removed in 8.0 + */ constructor(auditLogger: LegacyAuditLogger = { log() {} }) { this.auditLogger = auditLogger; } + + /** + * @deprecated will be removed in 8.0 + */ public spacesAuthorizationFailure(username: string, action: string, spaceIds?: string[]) { this.auditLogger.log( 'spaces_authorization_failure', @@ -24,6 +34,9 @@ export class SpacesAuditLogger { ); } + /** + * @deprecated will be removed in 8.0 + */ public spacesAuthorizationSuccess(username: string, action: string, spaceIds?: string[]) { this.auditLogger.log( 'spaces_authorization_success', diff --git a/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.test.ts b/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.test.ts new file mode 100644 index 0000000000000..90ee95f518089 --- /dev/null +++ b/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.test.ts @@ -0,0 +1,623 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { httpServerMock } from '../../../../../src/core/server/mocks'; + +import { SecureSpacesClientWrapper } from './secure_spaces_client_wrapper'; + +import { spacesClientMock } from '../../../spaces/server/mocks'; +import { deepFreeze } from '@kbn/std'; +import { Space } from '../../../spaces/server'; +import { authorizationMock } from '../authorization/index.mock'; +import { AuthorizationServiceSetup } from '../authorization'; +import { GetAllSpacesPurpose } from '../../../spaces/common/model/types'; +import { CheckPrivilegesResponse } from '../authorization/types'; +import { LegacySpacesAuditLogger } from './legacy_audit_logger'; +import { SavedObjectsErrorHelpers } from 'src/core/server'; + +interface Opts { + securityEnabled?: boolean; +} + +const spaces = (deepFreeze([ + { + id: 'default', + name: 'Default Space', + disabledFeatures: [], + }, + { + id: 'marketing', + name: 'Marketing Space', + disabledFeatures: [], + }, + { + id: 'sales', + name: 'Sales Space', + disabledFeatures: [], + }, +]) as unknown) as Space[]; + +const setup = ({ securityEnabled = false }: Opts = {}) => { + const baseClient = spacesClientMock.create(); + baseClient.getAll.mockResolvedValue([...spaces]); + + baseClient.get.mockImplementation(async (spaceId: string) => { + const space = spaces.find((s) => s.id === spaceId); + if (!space) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError('space', spaceId); + } + return space; + }); + + const authorization = authorizationMock.create({ + version: 'unit-test', + applicationName: 'kibana', + }); + authorization.mode.useRbacForRequest.mockReturnValue(securityEnabled); + + const legacyAuditLogger = ({ + spacesAuthorizationFailure: jest.fn(), + spacesAuthorizationSuccess: jest.fn(), + } as unknown) as jest.Mocked; + + const request = httpServerMock.createKibanaRequest(); + const wrapper = new SecureSpacesClientWrapper( + baseClient, + request, + authorization, + legacyAuditLogger + ); + return { + authorization, + wrapper, + request, + baseClient, + legacyAuditLogger, + }; +}; + +const expectNoAuthorizationCheck = (authorization: jest.Mocked) => { + expect(authorization.checkPrivilegesDynamicallyWithRequest).not.toHaveBeenCalled(); + expect(authorization.checkPrivilegesWithRequest).not.toHaveBeenCalled(); + expect(authorization.checkSavedObjectsPrivilegesWithRequest).not.toHaveBeenCalled(); +}; + +const expectNoAuditLogging = (auditLogger: jest.Mocked) => { + expect(auditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); + expect(auditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); +}; + +const expectForbiddenAuditLogging = ( + auditLogger: jest.Mocked, + username: string, + operation: string, + spaceId?: string +) => { + expect(auditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(1); + if (spaceId) { + expect(auditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, operation, [ + spaceId, + ]); + } else { + expect(auditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, operation); + } + + expect(auditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); +}; + +const expectSuccessAuditLogging = ( + auditLogger: jest.Mocked, + username: string, + operation: string, + spaceIds?: string[] +) => { + expect(auditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(1); + if (spaceIds) { + expect(auditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith( + username, + operation, + spaceIds + ); + } else { + expect(auditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, operation); + } + + expect(auditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); +}; + +describe('SecureSpacesClientWrapper', () => { + describe('#getAll', () => { + const savedObjects = [ + { + id: 'default', + attributes: { + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + }, + }, + { + id: 'marketing', + attributes: { + name: 'bar-name', + description: 'bar-description', + bar: 'bar-bar', + }, + }, + { + id: 'sales', + attributes: { + name: 'bar-name', + description: 'bar-description', + bar: 'bar-bar', + }, + }, + ]; + + it('delegates to base client when security is not enabled', async () => { + const { wrapper, baseClient, authorization, legacyAuditLogger } = setup({ + securityEnabled: false, + }); + + const response = await wrapper.getAll(); + expect(baseClient.getAll).toHaveBeenCalledTimes(1); + expect(baseClient.getAll).toHaveBeenCalledWith({ purpose: 'any' }); + expect(response).toEqual(spaces); + expectNoAuthorizationCheck(authorization); + expectNoAuditLogging(legacyAuditLogger); + }); + + [ + { + purpose: undefined, + expectedPrivilege: (mockAuthorization: AuthorizationServiceSetup) => [ + mockAuthorization.actions.login, + ], + }, + { + purpose: 'any' as GetAllSpacesPurpose, + expectedPrivilege: (mockAuthorization: AuthorizationServiceSetup) => [ + mockAuthorization.actions.login, + ], + }, + { + purpose: 'copySavedObjectsIntoSpace' as GetAllSpacesPurpose, + expectedPrivilege: (mockAuthorization: AuthorizationServiceSetup) => [ + mockAuthorization.actions.ui.get('savedObjectsManagement', 'copyIntoSpace'), + ], + }, + { + purpose: 'findSavedObjects' as GetAllSpacesPurpose, + expectedPrivilege: (mockAuthorization: AuthorizationServiceSetup) => [ + mockAuthorization.actions.login, + mockAuthorization.actions.savedObject.get('config', 'find'), + ], + }, + { + purpose: 'shareSavedObjectsIntoSpace' as GetAllSpacesPurpose, + expectedPrivilege: (mockAuthorization: AuthorizationServiceSetup) => [ + 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 () => { + const username = 'some-user'; + const { authorization, wrapper, baseClient, request, legacyAuditLogger } = setup({ + securityEnabled: true, + }); + + const privileges = scenario.expectedPrivilege(authorization); + + const checkPrivileges = jest.fn().mockResolvedValue({ + username, + privileges: { + kibana: [ + ...privileges + .map((privilege) => [ + { resource: savedObjects[0].id, privilege, authorized: false }, + { resource: savedObjects[1].id, privilege, authorized: false }, + ]) + .flat(), + ], + }, + } as CheckPrivilegesResponse); + authorization.checkPrivilegesWithRequest.mockReturnValue({ atSpaces: checkPrivileges }); + + await expect(wrapper.getAll({ purpose: scenario.purpose })).rejects.toThrowError( + 'Forbidden' + ); + + expect(baseClient.getAll).toHaveBeenCalledWith({ purpose: scenario.purpose ?? 'any' }); + expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(checkPrivileges).toHaveBeenCalledWith( + savedObjects.map((savedObject) => savedObject.id), + { kibana: privileges } + ); + + expectForbiddenAuditLogging(legacyAuditLogger, username, 'getAll'); + }); + + test(`returns spaces that the user is authorized for`, async () => { + const username = 'some-user'; + const { authorization, wrapper, baseClient, request, legacyAuditLogger } = setup({ + securityEnabled: true, + }); + + const privileges = scenario.expectedPrivilege(authorization); + + const checkPrivileges = jest.fn().mockResolvedValue({ + username, + privileges: { + kibana: [ + ...privileges + .map((privilege) => [ + { resource: savedObjects[0].id, privilege, authorized: true }, + { resource: savedObjects[1].id, privilege, authorized: false }, + ]) + .flat(), + ], + }, + } as CheckPrivilegesResponse); + authorization.checkPrivilegesWithRequest.mockReturnValue({ atSpaces: checkPrivileges }); + + const actualSpaces = await wrapper.getAll({ purpose: scenario.purpose }); + + expect(actualSpaces).toEqual([spaces[0]]); + expect(baseClient.getAll).toHaveBeenCalledWith({ purpose: scenario.purpose ?? 'any' }); + expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(checkPrivileges).toHaveBeenCalledWith( + savedObjects.map((savedObject) => savedObject.id), + { kibana: privileges } + ); + + expectSuccessAuditLogging(legacyAuditLogger, username, 'getAll', [spaces[0].id]); + }); + }); + }); + }); + + describe('#get', () => { + it('delegates to base client when security is not enabled', async () => { + const { wrapper, baseClient, authorization, legacyAuditLogger } = setup({ + securityEnabled: false, + }); + + const response = await wrapper.get('default'); + expect(baseClient.get).toHaveBeenCalledTimes(1); + expect(baseClient.get).toHaveBeenCalledWith('default'); + expect(response).toEqual(spaces[0]); + expectNoAuthorizationCheck(authorization); + expectNoAuditLogging(legacyAuditLogger); + }); + + test(`throws a forbidden error when unauthorized`, async () => { + const username = 'some_user'; + const spaceId = 'default'; + + const { wrapper, baseClient, authorization, legacyAuditLogger, request } = setup({ + securityEnabled: true, + }); + + const checkPrivileges = jest.fn().mockResolvedValue({ + username, + hasAllRequested: false, + privileges: { + kibana: [ + { resource: spaceId, privilege: authorization.actions.login, authorized: false }, + ], + }, + } as CheckPrivilegesResponse); + authorization.checkPrivilegesWithRequest.mockReturnValue({ atSpace: checkPrivileges }); + + await expect(wrapper.get(spaceId)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized to get default space"` + ); + + expect(baseClient.get).not.toHaveBeenCalled(); + + expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + + expect(checkPrivileges).toHaveBeenCalledWith(spaceId, { + kibana: authorization.actions.login, + }); + + expectForbiddenAuditLogging(legacyAuditLogger, username, 'get', spaceId); + }); + + it('returns the space when authorized', async () => { + const username = 'some_user'; + const spaceId = 'default'; + + const { wrapper, baseClient, authorization, legacyAuditLogger, request } = setup({ + securityEnabled: true, + }); + + const checkPrivileges = jest.fn().mockResolvedValue({ + username, + hasAllRequested: true, + privileges: { + kibana: [{ resource: spaceId, privilege: authorization.actions.login, authorized: true }], + }, + } as CheckPrivilegesResponse); + authorization.checkPrivilegesWithRequest.mockReturnValue({ atSpace: checkPrivileges }); + + const response = await wrapper.get(spaceId); + + expect(baseClient.get).toHaveBeenCalledTimes(1); + expect(baseClient.get).toHaveBeenCalledWith(spaceId); + + expect(response).toEqual(spaces[0]); + + expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + + expect(checkPrivileges).toHaveBeenCalledWith(spaceId, { + kibana: authorization.actions.login, + }); + + expectSuccessAuditLogging(legacyAuditLogger, username, 'get', [spaceId]); + }); + }); + + describe('#create', () => { + const space = Object.freeze({ + id: 'new_space', + name: 'new space', + disabledFeatures: [], + }); + + it('delegates to base client when security is not enabled', async () => { + const { wrapper, baseClient, authorization, legacyAuditLogger } = setup({ + securityEnabled: false, + }); + + const response = await wrapper.create(space); + expect(baseClient.create).toHaveBeenCalledTimes(1); + expect(baseClient.create).toHaveBeenCalledWith(space); + expect(response).toEqual(space); + expectNoAuthorizationCheck(authorization); + expectNoAuditLogging(legacyAuditLogger); + }); + + test(`throws a forbidden error when unauthorized`, async () => { + const username = 'some_user'; + + const { wrapper, baseClient, authorization, legacyAuditLogger, request } = setup({ + securityEnabled: true, + }); + + const checkPrivileges = jest.fn().mockResolvedValue({ + username, + hasAllRequested: false, + privileges: { + kibana: [{ privilege: authorization.actions.space.manage, authorized: false }], + }, + } as CheckPrivilegesResponse); + authorization.checkPrivilegesWithRequest.mockReturnValue({ globally: checkPrivileges }); + + await expect(wrapper.create(space)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized to create spaces"` + ); + + expect(baseClient.create).not.toHaveBeenCalled(); + + expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + + expect(checkPrivileges).toHaveBeenCalledWith({ + kibana: authorization.actions.space.manage, + }); + + expectForbiddenAuditLogging(legacyAuditLogger, username, 'create'); + }); + + it('creates the space when authorized', async () => { + const username = 'some_user'; + + const { wrapper, baseClient, authorization, legacyAuditLogger, request } = setup({ + securityEnabled: true, + }); + + const checkPrivileges = jest.fn().mockResolvedValue({ + username, + hasAllRequested: true, + privileges: { + kibana: [{ privilege: authorization.actions.space.manage, authorized: true }], + }, + } as CheckPrivilegesResponse); + authorization.checkPrivilegesWithRequest.mockReturnValue({ globally: checkPrivileges }); + + const response = await wrapper.create(space); + + expect(baseClient.create).toHaveBeenCalledTimes(1); + expect(baseClient.create).toHaveBeenCalledWith(space); + + expect(response).toEqual(space); + + expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + + expect(checkPrivileges).toHaveBeenCalledWith({ + kibana: authorization.actions.space.manage, + }); + + expectSuccessAuditLogging(legacyAuditLogger, username, 'create'); + }); + }); + + describe('#update', () => { + const space = Object.freeze({ + id: 'existing_space', + name: 'existing space', + disabledFeatures: [], + }); + + it('delegates to base client when security is not enabled', async () => { + const { wrapper, baseClient, authorization, legacyAuditLogger } = setup({ + securityEnabled: false, + }); + + const response = await wrapper.update(space.id, space); + expect(baseClient.update).toHaveBeenCalledTimes(1); + expect(baseClient.update).toHaveBeenCalledWith(space.id, space); + expect(response).toEqual(space.id); + expectNoAuthorizationCheck(authorization); + expectNoAuditLogging(legacyAuditLogger); + }); + + test(`throws a forbidden error when unauthorized`, async () => { + const username = 'some_user'; + + const { wrapper, baseClient, authorization, legacyAuditLogger, request } = setup({ + securityEnabled: true, + }); + + const checkPrivileges = jest.fn().mockResolvedValue({ + username, + hasAllRequested: false, + privileges: { + kibana: [{ privilege: authorization.actions.space.manage, authorized: false }], + }, + } as CheckPrivilegesResponse); + authorization.checkPrivilegesWithRequest.mockReturnValue({ globally: checkPrivileges }); + + await expect(wrapper.update(space.id, space)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized to update spaces"` + ); + + expect(baseClient.update).not.toHaveBeenCalled(); + + expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + + expect(checkPrivileges).toHaveBeenCalledWith({ + kibana: authorization.actions.space.manage, + }); + + expectForbiddenAuditLogging(legacyAuditLogger, username, 'update'); + }); + + it('updates the space when authorized', async () => { + const username = 'some_user'; + + const { wrapper, baseClient, authorization, legacyAuditLogger, request } = setup({ + securityEnabled: true, + }); + + const checkPrivileges = jest.fn().mockResolvedValue({ + username, + hasAllRequested: true, + privileges: { + kibana: [{ privilege: authorization.actions.space.manage, authorized: true }], + }, + } as CheckPrivilegesResponse); + authorization.checkPrivilegesWithRequest.mockReturnValue({ globally: checkPrivileges }); + + const response = await wrapper.update(space.id, space); + + expect(baseClient.update).toHaveBeenCalledTimes(1); + expect(baseClient.update).toHaveBeenCalledWith(space.id, space); + + expect(response).toEqual(space.id); + + expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + + expect(checkPrivileges).toHaveBeenCalledWith({ + kibana: authorization.actions.space.manage, + }); + + expectSuccessAuditLogging(legacyAuditLogger, username, 'update'); + }); + }); + + describe('#delete', () => { + const space = Object.freeze({ + id: 'existing_space', + name: 'existing space', + disabledFeatures: [], + }); + + it('delegates to base client when security is not enabled', async () => { + const { wrapper, baseClient, authorization, legacyAuditLogger } = setup({ + securityEnabled: false, + }); + + await wrapper.delete(space.id); + expect(baseClient.delete).toHaveBeenCalledTimes(1); + expect(baseClient.delete).toHaveBeenCalledWith(space.id); + expectNoAuthorizationCheck(authorization); + expectNoAuditLogging(legacyAuditLogger); + }); + + test(`throws a forbidden error when unauthorized`, async () => { + const username = 'some_user'; + + const { wrapper, baseClient, authorization, legacyAuditLogger, request } = setup({ + securityEnabled: true, + }); + + const checkPrivileges = jest.fn().mockResolvedValue({ + username, + hasAllRequested: false, + privileges: { + kibana: [{ privilege: authorization.actions.space.manage, authorized: false }], + }, + } as CheckPrivilegesResponse); + authorization.checkPrivilegesWithRequest.mockReturnValue({ globally: checkPrivileges }); + + await expect(wrapper.delete(space.id)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized to delete spaces"` + ); + + expect(baseClient.delete).not.toHaveBeenCalled(); + + expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + + expect(checkPrivileges).toHaveBeenCalledWith({ + kibana: authorization.actions.space.manage, + }); + + expectForbiddenAuditLogging(legacyAuditLogger, username, 'delete'); + }); + + it('deletes the space when authorized', async () => { + const username = 'some_user'; + + const { wrapper, baseClient, authorization, legacyAuditLogger, request } = setup({ + securityEnabled: true, + }); + + const checkPrivileges = jest.fn().mockResolvedValue({ + username, + hasAllRequested: true, + privileges: { + kibana: [{ privilege: authorization.actions.space.manage, authorized: true }], + }, + } as CheckPrivilegesResponse); + authorization.checkPrivilegesWithRequest.mockReturnValue({ globally: checkPrivileges }); + + await wrapper.delete(space.id); + + expect(baseClient.delete).toHaveBeenCalledTimes(1); + expect(baseClient.delete).toHaveBeenCalledWith(space.id); + + expect(authorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + + expect(checkPrivileges).toHaveBeenCalledWith({ + kibana: authorization.actions.space.manage, + }); + + expectSuccessAuditLogging(legacyAuditLogger, username, 'delete'); + }); + }); +}); diff --git a/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.ts b/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.ts new file mode 100644 index 0000000000000..bd65673422fc1 --- /dev/null +++ b/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.ts @@ -0,0 +1,204 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 Boom from '@hapi/boom'; +import { KibanaRequest } from 'src/core/server'; +import { GetAllSpacesPurpose, GetSpaceResult } from '../../../spaces/common/model/types'; +import { Space, ISpacesClient } from '../../../spaces/server'; +import { LegacySpacesAuditLogger } from './legacy_audit_logger'; +import { AuthorizationServiceSetup } from '../authorization'; +import { SecurityPluginSetup } from '..'; + +const PURPOSE_PRIVILEGE_MAP: Record< + GetAllSpacesPurpose, + (authorization: SecurityPluginSetup['authz']) => string[] +> = { + any: (authorization) => [authorization.actions.login], + copySavedObjectsIntoSpace: (authorization) => [ + authorization.actions.ui.get('savedObjectsManagement', 'copyIntoSpace'), + ], + findSavedObjects: (authorization) => { + return [authorization.actions.login, authorization.actions.savedObject.get('config', 'find')]; + }, + shareSavedObjectsIntoSpace: (authorization) => [ + authorization.actions.ui.get('savedObjectsManagement', 'shareIntoSpace'), + ], +}; + +interface GetAllSpacesOptions { + purpose?: GetAllSpacesPurpose; + includeAuthorizedPurposes?: boolean; +} + +export class SecureSpacesClientWrapper implements ISpacesClient { + private readonly useRbac = this.authorization.mode.useRbacForRequest(this.request); + + constructor( + private readonly spacesClient: ISpacesClient, + private readonly request: KibanaRequest, + private readonly authorization: AuthorizationServiceSetup, + private readonly legacyAuditLogger: LegacySpacesAuditLogger + ) {} + + public async getAll({ + purpose = 'any', + includeAuthorizedPurposes, + }: GetAllSpacesOptions = {}): Promise { + const allSpaces = await this.spacesClient.getAll({ purpose, includeAuthorizedPurposes }); + + if (!this.useRbac) { + return allSpaces; + } + + const spaceIds = allSpaces.map((space: Space) => space.id); + + const checkPrivileges = this.authorization.checkPrivilegesWithRequest(this.request); + + // Collect all privileges which need to be checked + const allPrivileges = Object.entries(PURPOSE_PRIVILEGE_MAP).reduce( + (acc, [getSpacesPurpose, privilegeFactory]) => + !includeAuthorizedPurposes && getSpacesPurpose !== purpose + ? acc + : { ...acc, [getSpacesPurpose]: privilegeFactory(this.authorization) }, + {} as Record + ); + + // Check all privileges against all spaces + const { username, privileges } = await checkPrivileges.atSpaces(spaceIds, { + kibana: Object.values(allPrivileges).flat(), + }); + + // Determine which purposes the user is authorized for within each space. + // Remove any spaces for which user is fully unauthorized. + const checkHasAllRequired = (space: Space, actions: string[]) => + actions.every((action) => + privileges.kibana.some( + ({ resource, privilege, authorized }) => + resource === space.id && privilege === action && authorized + ) + ); + const authorizedSpaces: GetSpaceResult[] = allSpaces + .map((space: Space) => { + if (!includeAuthorizedPurposes) { + // Check if the user is authorized for a single purpose + const requiredActions = PURPOSE_PRIVILEGE_MAP[purpose](this.authorization); + return checkHasAllRequired(space, requiredActions) ? space : null; + } + + // Check if the user is authorized for each purpose + let hasAnyAuthorization = false; + const authorizedPurposes = Object.entries(PURPOSE_PRIVILEGE_MAP).reduce( + (acc, [purposeKey, privilegeFactory]) => { + const requiredActions = privilegeFactory(this.authorization); + const hasAllRequired = checkHasAllRequired(space, requiredActions); + hasAnyAuthorization = hasAnyAuthorization || hasAllRequired; + return { ...acc, [purposeKey]: hasAllRequired }; + }, + {} as Record + ); + + if (!hasAnyAuthorization) { + return null; + } + return { ...space, authorizedPurposes }; + }) + .filter(this.filterUnauthorizedSpaceResults); + + if (authorizedSpaces.length === 0) { + this.legacyAuditLogger.spacesAuthorizationFailure(username, 'getAll'); + throw Boom.forbidden(); // Note: there is a catch for this in `SpacesSavedObjectsClient.find`; if we get rid of this error, remove that too + } + + const authorizedSpaceIds = authorizedSpaces.map((space) => space.id); + this.legacyAuditLogger.spacesAuthorizationSuccess(username, 'getAll', authorizedSpaceIds); + + return authorizedSpaces; + } + + public async get(id: string) { + if (this.useRbac) { + await this.ensureAuthorizedAtSpace( + id, + this.authorization.actions.login, + 'get', + `Unauthorized to get ${id} space` + ); + } + + return this.spacesClient.get(id); + } + + public async create(space: Space) { + if (this.useRbac) { + await this.ensureAuthorizedGlobally( + this.authorization.actions.space.manage, + 'create', + 'Unauthorized to create spaces' + ); + } + + return this.spacesClient.create(space); + } + + public async update(id: string, space: Space) { + if (this.useRbac) { + await this.ensureAuthorizedGlobally( + this.authorization.actions.space.manage, + 'update', + 'Unauthorized to update spaces' + ); + } + + return this.spacesClient.update(id, space); + } + + public async delete(id: string) { + if (this.useRbac) { + await this.ensureAuthorizedGlobally( + this.authorization.actions.space.manage, + 'delete', + 'Unauthorized to delete spaces' + ); + } + + return this.spacesClient.delete(id); + } + + private async ensureAuthorizedGlobally(action: string, method: string, forbiddenMessage: string) { + const checkPrivileges = this.authorization.checkPrivilegesWithRequest(this.request); + const { username, hasAllRequested } = await checkPrivileges.globally({ kibana: action }); + + if (hasAllRequested) { + this.legacyAuditLogger.spacesAuthorizationSuccess(username, method); + } else { + this.legacyAuditLogger.spacesAuthorizationFailure(username, method); + throw Boom.forbidden(forbiddenMessage); + } + } + + private async ensureAuthorizedAtSpace( + spaceId: string, + action: string, + method: string, + forbiddenMessage: string + ) { + const checkPrivileges = this.authorization.checkPrivilegesWithRequest(this.request); + const { username, hasAllRequested } = await checkPrivileges.atSpace(spaceId, { + kibana: action, + }); + + if (hasAllRequested) { + this.legacyAuditLogger.spacesAuthorizationSuccess(username, method, [spaceId]); + } else { + this.legacyAuditLogger.spacesAuthorizationFailure(username, method, [spaceId]); + throw Boom.forbidden(forbiddenMessage); + } + } + + private filterUnauthorizedSpaceResults(value: GetSpaceResult | null): value is GetSpaceResult { + return value !== null; + } +} diff --git a/x-pack/plugins/security/server/spaces/setup_spaces_client.test.ts b/x-pack/plugins/security/server/spaces/setup_spaces_client.test.ts new file mode 100644 index 0000000000000..ee17f366583ba --- /dev/null +++ b/x-pack/plugins/security/server/spaces/setup_spaces_client.test.ts @@ -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 { coreMock, httpServerMock } from '../../../../../src/core/server/mocks'; + +import { spacesMock } from '../../../spaces/server/mocks'; + +import { auditServiceMock } from '../audit/index.mock'; +import { authorizationMock } from '../authorization/index.mock'; +import { setupSpacesClient } from './setup_spaces_client'; + +describe('setupSpacesClient', () => { + it('does not setup the spaces client when spaces is disabled', () => { + const authz = authorizationMock.create(); + const audit = auditServiceMock.create(); + + setupSpacesClient({ authz, audit }); + + expect(audit.getLogger).not.toHaveBeenCalled(); + }); + + it('configures the repository factory, wrapper, and audit logger', () => { + const authz = authorizationMock.create(); + const audit = auditServiceMock.create(); + const spaces = spacesMock.createSetup(); + + setupSpacesClient({ authz, audit, spaces }); + + expect(spaces.spacesClient.registerClientWrapper).toHaveBeenCalledTimes(1); + expect(spaces.spacesClient.setClientRepositoryFactory).toHaveBeenCalledTimes(1); + expect(audit.getLogger).toHaveBeenCalledTimes(1); + }); + + it('creates a factory that creates an internal repository when RBAC is used for the request', () => { + const authz = authorizationMock.create(); + const audit = auditServiceMock.create(); + const spaces = spacesMock.createSetup(); + + const { savedObjects } = coreMock.createStart(); + + setupSpacesClient({ authz, audit, spaces }); + + expect(spaces.spacesClient.setClientRepositoryFactory).toHaveBeenCalledTimes(1); + const [repositoryFactory] = spaces.spacesClient.setClientRepositoryFactory.mock.calls[0]; + + const request = httpServerMock.createKibanaRequest(); + authz.mode.useRbacForRequest.mockReturnValueOnce(true); + + repositoryFactory(request, savedObjects); + + expect(savedObjects.createInternalRepository).toHaveBeenCalledTimes(1); + expect(savedObjects.createInternalRepository).toHaveBeenCalledWith(['space']); + expect(savedObjects.createScopedRepository).not.toHaveBeenCalled(); + }); + + it('creates a factory that creates a scoped repository when RBAC is NOT used for the request', () => { + const authz = authorizationMock.create(); + const audit = auditServiceMock.create(); + const spaces = spacesMock.createSetup(); + + const { savedObjects } = coreMock.createStart(); + + setupSpacesClient({ authz, audit, spaces }); + + expect(spaces.spacesClient.setClientRepositoryFactory).toHaveBeenCalledTimes(1); + const [repositoryFactory] = spaces.spacesClient.setClientRepositoryFactory.mock.calls[0]; + + const request = httpServerMock.createKibanaRequest(); + authz.mode.useRbacForRequest.mockReturnValueOnce(false); + + repositoryFactory(request, savedObjects); + + expect(savedObjects.createInternalRepository).not.toHaveBeenCalled(); + expect(savedObjects.createScopedRepository).toHaveBeenCalledTimes(1); + expect(savedObjects.createScopedRepository).toHaveBeenCalledWith(request, ['space']); + }); +}); diff --git a/x-pack/plugins/security/server/spaces/setup_spaces_client.ts b/x-pack/plugins/security/server/spaces/setup_spaces_client.ts new file mode 100644 index 0000000000000..f9b105d630516 --- /dev/null +++ b/x-pack/plugins/security/server/spaces/setup_spaces_client.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 { SpacesPluginSetup } from '../../../spaces/server'; +import { AuditServiceSetup } from '../audit'; +import { AuthorizationServiceSetup } from '../authorization'; +import { LegacySpacesAuditLogger } from './legacy_audit_logger'; +import { SecureSpacesClientWrapper } from './secure_spaces_client_wrapper'; + +interface Deps { + audit: AuditServiceSetup; + authz: AuthorizationServiceSetup; + spaces?: SpacesPluginSetup; +} + +export const setupSpacesClient = ({ audit, authz, spaces }: Deps) => { + if (!spaces) { + return; + } + const { spacesClient } = spaces; + + spacesClient.setClientRepositoryFactory((request, savedObjectsStart) => { + if (authz.mode.useRbacForRequest(request)) { + return savedObjectsStart.createInternalRepository(['space']); + } + return savedObjectsStart.createScopedRepository(request, ['space']); + }); + + const spacesAuditLogger = new LegacySpacesAuditLogger(audit.getLogger()); + + spacesClient.registerClientWrapper( + (request, baseClient) => + new SecureSpacesClientWrapper(baseClient, request, authz, spacesAuditLogger) + ); +}; diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 8c423c663a4e8..e58aed15a8a10 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -165,6 +165,9 @@ export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ '.slack', '.pagerduty', '.webhook', + '.servicenow', + '.jira', + '.resilient', ]; export const NOTIFICATION_THROTTLE_NO_ACTIONS = 'no_actions'; export const NOTIFICATION_THROTTLE_RULE = 'rule'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/types.ts b/x-pack/plugins/security_solution/common/detection_engine/types.ts index 6099a34f9afd1..9e4c71d5eb116 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/types.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/types.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 { AlertAction } from '../../../alerts/common'; export type RuleAlertAction = Omit & { diff --git a/x-pack/plugins/security_solution/common/test/index.ts b/x-pack/plugins/security_solution/common/test/index.ts new file mode 100644 index 0000000000000..2fa5fa4ada45a --- /dev/null +++ b/x-pack/plugins/security_solution/common/test/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// For the source of these roles please consult the PR these were introduced https://github.com/elastic/kibana/pull/81866#issue-511165754 +export enum ROLES { + t1_analyst = 't1_analyst', + t2_analyst = 't2_analyst', + hunter = 'hunter', + rule_author = 'rule_author', + soc_manager = 'soc_manager', + platform_engineer = 'platform_engineer', + detections_admin = 'detections_admin', +} + +export type RolesType = keyof typeof ROLES; diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts index 36dc38b684742..db841d2a732c4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts @@ -30,8 +30,7 @@ import { loginAndWaitForPage } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; -// FLAKY: https://github.com/elastic/kibana/issues/83773 -describe.skip('Alerts', () => { +describe('Alerts', () => { context('Closing alerts', () => { beforeEach(() => { esArchiverLoad('alerts'); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index 83f1a02aceeb8..fb1f2920aaceb 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -114,8 +114,7 @@ const expectedEditedtags = editedRule.tags.join(''); const expectedEditedIndexPatterns = editedRule.index && editedRule.index.length ? editedRule.index : indexPatterns; -// SKIP: https://github.com/elastic/kibana/issues/83769 -describe.skip('Custom detection rules creation', () => { +describe('Custom detection rules creation', () => { before(() => { esArchiverLoad('timeline'); }); @@ -216,8 +215,7 @@ describe.skip('Custom detection rules creation', () => { }); }); -// FLAKY: https://github.com/elastic/kibana/issues/83793 -describe.skip('Custom detection rules deletion and edition', () => { +describe('Custom detection rules deletion and edition', () => { beforeEach(() => { esArchiverLoad('custom_rules'); loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts index 6f995045dfc6a..eb8448233c624 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts @@ -17,8 +17,7 @@ import { DETECTIONS_URL } from '../urls/navigation'; const EXPECTED_EXPORTED_RULE_FILE_PATH = 'cypress/test_files/expected_rules_export.ndjson'; -// SKIP: https://github.com/elastic/kibana/issues/83769 -describe.skip('Export rules', () => { +describe('Export rules', () => { before(() => { esArchiverLoad('export_rule'); cy.server(); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts index c28c4e842e08b..31d8e4666d91d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts @@ -17,8 +17,7 @@ import { loginAndWaitForPage } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; -// FLAKY: https://github.com/elastic/kibana/issues/83771 -describe.skip('Alerts timeline', () => { +describe('Alerts timeline', () => { beforeEach(() => { esArchiverLoad('timeline_alerts'); loginAndWaitForPage(DETECTIONS_URL); diff --git a/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts index 1bba390780264..ed885ad653e5d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts @@ -17,8 +17,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { CASES_URL } from '../urls/navigation'; -// FLAKY: https://github.com/elastic/kibana/issues/65278 -describe.skip('Cases connectors', () => { +describe('Cases connectors', () => { before(() => { cy.server(); cy.route('POST', '**/api/actions/action').as('createConnector'); diff --git a/x-pack/plugins/security_solution/cypress/integration/value_lists.spec.ts b/x-pack/plugins/security_solution/cypress/integration/value_lists.spec.ts index f4de6d978a70d..403538a37f523 100644 --- a/x-pack/plugins/security_solution/cypress/integration/value_lists.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/value_lists.spec.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; +import { ROLES } from '../../common/test'; +import { deleteRoleAndUser, loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; import { waitForAlertsPanelToBeLoaded, @@ -24,7 +25,7 @@ import { deleteValueListsFile, exportValueList, } from '../tasks/lists'; -import { VALUE_LISTS_TABLE, VALUE_LISTS_ROW } from '../screens/lists'; +import { VALUE_LISTS_TABLE, VALUE_LISTS_ROW, VALUE_LISTS_MODAL_ACTIVATOR } from '../screens/lists'; describe('value lists', () => { describe('management modal', () => { @@ -220,4 +221,19 @@ describe('value lists', () => { }); }); }); + + describe('user with restricted access role', () => { + beforeEach(() => { + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL, ROLES.t1_analyst); + goToManageAlertsDetectionRules(); + }); + + afterEach(() => { + deleteRoleAndUser(ROLES.t1_analyst); + }); + + it('Does not allow a t1 analyst user to upload a value list', () => { + cy.get(VALUE_LISTS_MODAL_ACTIVATOR).should('have.attr', 'disabled'); + }); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/login.ts b/x-pack/plugins/security_solution/cypress/tasks/login.ts index 65f821ec5bfb7..9f385d9ccd2fc 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/login.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/login.ts @@ -5,6 +5,9 @@ */ import * as yaml from 'js-yaml'; +import Url, { UrlObject } from 'url'; + +import { RolesType } from '../../common/test'; import { TIMELINE_FLYOUT_BODY } from '../screens/timeline'; /** @@ -42,6 +45,89 @@ const ELASTICSEARCH_PASSWORD = 'ELASTICSEARCH_PASSWORD'; */ const LOGIN_API_ENDPOINT = '/internal/security/login'; +/** + * cy.visit will default to the baseUrl which uses the default kibana test user + * This function will override that functionality in cy.visit by building the baseUrl + * directly from the environment variables set up in x-pack/test/security_solution_cypress/runner.ts + * + * @param role string role/user to log in with + * @param route string route to visit + */ +export const getUrlWithRoute = (role: RolesType, route: string) => { + const theUrl = `${Url.format({ + auth: `${role}:changeme`, + username: role, + password: 'changeme', + protocol: Cypress.env('protocol'), + hostname: Cypress.env('hostname'), + port: Cypress.env('configport'), + } as UrlObject)}${route.startsWith('/') ? '' : '/'}${route}`; + cy.log(`origin: ${theUrl}`); + return theUrl; +}; + +export const getCurlScriptEnvVars = () => ({ + ELASTICSEARCH_URL: Cypress.env('ELASTICSEARCH_URL'), + ELASTICSEARCH_USERNAME: Cypress.env('ELASTICSEARCH_USERNAME'), + ELASTICSEARCH_PASSWORD: Cypress.env('ELASTICSEARCH_PASSWORD'), + KIBANA_URL: Cypress.env('KIBANA_URL'), +}); + +export const postRoleAndUser = (role: RolesType) => { + const env = getCurlScriptEnvVars(); + const detectionsRoleScriptPath = `./server/lib/detection_engine/scripts/roles_users/${role}/post_detections_role.sh`; + const detectionsRoleJsonPath = `./server/lib/detection_engine/scripts/roles_users/${role}/detections_role.json`; + const detectionsUserScriptPath = `./server/lib/detection_engine/scripts/roles_users/${role}/post_detections_user.sh`; + const detectionsUserJsonPath = `./server/lib/detection_engine/scripts/roles_users/${role}/detections_user.json`; + + // post the role + cy.exec(`bash ${detectionsRoleScriptPath} ${detectionsRoleJsonPath}`, { + env, + }); + + // post the user associated with the role to elasticsearch + cy.exec(`bash ${detectionsUserScriptPath} ${detectionsUserJsonPath}`, { + env, + }); +}; + +export const deleteRoleAndUser = (role: RolesType) => { + const env = getCurlScriptEnvVars(); + const detectionsUserDeleteScriptPath = `./server/lib/detection_engine/scripts/roles_users/${role}/delete_detections_user.sh`; + + // delete the role + cy.exec(`bash ${detectionsUserDeleteScriptPath}`, { + env, + }); +}; + +export const loginWithRole = async (role: RolesType) => { + postRoleAndUser(role); + const theUrl = Url.format({ + auth: `${role}:changeme`, + username: role, + password: 'changeme', + protocol: Cypress.env('protocol'), + hostname: Cypress.env('hostname'), + port: Cypress.env('configport'), + } as UrlObject); + cy.log(`origin: ${theUrl}`); + cy.request({ + body: { + providerType: 'basic', + providerName: 'basic', + currentURL: '/', + params: { + username: role, + password: 'changeme', + }, + }, + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'POST', + url: getUrlWithRoute(role, LOGIN_API_ENDPOINT), + }); +}; + /** * Authenticates with Kibana using, if specified, credentials specified by * environment variables. The credentials in `kibana.dev.yml` will be used @@ -50,8 +136,10 @@ const LOGIN_API_ENDPOINT = '/internal/security/login'; * To speed the execution of tests, prefer this non-interactive authentication, * which is faster than authentication via Kibana's interactive login page. */ -export const login = () => { - if (credentialsProvidedByEnvironment()) { +export const login = (role?: RolesType) => { + if (role != null) { + loginWithRole(role); + } else if (credentialsProvidedByEnvironment()) { loginViaEnvironmentCredentials(); } else { loginViaConfig(); @@ -129,8 +217,8 @@ const loginViaConfig = () => { * Authenticates with Kibana, visits the specified `url`, and waits for the * Kibana global nav to be displayed before continuing */ -export const loginAndWaitForPage = (url: string) => { - login(); +export const loginAndWaitForPage = (url: string, role?: RolesType) => { + login(role); cy.viewport('macbook-15'); cy.visit( `${url}?timerange=(global:(linkTo:!(timeline),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)))` @@ -138,17 +226,19 @@ export const loginAndWaitForPage = (url: string) => { cy.get('[data-test-subj="headerGlobalNav"]'); }; -export const loginAndWaitForPageWithoutDateRange = (url: string) => { - login(); +export const loginAndWaitForPageWithoutDateRange = (url: string, role?: RolesType) => { + login(role); cy.viewport('macbook-15'); - cy.visit(url); + cy.visit(role ? getUrlWithRoute(role, url) : url); cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 }); }; -export const loginAndWaitForTimeline = (timelineId: string) => { - login(); +export const loginAndWaitForTimeline = (timelineId: string, role?: RolesType) => { + const route = `/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t)`; + + login(role); cy.viewport('macbook-15'); - cy.visit(`/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t)`); + cy.visit(role ? getUrlWithRoute(role, route) : route); cy.get('[data-test-subj="headerGlobalNav"]'); cy.get(TIMELINE_FLYOUT_BODY).should('be.visible'); }; diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 97410d8a97cef..048f3846cc322 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -10,7 +10,7 @@ "build-graphql-types": "node scripts/generate_types_from_graphql.js", "cypress:open": "../../../node_modules/.bin/cypress open --config-file ./cypress/cypress.json", "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/visual_config.ts", - "cypress:run": "../../../node_modules/.bin/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 ../../../target/kibana-security-solution/cypress/results/mochawesome*.json > ../../../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": "../../../node_modules/.bin/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 ../../../target/kibana-security-solution/cypress/results/mochawesome*.json > ../../../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 --max-old-space-size=2048 ../../../scripts/functional_tests --config ../../test/security_solution_cypress/cli_config.ts", "test:generate": "node scripts/endpoint/resolver_generator" } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx index b4b82b7f692b9..e4e03e9453f7a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx @@ -48,7 +48,7 @@ export const AdvancedPolicyForms = React.memo(() => { /> - + {AdvancedPolicySchema.map((advancedField, index) => { const configPath = advancedField.key.split('.'); return ( @@ -114,7 +114,12 @@ const PolicyAdvanced = React.memo( } > - + ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/README.md new file mode 100644 index 0000000000000..cb38a23ebdea8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/README.md @@ -0,0 +1,12 @@ +1. When first starting up elastic, detections will not be available until you visit the page with a SOC Manager role or Platform Engineer role +2. I gave the Hunter role "all" privileges for saved objects management and builtInAlerts so that they can create rules. +3. Rule Author has the ability to create rules and create value lists + +| Role | Data Sources | Security Solution ML Jobs/Results | Lists | Rules/Exceptions | Action Connectors | Signals/Alerts | +| :------------------------------------------: | :----------: | :-------------------------------: | :---------: | :--------------: | :---------------: | :------------------------------: | +| T1 Analyst | read | read | none | read | read | read, write | +| T2 Analyst | read | read | read | read | read | read, write | +| Hunter / T3 Analyst | read, write | read | read | read, write | read | read, write | +| Rule Author / Manager / Detections Engineer | read, write | read | read, write | read, write | read | read, write, view_index_metadata | +| SOC Manager | read, write | read | read, write | read, write | all | read, write, manage | +| Platform Engineer (data ingest, cluster ops) | read, write | all | all | read, write | all | all | diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/README.md new file mode 100644 index 0000000000000..2ebcedcc75d95 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/README.md @@ -0,0 +1 @@ +This user contains all the possible privileges listed in our detections privileges docs https://www.elastic.co/guide/en/security/current/detections-permissions-section.html This user has higher privileges than the Platform Engineer user diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/delete_detections_user.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/delete_detections_user.sh new file mode 100755 index 0000000000000..d17d4792af4c5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/delete_detections_user.sh @@ -0,0 +1,10 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +curl -v -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XDELETE ${ELASTICSEARCH_URL}/_security/user/detections_admin diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json new file mode 100644 index 0000000000000..357b8cde8ad10 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json @@ -0,0 +1,35 @@ +{ + "elasticsearch": { + "cluster": ["manage"], + "indices": [ + { + "names": [ + ".siem-signals-*", + ".lists*", + ".items*", + "apm-*-transaction*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "privileges": ["manage", "write", "read"] + } + ] + }, + "kibana": [ + { + "feature": { + "ml": ["all"], + "siem": ["all"], + "actions": ["read"], + "builtInAlerts": ["all"], + "dev_tools": ["all"], + "savedObjectsManagement": ["all"] + }, + "spaces": ["*"] + } + ] +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_user.json new file mode 100644 index 0000000000000..9910d9b516a20 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_user.json @@ -0,0 +1,6 @@ +{ + "password": "changeme", + "roles": ["detections_admin"], + "full_name": "Detections User", + "email": "detections-user@example.com" +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/get_detections_role.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/get_detections_role.sh new file mode 100755 index 0000000000000..f64e9d888fe66 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/get_detections_role.sh @@ -0,0 +1,10 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XGET ${KIBANA_URL}/api/security/role/detections_admin | jq -S . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/post_detections_role.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/post_detections_role.sh new file mode 100755 index 0000000000000..318fca59a85a6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/post_detections_role.sh @@ -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. +# + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XPUT ${KIBANA_URL}/api/security/role/detections_admin \ +-d @detections_role.json diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/post_detections_user.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/post_detections_user.sh new file mode 100755 index 0000000000000..2561888f447a1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/post_detections_user.sh @@ -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. +# + +USER=(${@:-./detections_user.json}) + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + ${ELASTICSEARCH_URL}/_security/user/detections_admin \ +-d @${USER} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/README.md new file mode 100644 index 0000000000000..f0060fb006e32 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/README.md @@ -0,0 +1,12 @@ +This user can CRUD rules and signals. The main difference here is the user has + +```json +"builtInAlerts": ["all"], +"savedObjectsManagement": ["all"] +``` + +privileges whereas the T1 and T2 have "read" privileges which prevents them from creating rules + +| Role | Data Sources | Security Solution ML Jobs/Results | Lists | Rules/Exceptions | Action Connectors | Signals/Alerts | +| :-----------------: | :----------: | :------------------: | :---: | :--------------: | :---------------: | :------------: | +| Hunter / T3 Analyst | read, write | read | read | read, write | read | read, write | diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/delete_detections_user.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/delete_detections_user.sh new file mode 100755 index 0000000000000..04146cf20f8ec --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/delete_detections_user.sh @@ -0,0 +1,10 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +curl -v -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XDELETE ${ELASTICSEARCH_URL}/_security/user/hunter diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json new file mode 100644 index 0000000000000..f5482643fb268 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json @@ -0,0 +1,39 @@ +{ + "elasticsearch": { + "cluster": [], + "indices": [ + { + "names": [ + "apm-*-transaction*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "privileges": ["read", "write"] + }, + { + "names": [".siem-signals-*"], + "privileges": ["read", "write"] + }, + { + "names": [".lists*", ".items*"], + "privileges": ["read", "write"] + } + ] + }, + "kibana": [ + { + "feature": { + "ml": ["read"], + "siem": ["all"], + "actions": ["read"], + "builtInAlerts": ["all"], + "savedObjectsManagement": ["all"] + }, + "spaces": ["*"] + } + ] +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_user.json new file mode 100644 index 0000000000000..f9454cc0ad2fe --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_user.json @@ -0,0 +1,6 @@ +{ + "password": "changeme", + "roles": ["hunter"], + "full_name": "Hunter", + "email": "detections-reader@example.com" +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/get_detections_role.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/get_detections_role.sh new file mode 100755 index 0000000000000..b79c40cda3df2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/get_detections_role.sh @@ -0,0 +1,10 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XGET ${KIBANA_URL}/api/security/role/hunter | jq -S . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/post_detections_role.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/post_detections_role.sh new file mode 100755 index 0000000000000..11efa658fcdd2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/post_detections_role.sh @@ -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. +# + +ROLE=(${@:-./detections_role.json}) + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XPUT ${KIBANA_URL}/api/security/role/hunter \ +-d @${ROLE} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/post_detections_user.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/post_detections_user.sh new file mode 100755 index 0000000000000..75f21b8017204 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/post_detections_user.sh @@ -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. +# + +USER=(${@:-./detections_user.json}) + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + ${ELASTICSEARCH_URL}/_security/user/hunter \ +-d @${USER} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/README.md new file mode 100644 index 0000000000000..b9173c973abab --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/README.md @@ -0,0 +1,5 @@ +essentially a superuser for security solution + +| Role | Data Sources | Security Solution ML Jobs/Results | Lists | Rules/Exceptions | Action Connectors | Signals/Alerts | +| :------------------------------------------: | :----------: | :------------------: | :---: | :--------------: | :---------------: | :------------: | +| Platform Engineer (data ingest, cluster ops) | all | all | all | read, write | all | all | diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/delete_detections_user.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/delete_detections_user.sh new file mode 100755 index 0000000000000..2a7a56f42d98c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/delete_detections_user.sh @@ -0,0 +1,10 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +curl -v -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XDELETE ${ELASTICSEARCH_URL}/_security/user/platform_engineer diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json new file mode 100644 index 0000000000000..75001292242c3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json @@ -0,0 +1,39 @@ +{ + "elasticsearch": { + "cluster": ["manage"], + "indices": [ + { + "names": [".lists*", ".items*"], + "privileges": ["all"] + }, + { + "names": [ + "apm-*-transaction*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "privileges": ["all"] + }, + { + "names": [".siem-signals-*"], + "privileges": ["all"] + } + ] + }, + "kibana": [ + { + "feature": { + "ml": ["all"], + "siem": ["all"], + "actions": ["all"], + "builtInAlerts": ["all"], + "savedObjectsManagement": ["all"] + }, + "spaces": ["*"] + } + ] +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_user.json new file mode 100644 index 0000000000000..8c4eab8b05e6e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_user.json @@ -0,0 +1,6 @@ +{ + "password": "changeme", + "roles": ["platform_engineer"], + "full_name": "platform engineer", + "email": "detections-reader@example.com" +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/get_detections_role.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/get_detections_role.sh new file mode 100755 index 0000000000000..b7a04beda8934 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/get_detections_role.sh @@ -0,0 +1,10 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XGET ${KIBANA_URL}/api/security/role/platform_engineer | jq -S . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/post_detections_role.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/post_detections_role.sh new file mode 100755 index 0000000000000..a6d7504bd8d5b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/post_detections_role.sh @@ -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. +# + +ROLE=(${@:-./detections_role.json}) + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XPUT ${KIBANA_URL}/api/security/role/platform_engineer \ +-d @${ROLE} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/post_detections_user.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/post_detections_user.sh new file mode 100755 index 0000000000000..88217795da40b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/post_detections_user.sh @@ -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. +# + +USER=(${@:-./detections_user.json}) + + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + ${ELASTICSEARCH_URL}/_security/user/platform_engineer \ +-d @${USER} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/README.md new file mode 100644 index 0000000000000..1d2ef736f580c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/README.md @@ -0,0 +1,5 @@ +rule author has the same privileges as hunter with the additional privileges of uploading value lists + +| Role | Data Sources | Security Solution ML Jobs/Results | Lists | Rules/Exceptions | Action Connectors | Signals/Alerts | +| :-----------------------------------------: | :----------: | :------------------: | :---------: | :--------------: | :---------------: | :------------------------------: | +| Rule Author / Manager / Detections Engineer | read, write | read | read, write | read, write | read | read, write, view_index_metadata | diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/delete_detections_user.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/delete_detections_user.sh new file mode 100755 index 0000000000000..66c49bd210135 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/delete_detections_user.sh @@ -0,0 +1,10 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +curl -v -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XDELETE ${ELASTICSEARCH_URL}/_security/user/rule_author diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json new file mode 100644 index 0000000000000..f4950a25fdb77 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json @@ -0,0 +1,37 @@ +{ + "elasticsearch": { + "cluster": [], + "indices": [ + { + "names": [ + "apm-*-transaction*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + ".lists*", + ".items*" + ], + "privileges": ["read", "write"] + }, + { + "names": [".siem-signals-*"], + "privileges": ["read", "write", "view_index_metadata"] + } + ] + }, + "kibana": [ + { + "feature": { + "ml": ["read"], + "siem": ["all"], + "actions": ["read"], + "builtInAlerts": ["all"], + "savedObjectsManagement": ["all"] + }, + "spaces": ["*"] + } + ] +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_user.json new file mode 100644 index 0000000000000..ae08072b5890e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_user.json @@ -0,0 +1,6 @@ +{ + "password": "changeme", + "roles": ["rule_author"], + "full_name": "rule author", + "email": "detections-reader@example.com" +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/get_detections_role.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/get_detections_role.sh new file mode 100755 index 0000000000000..0aa8a5f70f4de --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/get_detections_role.sh @@ -0,0 +1,10 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XGET ${KIBANA_URL}/api/security/role/rule_author | jq -S . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/post_detections_role.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/post_detections_role.sh new file mode 100755 index 0000000000000..01c132c3f947f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/post_detections_role.sh @@ -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. +# + +ROLE=(${@:-./detections_role.json}) + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XPUT ${KIBANA_URL}/api/security/role/rule_author \ +-d @${ROLE} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/post_detections_user.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/post_detections_user.sh new file mode 100755 index 0000000000000..63eb626f580d4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/post_detections_user.sh @@ -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. +# + +USER=(${@:-./detections_user.json}) + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + ${ELASTICSEARCH_URL}/_security/user/rule_author \ +-d @${USER} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/README.md new file mode 100644 index 0000000000000..fef99dfed2fbb --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/README.md @@ -0,0 +1,5 @@ +SOC Manager has all of the privileges of a rule author role with the additional privilege of managing the signals index. It can't create the signals index though. + +| Role | Data Sources | Security Solution ML Jobs/Results | Lists | Rules/Exceptions | Action Connectors | Signals/Alerts | +| :---------: | :----------: | :------------------: | :---------: | :--------------: | :---------------: | :-----------------: | +| SOC Manager | read, write | read | read, write | read, write | all | read, write, manage | diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/delete_detections_user.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/delete_detections_user.sh new file mode 100755 index 0000000000000..5bc3e4401c015 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/delete_detections_user.sh @@ -0,0 +1,10 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +curl -v -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XDELETE ${ELASTICSEARCH_URL}/_security/user/soc_manager diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json new file mode 100644 index 0000000000000..a6cb64ef83ba7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json @@ -0,0 +1,37 @@ +{ + "elasticsearch": { + "cluster": [], + "indices": [ + { + "names": [ + "apm-*-transaction*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + ".lists*", + ".items*" + ], + "privileges": ["read", "write"] + }, + { + "names": [".siem-signals-*"], + "privileges": ["read", "write", "manage"] + } + ] + }, + "kibana": [ + { + "feature": { + "ml": ["read"], + "siem": ["all"], + "actions": ["all"], + "builtInAlerts": ["all"], + "savedObjectsManagement": ["all"] + }, + "spaces": ["*"] + } + ] +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_user.json new file mode 100644 index 0000000000000..18c7cc2312bf5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_user.json @@ -0,0 +1,6 @@ +{ + "password": "changeme", + "roles": ["soc_manager"], + "full_name": "SOC manager", + "email": "detections-reader@example.com" +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/get_detections_role.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/get_detections_role.sh new file mode 100755 index 0000000000000..a93911573d374 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/get_detections_role.sh @@ -0,0 +1,10 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XGET ${KIBANA_URL}/api/security/role/soc_manager | jq -S . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/post_detections_role.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/post_detections_role.sh new file mode 100755 index 0000000000000..313011859c487 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/post_detections_role.sh @@ -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. +# + +ROLE=(${@:-./detections_role.json}) + + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XPUT ${KIBANA_URL}/api/security/role/soc_manager \ +-d @${ROLE} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/post_detections_user.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/post_detections_user.sh new file mode 100755 index 0000000000000..c0928dbeb15ed --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/post_detections_user.sh @@ -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. +# + +USER=(${@:-./detections_user.json}) + + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + ${ELASTICSEARCH_URL}/_security/user/soc_manager \ +-d @${USER} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/README.md new file mode 100644 index 0000000000000..9ba0deba763aa --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/README.md @@ -0,0 +1,3 @@ +| Role | Data Sources | Security Solution ML Jobs/Results | Lists | Rules/Exceptions | Actions Connectors | Signals/Alerts | +| :--------: | :----------: | :------------------: | :---: | :--------------: | :----------------: | :------------: | +| T1 Analyst | read | read | none | read | read | read, write | diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/delete_detections_user.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/delete_detections_user.sh new file mode 100755 index 0000000000000..d0f1773c30cc7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/delete_detections_user.sh @@ -0,0 +1,10 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +curl -v -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XDELETE ${ELASTICSEARCH_URL}/_security/user/t1_analyst diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json new file mode 100644 index 0000000000000..87be597e4bdb5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json @@ -0,0 +1,32 @@ +{ + "elasticsearch": { + "cluster": [], + "indices": [ + { "names": [".siem-signals-*"], "privileges": ["read", "write"] }, + { + "names": [ + "apm-*-transaction*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "privileges": ["read"] + } + ] + }, + "kibana": [ + { + "feature": { + "ml": ["read"], + "siem": ["all"], + "actions": ["read"], + "builtInAlerts": ["read"], + "savedObjectsManagement": ["read"] + }, + "spaces": ["*"] + } + ] +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_user.json new file mode 100644 index 0000000000000..203abec8ad433 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_user.json @@ -0,0 +1,6 @@ +{ + "password": "changeme", + "roles": ["t1_analyst"], + "full_name": "T1 Analyst", + "email": "detections-reader@example.com" +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/get_detections_role.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/get_detections_role.sh new file mode 100755 index 0000000000000..3570a3fc49947 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/get_detections_role.sh @@ -0,0 +1,10 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XGET ${KIBANA_URL}/api/security/role/t1_analyst | jq -S . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/post_detections_role.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/post_detections_role.sh new file mode 100755 index 0000000000000..da0f03b5916f3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/post_detections_role.sh @@ -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. +# + +# Uses a default if no argument is specified +ROLE=(${@:-./detections_role.json}) + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XPUT ${KIBANA_URL}/api/security/role/t1_analyst \ +-d @${ROLE} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/post_detections_user.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/post_detections_user.sh new file mode 100755 index 0000000000000..6ae5521a43638 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/post_detections_user.sh @@ -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. +# + +USER=(${@:-./detections_user.json}) + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + ${ELASTICSEARCH_URL}/_security/user/t1_analyst \ +-d @${USER} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/README.md new file mode 100644 index 0000000000000..3988e88870755 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/README.md @@ -0,0 +1,5 @@ +This role can view rules. Essentially there is no difference between a T1 and T2 analyst. + +| Role | Data Sources | Security Solution ML Jobs/Results | Lists | Rules/Exceptions | Action Connectors | Signals/Alerts | +| :--------: | :----------: | :------------------: | :---: | :--------------: | :---------------: | :------------: | +| T2 Analyst | read | read | read | read | read | read, write | diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/delete_detections_user.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/delete_detections_user.sh new file mode 100755 index 0000000000000..487c66064ce42 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/delete_detections_user.sh @@ -0,0 +1,10 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +curl -v -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XDELETE ${ELASTICSEARCH_URL}/_security/user/t2_analyst diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json new file mode 100644 index 0000000000000..18ada2ef7ab21 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json @@ -0,0 +1,34 @@ +{ + "elasticsearch": { + "cluster": [], + "indices": [ + { "names": [".siem-signals-*"], "privileges": ["read", "write"] }, + { + "names": [ + ".lists*", + ".items*", + "apm-*-transaction*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "privileges": ["read"] + } + ] + }, + "kibana": [ + { + "feature": { + "ml": ["read"], + "siem": ["all"], + "actions": ["read"], + "builtInAlerts": ["read"], + "savedObjectsManagement": ["read"] + }, + "spaces": ["*"] + } + ] +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_user.json new file mode 100644 index 0000000000000..3f5da2752314f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_user.json @@ -0,0 +1,6 @@ +{ + "password": "changeme", + "roles": ["t2_analyst"], + "full_name": "t2 analyst", + "email": "detections-reader@example.com" +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/get_detections_role.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/get_detections_role.sh new file mode 100755 index 0000000000000..8625211591303 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/get_detections_role.sh @@ -0,0 +1,10 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XGET ${KIBANA_URL}/api/security/role/t2_analyst | jq -S . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/post_detections_role.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/post_detections_role.sh new file mode 100755 index 0000000000000..67f971f4b6de6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/post_detections_role.sh @@ -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. +# + +ROLE=(${@:-./detections_role.json}) + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XPUT ${KIBANA_URL}/api/security/role/t2_analyst \ +-d @${ROLE} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/post_detections_user.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/post_detections_user.sh new file mode 100755 index 0000000000000..45f20381d2738 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/post_detections_user.sh @@ -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. +# + +USER=(${@:-./detections_user.json}) + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + ${ELASTICSEARCH_URL}/_security/user/t2_analyst \ +-d @${USER} diff --git a/x-pack/plugins/spaces/common/lib/__snapshots__/spaces_url_parser.test.ts.snap b/x-pack/plugins/spaces/common/lib/__snapshots__/spaces_url_parser.test.ts.snap deleted file mode 100644 index d08be39f9282e..0000000000000 --- a/x-pack/plugins/spaces/common/lib/__snapshots__/spaces_url_parser.test.ts.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`addSpaceIdToPath it throws an error when the requested path does not start with a slash 1`] = `"path must start with a /"`; diff --git a/x-pack/plugins/spaces/common/lib/spaces_url_parser.test.ts b/x-pack/plugins/spaces/common/lib/spaces_url_parser.test.ts index 2b34bc77ec686..90486d499b947 100644 --- a/x-pack/plugins/spaces/common/lib/spaces_url_parser.test.ts +++ b/x-pack/plugins/spaces/common/lib/spaces_url_parser.test.ts @@ -102,6 +102,6 @@ describe('addSpaceIdToPath', () => { test('it throws an error when the requested path does not start with a slash', () => { expect(() => { addSpaceIdToPath('', '', 'foo'); - }).toThrowErrorMatchingSnapshot(); + }).toThrowErrorMatchingInlineSnapshot(`"path must start with a /"`); }); }); diff --git a/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts b/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts index 6466835899f16..e266af704e8b6 100644 --- a/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts +++ b/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts @@ -47,10 +47,12 @@ export function addSpaceIdToPath( throw new Error(`path must start with a /`); } + const normalizedBasePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath; + if (spaceId && spaceId !== DEFAULT_SPACE_ID) { - return `${basePath}/s/${spaceId}${requestedPath}`; + return `${normalizedBasePath}/s/${spaceId}${requestedPath}`; } - return `${basePath}${requestedPath}`; + return `${normalizedBasePath}${requestedPath}` || '/'; } function stripServerBasePath(requestBasePath: string, serverBasePath: string) { diff --git a/x-pack/plugins/spaces/kibana.json b/x-pack/plugins/spaces/kibana.json index 4443b6d8a685b..62a86409d8889 100644 --- a/x-pack/plugins/spaces/kibana.json +++ b/x-pack/plugins/spaces/kibana.json @@ -8,7 +8,6 @@ "advancedSettings", "home", "management", - "security", "usageCollection", "savedObjectsManagement" ], diff --git a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.test.ts b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.test.ts index 42f3d766adf85..bc861964bf56d 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.test.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.test.ts @@ -116,12 +116,54 @@ describe('SpacesManager', () => { const result = await spacesManager.getShareSavedObjectPermissions('foo'); expect(coreStart.http.get).toHaveBeenCalledTimes(2); expect(coreStart.http.get).toHaveBeenLastCalledWith( - '/internal/spaces/_share_saved_object_permissions', + '/internal/security/_share_saved_object_permissions', { query: { type: 'foo' }, } ); expect(result).toEqual({ shareToAllSpaces }); }); + + it('allows the share if security is disabled', async () => { + const coreStart = coreMock.createStart(); + coreStart.http.get.mockResolvedValueOnce({}); + coreStart.http.get.mockRejectedValueOnce({ + body: { + statusCode: 404, + }, + }); + const spacesManager = new SpacesManager(coreStart.http); + expect(coreStart.http.get).toHaveBeenCalledTimes(1); // initial call to get active space + + const result = await spacesManager.getShareSavedObjectPermissions('foo'); + expect(coreStart.http.get).toHaveBeenCalledTimes(2); + expect(coreStart.http.get).toHaveBeenLastCalledWith( + '/internal/security/_share_saved_object_permissions', + { + query: { type: 'foo' }, + } + ); + expect(result).toEqual({ shareToAllSpaces: true }); + }); + + it('throws all other errors', async () => { + const coreStart = coreMock.createStart(); + coreStart.http.get.mockResolvedValueOnce({}); + coreStart.http.get.mockRejectedValueOnce(new Error('Get out of here!')); + const spacesManager = new SpacesManager(coreStart.http); + expect(coreStart.http.get).toHaveBeenCalledTimes(1); // initial call to get active space + + await expect( + spacesManager.getShareSavedObjectPermissions('foo') + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Get out of here!"`); + + expect(coreStart.http.get).toHaveBeenCalledTimes(2); + expect(coreStart.http.get).toHaveBeenLastCalledWith( + '/internal/security/_share_saved_object_permissions', + { + query: { type: 'foo' }, + } + ); + }); }); }); 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 8ddda7130d8b8..8e530ddf8ff2e 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts @@ -115,7 +115,16 @@ export class SpacesManager { public async getShareSavedObjectPermissions( type: string ): Promise<{ shareToAllSpaces: boolean }> { - return this.http.get('/internal/spaces/_share_saved_object_permissions', { query: { type } }); + return this.http + .get('/internal/security/_share_saved_object_permissions', { query: { type } }) + .catch((err) => { + const isNotFound = err?.body?.statusCode === 404; + if (isNotFound) { + // security is not enabled + return { shareToAllSpaces: true }; + } + throw err; + }); } public async shareSavedObjectAdd(object: SavedObject, spaces: string[]): Promise { diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts index 0dd070e63ba31..bfd73984811ef 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts @@ -126,14 +126,14 @@ const setup = (space: Space) => { {}, ]); - const spacesService = spacesServiceMock.createSetupContract(); + const spacesService = spacesServiceMock.createStartContract(); spacesService.getActiveSpace.mockResolvedValue(space); const logger = loggingSystemMock.createLogger(); const switcher = setupCapabilitiesSwitcher( (coreSetup as unknown) as CoreSetup, - spacesService, + () => spacesService, logger ); diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts index 8b0b955c40d92..ee059f7b9c26e 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts @@ -7,12 +7,12 @@ import _ from 'lodash'; import { Capabilities, CapabilitiesSwitcher, CoreSetup, Logger } from 'src/core/server'; import { KibanaFeature } from '../../../../plugins/features/server'; import { Space } from '../../common/model/space'; -import { SpacesServiceSetup } from '../spaces_service'; +import { SpacesServiceStart } from '../spaces_service'; import { PluginsStart } from '../plugin'; export function setupCapabilitiesSwitcher( core: CoreSetup, - spacesService: SpacesServiceSetup, + getSpacesService: () => SpacesServiceStart, logger: Logger ): CapabilitiesSwitcher { return async (request, capabilities) => { @@ -24,7 +24,7 @@ export function setupCapabilitiesSwitcher( try { const [activeSpace, [, { features }]] = await Promise.all([ - spacesService.getActiveSpace(request), + getSpacesService().getActiveSpace(request), core.getStartServices(), ]); diff --git a/x-pack/plugins/spaces/server/capabilities/index.ts b/x-pack/plugins/spaces/server/capabilities/index.ts index 56a72a2eeaf19..32620528682e4 100644 --- a/x-pack/plugins/spaces/server/capabilities/index.ts +++ b/x-pack/plugins/spaces/server/capabilities/index.ts @@ -8,13 +8,13 @@ import { CoreSetup, Logger } from 'src/core/server'; import { capabilitiesProvider } from './capabilities_provider'; import { setupCapabilitiesSwitcher } from './capabilities_switcher'; import { PluginsStart } from '../plugin'; -import { SpacesServiceSetup } from '../spaces_service'; +import { SpacesServiceStart } from '../spaces_service'; export const setupCapabilities = ( core: CoreSetup, - spacesService: SpacesServiceSetup, + getSpacesService: () => SpacesServiceStart, logger: Logger ) => { core.capabilities.registerProvider(capabilitiesProvider); - core.capabilities.registerSwitcher(setupCapabilitiesSwitcher(core, spacesService, logger)); + core.capabilities.registerSwitcher(setupCapabilitiesSwitcher(core, getSpacesService, logger)); }; diff --git a/x-pack/plugins/spaces/server/index.ts b/x-pack/plugins/spaces/server/index.ts index 77eb3e9c73980..85f1facf6131c 100644 --- a/x-pack/plugins/spaces/server/index.ts +++ b/x-pack/plugins/spaces/server/index.ts @@ -13,10 +13,13 @@ import { Plugin } from './plugin'; // reduce number of such exports to zero and provide everything we want to expose via Setup/Start // run-time contracts. +export { addSpaceIdToPath } from '../common'; + // end public contract exports -export { SpacesPluginSetup } from './plugin'; -export { SpacesServiceSetup } from './spaces_service'; +export { SpacesPluginSetup, SpacesPluginStart } from './plugin'; +export { SpacesServiceSetup, SpacesServiceStart } from './spaces_service'; +export { ISpacesClient } from './spaces_client'; export { Space } from '../common/model/space'; export const config = { schema: ConfigSchema }; 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 89371259ae04c..ec540a08c07b9 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 @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import * as Rx from 'rxjs'; import Boom from '@hapi/boom'; import { Legacy } from 'kibana'; // @ts-ignore @@ -22,13 +21,11 @@ import { } from '../../../../../../src/core/server/mocks'; 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'; import { initSpacesOnPostAuthRequestInterceptor } from './on_post_auth_interceptor'; import { KibanaFeature } from '../../../../features/server'; -import { spacesConfig } from '../__fixtures__'; -import { securityMock } from '../../../../security/server/mocks'; import { featuresPluginMock } from '../../../../features/server/mocks'; +import { spacesClientServiceMock } from '../../spaces_client/spaces_client_service.mock'; // FLAKY: https://github.com/elastic/kibana/issues/55953 describe.skip('onPostAuthInterceptor', () => { @@ -166,17 +163,18 @@ describe.skip('onPostAuthInterceptor', () => { coreStart.savedObjects.createInternalRepository.mockImplementation(mockRepository); coreStart.savedObjects.createScopedRepository.mockImplementation(mockRepository); - const service = new SpacesService(loggingMock); + const service = new SpacesService(); - const spacesService = await service.setup({ - http: (http as unknown) as CoreSetup['http'], - getStartServices: async () => [coreStart, {}, {}], - authorization: securityMock.createSetup().authz, - auditLogger: {} as SpacesAuditLogger, - config$: Rx.of(spacesConfig), + service.setup({ + basePath: http.basePath, + }); + + const spacesServiceStart = service.start({ + basePath: http.basePath, + spacesClientService: spacesClientServiceMock.createStart(), }); - spacesService.scopedClient = jest.fn().mockResolvedValue({ + spacesServiceStart.createSpacesClient = jest.fn().mockReturnValue({ getAll() { if (testOptions.simulateGetSpacesFailure) { throw Boom.unauthorized('missing credendials', 'Protected Elasticsearch'); @@ -206,7 +204,7 @@ describe.skip('onPostAuthInterceptor', () => { http: (http as unknown) as CoreSetup['http'], log: loggingMock, features: featuresPlugin, - spacesService, + getSpacesService: () => spacesServiceStart, }); const router = http.createRouter('/'); @@ -221,7 +219,7 @@ describe.skip('onPostAuthInterceptor', () => { return { response, - spacesService, + spacesService: spacesServiceStart, }; } @@ -342,7 +340,7 @@ describe.skip('onPostAuthInterceptor', () => { } `); - expect(spacesService.scopedClient).toHaveBeenCalledWith( + expect(spacesService.createSpacesClient).toHaveBeenCalledWith( expect.objectContaining({ headers: expect.objectContaining({ authorization: headers.authorization, @@ -381,7 +379,7 @@ describe.skip('onPostAuthInterceptor', () => { } `); - expect(spacesService.scopedClient).toHaveBeenCalledWith( + expect(spacesService.createSpacesClient).toHaveBeenCalledWith( expect.objectContaining({ headers: expect.objectContaining({ authorization: headers.authorization, @@ -414,7 +412,7 @@ describe.skip('onPostAuthInterceptor', () => { expect(response.status).toEqual(302); expect(response.header.location).toEqual(`/spaces/space_selector`); - expect(spacesService.scopedClient).toHaveBeenCalledWith( + expect(spacesService.createSpacesClient).toHaveBeenCalledWith( expect.objectContaining({ headers: expect.objectContaining({ authorization: headers.authorization, @@ -447,7 +445,7 @@ describe.skip('onPostAuthInterceptor', () => { expect(response.status).toEqual(302); expect(response.header.location).toEqual(`/s/a-space/spaces/enter`); - expect(spacesService.scopedClient).toHaveBeenCalledWith( + expect(spacesService.createSpacesClient).toHaveBeenCalledWith( expect.objectContaining({ headers: expect.objectContaining({ authorization: headers.authorization, @@ -473,7 +471,7 @@ describe.skip('onPostAuthInterceptor', () => { expect(response.status).toEqual(302); expect(response.header.location).toEqual(`/s/a-space/spaces/enter`); - expect(spacesService.scopedClient).toHaveBeenCalledWith( + expect(spacesService.createSpacesClient).toHaveBeenCalledWith( expect.objectContaining({ headers: expect.objectContaining({ authorization: headers.authorization, @@ -501,7 +499,7 @@ describe.skip('onPostAuthInterceptor', () => { expect(response.status).toEqual(302); expect(response.header.location).toEqual('/spaces/enter'); - expect(spacesService.scopedClient).toHaveBeenCalledWith( + expect(spacesService.createSpacesClient).toHaveBeenCalledWith( expect.objectContaining({ headers: expect.objectContaining({ authorization: headers.authorization, @@ -526,7 +524,7 @@ describe.skip('onPostAuthInterceptor', () => { expect(response.status).toEqual(200); - expect(spacesService.scopedClient).toHaveBeenCalledWith( + expect(spacesService.createSpacesClient).toHaveBeenCalledWith( expect.objectContaining({ headers: expect.objectContaining({ authorization: headers.authorization, @@ -551,7 +549,7 @@ describe.skip('onPostAuthInterceptor', () => { expect(response.status).toEqual(200); - expect(spacesService.scopedClient).toHaveBeenCalledWith( + expect(spacesService.createSpacesClient).toHaveBeenCalledWith( expect.objectContaining({ headers: expect.objectContaining({ authorization: headers.authorization, @@ -576,7 +574,7 @@ describe.skip('onPostAuthInterceptor', () => { expect(response.status).toEqual(404); - expect(spacesService.scopedClient).toHaveBeenCalledWith( + expect(spacesService.createSpacesClient).toHaveBeenCalledWith( expect.objectContaining({ headers: expect.objectContaining({ authorization: headers.authorization, 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 1aa2011a15b35..4731ddbac10c3 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 @@ -6,7 +6,7 @@ import { Logger, CoreSetup } from 'src/core/server'; import { Space } from '../../../common/model/space'; import { wrapError } from '../errors'; -import { SpacesServiceSetup } from '../../spaces_service/spaces_service'; +import { SpacesServiceStart } from '../../spaces_service/spaces_service'; import { PluginsSetup } from '../../plugin'; import { getSpaceSelectorUrl } from '../get_space_selector_url'; import { DEFAULT_SPACE_ID, ENTER_SPACE_PATH } from '../../../common/constants'; @@ -15,13 +15,13 @@ import { addSpaceIdToPath } from '../../../common'; export interface OnPostAuthInterceptorDeps { http: CoreSetup['http']; features: PluginsSetup['features']; - spacesService: SpacesServiceSetup; + getSpacesService: () => SpacesServiceStart; log: Logger; } export function initSpacesOnPostAuthRequestInterceptor({ features, - spacesService, + getSpacesService, log, http, }: OnPostAuthInterceptorDeps) { @@ -30,6 +30,8 @@ export function initSpacesOnPostAuthRequestInterceptor({ const path = request.url.pathname; + const spacesService = getSpacesService(); + const spaceId = spacesService.getSpaceId(request); // The root of kibana is also the root of the defaut space, @@ -43,7 +45,7 @@ export function initSpacesOnPostAuthRequestInterceptor({ // which is not available at the time of "onRequest". if (isRequestingKibanaRoot) { try { - const spacesClient = await spacesService.scopedClient(request); + const spacesClient = spacesService.createSpacesClient(request); const spaces = await spacesClient.getAll(); if (spaces.length === 1) { @@ -76,7 +78,7 @@ export function initSpacesOnPostAuthRequestInterceptor({ try { log.debug(`Verifying access to space "${spaceId}"`); - const spacesClient = await spacesService.scopedClient(request); + const spacesClient = spacesService.createSpacesClient(request); space = await spacesClient.get(spaceId); } catch (error) { const wrappedError = wrapError(error); 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 deleted file mode 100644 index 095a9046d6d3b..0000000000000 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts +++ /dev/null @@ -1,1237 +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 { SecurityPluginSetup } from '../../../../security/server'; -import { SpacesClient } from './spaces_client'; -import { ConfigType, ConfigSchema } from '../../config'; -import { GetAllSpacesPurpose } from '../../../common/model/types'; - -import { savedObjectsRepositoryMock } from 'src/core/server/mocks'; -import { securityMock } from '../../../../security/server/mocks'; - -const createMockAuditLogger = () => { - return { - spacesAuthorizationFailure: jest.fn(), - spacesAuthorizationSuccess: jest.fn(), - }; -}; - -const createMockAuthorization = () => { - const mockCheckPrivilegesAtSpace = jest.fn(); - const mockCheckPrivilegesAtSpaces = jest.fn(); - const mockCheckPrivilegesGlobally = jest.fn(); - - const mockAuthorization = securityMock.createSetup().authz; - mockAuthorization.checkPrivilegesWithRequest.mockImplementation(() => ({ - atSpaces: mockCheckPrivilegesAtSpaces, - atSpace: mockCheckPrivilegesAtSpace, - globally: mockCheckPrivilegesGlobally, - })); - (mockAuthorization.actions.savedObject.get as jest.MockedFunction< - typeof mockAuthorization.actions.savedObject.get - >).mockImplementation((featureId, ...uiCapabilityParts) => { - return `mockSavedObjectAction:${featureId}/${uiCapabilityParts.join('/')}`; - }); - (mockAuthorization.actions.ui.get as jest.MockedFunction< - typeof mockAuthorization.actions.ui.get - >).mockImplementation((featureId, ...uiCapabilityParts) => { - return `mockUiAction:${featureId}/${uiCapabilityParts.join('/')}`; - }); - - return { - mockCheckPrivilegesAtSpaces, - mockCheckPrivilegesAtSpace, - mockCheckPrivilegesGlobally, - mockAuthorization, - }; -}; - -const createMockConfig = (mockConfig: ConfigType = { maxSpaces: 1000, enabled: true }) => { - return ConfigSchema.validate(mockConfig); -}; - -const baseSetup = (authorization: boolean | null) => { - const mockAuditLogger = createMockAuditLogger(); - const mockAuthorizationAndFunctions = createMockAuthorization(); - if (authorization !== null) { - mockAuthorizationAndFunctions.mockAuthorization.mode.useRbacForRequest.mockReturnValue( - authorization - ); - } - const mockCallWithRequestRepository = savedObjectsRepositoryMock.create(); - const mockConfig = createMockConfig(); - const mockInternalRepository = savedObjectsRepositoryMock.create(); - const request = Symbol() as any; - const client = new SpacesClient( - mockAuditLogger as any, - jest.fn(), - authorization === null ? null : mockAuthorizationAndFunctions.mockAuthorization, - mockCallWithRequestRepository, - mockConfig, - mockInternalRepository, - request - ); - - return { - mockAuditLogger, - ...mockAuthorizationAndFunctions, - mockCallWithRequestRepository, - mockConfig, - mockInternalRepository, - request, - client, - }; -}; - -describe('#getAll', () => { - const savedObjects = [ - { - id: 'foo', - attributes: { - name: 'foo-name', - description: 'foo-description', - bar: 'foo-bar', - }, - }, - { - id: 'bar', - attributes: { - name: 'bar-name', - description: 'bar-description', - bar: 'bar-bar', - }, - }, - { - id: 'baz', - attributes: { - name: 'baz-name', - description: 'baz-description', - bar: 'baz-bar', - }, - }, - ]; - - const expectedSpaces = [ - { - id: 'foo', - name: 'foo-name', - description: 'foo-description', - bar: 'foo-bar', - }, - { - id: 'bar', - name: 'bar-name', - description: 'bar-description', - bar: 'bar-bar', - }, - { - id: 'baz', - name: 'baz-name', - description: 'baz-description', - bar: 'baz-bar', - }, - ]; - - const setup = (authorization: boolean | null) => { - const result = baseSetup(authorization); - const { mockCallWithRequestRepository, mockInternalRepository } = result; - mockCallWithRequestRepository.find.mockResolvedValue({ saved_objects: savedObjects } as any); - mockInternalRepository.find.mockResolvedValue({ saved_objects: savedObjects } as any); - return result; - }; - - describe('authorization is null', () => { - test(`finds spaces using callWithRequestRepository`, async () => { - const { mockAuditLogger, mockCallWithRequestRepository, mockConfig, client } = setup(null); - const actualSpaces = await client.getAll(); - - expect(actualSpaces).toEqual(expectedSpaces); - expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({ - type: 'space', - page: 1, - perPage: mockConfig.maxSpaces, - sortField: 'name.keyword', - }); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - }); - - describe(`authorization.mode.useRbacForRequest returns false`, () => { - test(`finds spaces using callWithRequestRepository`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCallWithRequestRepository, - mockConfig, - request, - client, - } = setup(false); - const actualSpaces = await client.getAll(); - - expect(actualSpaces).toEqual(expectedSpaces); - expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({ - type: 'space', - page: 1, - perPage: mockConfig.maxSpaces, - sortField: 'name.keyword', - }); - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws Boom.badRequest when an invalid purpose is provided'`, async () => { - const { mockAuthorization, client } = setup(false); - const purpose = 'invalid_purpose' as GetAllSpacesPurpose; - await expect(client.getAll({ purpose })).rejects.toThrowError( - 'unsupported space purpose: invalid_purpose' - ); - - expect(mockAuthorization.mode.useRbacForRequest).not.toHaveBeenCalled(); - }); - }); - - describe('useRbacForRequest is true', () => { - it('throws Boom.badRequest when an invalid purpose is provided', async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCheckPrivilegesAtSpaces, - mockInternalRepository, - client, - } = setup(true); - const purpose = 'invalid_purpose' as GetAllSpacesPurpose; - await expect(client.getAll({ purpose })).rejects.toThrowError( - 'unsupported space purpose: invalid_purpose' - ); - - expect(mockInternalRepository.find).not.toHaveBeenCalled(); - expect(mockAuthorization.mode.useRbacForRequest).not.toHaveBeenCalled(); - expect(mockAuthorization.checkPrivilegesWithRequest).not.toHaveBeenCalled(); - expect(mockCheckPrivilegesAtSpaces).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - [ - { - purpose: undefined, - expectedPrivileges: (mockAuthorization: SecurityPluginSetup['authz']) => [ - mockAuthorization.actions.login, - ], - }, - { - purpose: 'any' as GetAllSpacesPurpose, - expectedPrivileges: (mockAuthorization: SecurityPluginSetup['authz']) => [ - mockAuthorization.actions.login, - ], - }, - { - purpose: 'copySavedObjectsIntoSpace' as GetAllSpacesPurpose, - expectedPrivileges: () => [`mockUiAction:savedObjectsManagement/copyIntoSpace`], - }, - { - purpose: 'findSavedObjects' as GetAllSpacesPurpose, - expectedPrivileges: (mockAuthorization: SecurityPluginSetup['authz']) => [ - mockAuthorization.actions.login, - `mockSavedObjectAction:config/find`, - ], - }, - { - purpose: 'shareSavedObjectsIntoSpace' as GetAllSpacesPurpose, - expectedPrivileges: () => [`mockUiAction:savedObjectsManagement/shareIntoSpace`], - }, - ].forEach((scenario) => { - const { purpose } = scenario; - describe(`with purpose='${purpose}'`, () => { - test(`throws Boom.forbidden when user isn't authorized for any spaces`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCheckPrivilegesAtSpaces, - mockConfig, - mockInternalRepository, - request, - client, - } = setup(true); - const username = Symbol(); - const privileges = scenario.expectedPrivileges(mockAuthorization); - mockCheckPrivilegesAtSpaces.mockReturnValue({ - username, - privileges: { - kibana: privileges - .map((privilege) => [ - { resource: savedObjects[0].id, privilege, authorized: false }, - { resource: savedObjects[1].id, privilege, authorized: false }, - { resource: savedObjects[2].id, privilege, authorized: false }, - ]) - .flat(), - }, - }); - await expect(client.getAll({ purpose })).rejects.toThrowError('Forbidden'); - - expect(mockInternalRepository.find).toHaveBeenCalledWith({ - type: 'space', - page: 1, - perPage: mockConfig.maxSpaces, - sortField: 'name.keyword', - }); - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesAtSpaces).toHaveBeenCalledTimes(1); - expect(mockCheckPrivilegesAtSpaces).toHaveBeenCalledWith( - savedObjects.map((savedObject) => savedObject.id), - { kibana: privileges } - ); - expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith( - username, - 'getAll' - ); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns spaces that the user is authorized for`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCheckPrivilegesAtSpaces, - mockConfig, - mockInternalRepository, - request, - client, - } = setup(true); - const username = Symbol(); - const privileges = scenario.expectedPrivileges(mockAuthorization); - mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); - mockCheckPrivilegesAtSpaces.mockReturnValue({ - username, - privileges: { - kibana: privileges - .map((privilege) => [ - { resource: savedObjects[0].id, privilege, authorized: true }, - { resource: savedObjects[1].id, privilege, authorized: false }, - { resource: savedObjects[2].id, privilege, authorized: false }, - ]) - .flat(), - }, - }); - const actualSpaces = await client.getAll({ purpose }); - - expect(actualSpaces).toEqual([expectedSpaces[0]]); - expect(mockInternalRepository.find).toHaveBeenCalledWith({ - type: 'space', - page: 1, - perPage: mockConfig.maxSpaces, - sortField: 'name.keyword', - }); - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesAtSpaces).toHaveBeenCalledTimes(1); - expect(mockCheckPrivilegesAtSpaces).toHaveBeenCalledWith( - savedObjects.map((savedObject) => savedObject.id), - { kibana: privileges } - ); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith( - username, - 'getAll', - [savedObjects[0].id] - ); - }); - }); - }); - }); - - describe('includeAuthorizedPurposes is true', () => { - const includeAuthorizedPurposes = true; - - ([ - 'any', - 'copySavedObjectsIntoSpace', - 'findSavedObjects', - 'shareSavedObjectsIntoSpace', - ] as GetAllSpacesPurpose[]).forEach((purpose) => { - describe(`with purpose='${purpose}'`, () => { - test('throws error', async () => { - const { client } = setup(null); - expect(client.getAll({ purpose, includeAuthorizedPurposes })).rejects.toThrowError( - `'purpose' cannot be supplied with 'includeAuthorizedPurposes'` - ); - }); - }); - }); - - describe('with purpose=undefined', () => { - describe('authorization is null', () => { - test(`finds spaces using callWithRequestRepository and returns unaugmented results`, async () => { - const { mockAuditLogger, mockCallWithRequestRepository, mockConfig, client } = setup( - null - ); - const actualSpaces = await client.getAll({ includeAuthorizedPurposes }); - - expect(actualSpaces).toEqual(expectedSpaces); - expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({ - type: 'space', - page: 1, - perPage: mockConfig.maxSpaces, - sortField: 'name.keyword', - }); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - }); - - describe(`authorization.mode.useRbacForRequest returns false`, () => { - test(`finds spaces using callWithRequestRepository and returns unaugmented results`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCallWithRequestRepository, - mockConfig, - request, - client, - } = setup(false); - const actualSpaces = await client.getAll({ includeAuthorizedPurposes }); - - expect(actualSpaces).toEqual(expectedSpaces); - expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({ - type: 'space', - page: 1, - perPage: mockConfig.maxSpaces, - sortField: 'name.keyword', - }); - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - }); - - describe('useRbacForRequest is true', () => { - test(`throws Boom.forbidden when user isn't authorized for any spaces`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCheckPrivilegesAtSpaces, - mockConfig, - mockInternalRepository, - request, - client, - } = setup(true); - const username = Symbol(); - const privileges = [ - mockAuthorization.actions.login, - `mockUiAction:savedObjectsManagement/copyIntoSpace`, - `mockSavedObjectAction:config/find`, - `mockUiAction:savedObjectsManagement/shareIntoSpace`, - ]; - mockCheckPrivilegesAtSpaces.mockReturnValue({ - username, - privileges: { - kibana: privileges - .map((privilege) => [ - { resource: savedObjects[0].id, privilege, authorized: false }, - { resource: savedObjects[1].id, privilege, authorized: false }, - { resource: savedObjects[2].id, privilege, authorized: false }, - ]) - .flat(), - }, - }); - await expect(client.getAll({ includeAuthorizedPurposes })).rejects.toThrowError( - 'Forbidden' - ); - - expect(mockInternalRepository.find).toHaveBeenCalledWith({ - type: 'space', - page: 1, - perPage: mockConfig.maxSpaces, - sortField: 'name.keyword', - }); - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesAtSpaces).toHaveBeenCalledTimes(1); - expect(mockCheckPrivilegesAtSpaces).toHaveBeenCalledWith( - savedObjects.map((savedObject) => savedObject.id), - { - kibana: [ - mockAuthorization.actions.login, - `mockUiAction:savedObjectsManagement/copyIntoSpace`, - mockAuthorization.actions.login, // the actual privilege check deduplicates this -- we mimicked that behavior in our mock result - `mockSavedObjectAction:config/find`, - `mockUiAction:savedObjectsManagement/shareIntoSpace`, - ], - } - ); - expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith( - username, - 'getAll' - ); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns augmented spaces that the user is authorized for`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCheckPrivilegesAtSpaces, - mockConfig, - mockInternalRepository, - request, - client, - } = setup(true); - const username = Symbol(); - const privileges = [ - mockAuthorization.actions.login, - `mockUiAction:savedObjectsManagement/copyIntoSpace`, - `mockSavedObjectAction:config/find`, - `mockUiAction:savedObjectsManagement/shareIntoSpace`, - ]; - mockCheckPrivilegesAtSpaces.mockReturnValue({ - username, - privileges: { - kibana: [ - ...privileges.map((privilege) => { - return { resource: savedObjects[0].id, privilege, authorized: true }; - }), - { - resource: savedObjects[1].id, - privilege: mockAuthorization.actions.login, - authorized: false, - }, - { - resource: savedObjects[1].id, - privilege: `mockUiAction:savedObjectsManagement/copyIntoSpace`, - authorized: false, - }, - { - resource: savedObjects[1].id, - privilege: `mockSavedObjectAction:config/find`, - authorized: true, // special case -- this alone will not authorize the user for the 'findSavedObjects purpose, since it also requires the login action - }, - { - resource: savedObjects[1].id, - privilege: `mockUiAction:savedObjectsManagement/shareIntoSpace`, - authorized: true, // note that this being authorized without the login action is contrived for this test case, and would never happen in a real world scenario - }, - ...privileges.map((privilege) => { - return { resource: savedObjects[2].id, privilege, authorized: false }; - }), - ], - }, - }); - const actualSpaces = await client.getAll({ includeAuthorizedPurposes }); - - expect(actualSpaces).toEqual([ - { - ...expectedSpaces[0], - authorizedPurposes: { - any: true, - copySavedObjectsIntoSpace: true, - findSavedObjects: true, - shareSavedObjectsIntoSpace: true, - }, - }, - { - ...expectedSpaces[1], - authorizedPurposes: { - any: false, - copySavedObjectsIntoSpace: false, - findSavedObjects: false, - shareSavedObjectsIntoSpace: true, - }, - }, - ]); - expect(mockInternalRepository.find).toHaveBeenCalledWith({ - type: 'space', - page: 1, - perPage: mockConfig.maxSpaces, - sortField: 'name.keyword', - }); - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesAtSpaces).toHaveBeenCalledTimes(1); - expect(mockCheckPrivilegesAtSpaces).toHaveBeenCalledWith( - savedObjects.map((savedObject) => savedObject.id), - { - kibana: [ - mockAuthorization.actions.login, - `mockUiAction:savedObjectsManagement/copyIntoSpace`, - mockAuthorization.actions.login, // the actual privilege check deduplicates this -- we mimicked that behavior in our mock result - `mockSavedObjectAction:config/find`, - `mockUiAction:savedObjectsManagement/shareIntoSpace`, - ], - } - ); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith( - username, - 'getAll', - [savedObjects[0].id, savedObjects[1].id] - ); - }); - }); - }); - }); -}); - -describe('#get', () => { - const savedObject = { - id: 'foo', - attributes: { - name: 'foo-name', - description: 'foo-description', - bar: 'foo-bar', - }, - }; - - const expectedSpace = { - id: 'foo', - name: 'foo-name', - description: 'foo-description', - bar: 'foo-bar', - }; - - const setup = (authorization: boolean | null) => { - const result = baseSetup(authorization); - const { mockCallWithRequestRepository, mockInternalRepository } = result; - mockCallWithRequestRepository.get.mockResolvedValue(savedObject as any); - mockInternalRepository.get.mockResolvedValue(savedObject as any); - return result; - }; - - describe(`authorization is null`, () => { - test(`gets space using callWithRequestRepository`, async () => { - const { mockAuditLogger, mockCallWithRequestRepository, client } = setup(null); - const id = savedObject.id; - const actualSpace = await client.get(id); - - expect(actualSpace).toEqual(expectedSpace); - expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - }); - - describe(`authorization.mode.useRbacForRequest returns false`, () => { - test(`gets space using callWithRequestRepository`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCallWithRequestRepository, - request, - client, - } = setup(false); - const id = savedObject.id; - const actualSpace = await client.get(id); - - expect(actualSpace).toEqual(expectedSpace); - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - }); - - describe('useRbacForRequest is true', () => { - test(`throws Boom.forbidden if the user isn't authorized at space`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCheckPrivilegesAtSpace, - request, - client, - } = setup(true); - const username = Symbol(); - mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); - mockCheckPrivilegesAtSpace.mockReturnValue({ - username, - hasAllRequested: false, - }); - const id = 'foo-space'; - - await expect(client.get(id)).rejects.toThrowError('Unauthorized to get foo-space space'); - - expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesAtSpace).toHaveBeenCalledWith(id, { - kibana: mockAuthorization.actions.login, - }); - expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'get', [ - id, - ]); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns space using internalRepository if the user is authorized at space`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCheckPrivilegesAtSpace, - mockInternalRepository, - request, - client, - } = setup(true); - const username = Symbol(); - mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); - mockCheckPrivilegesAtSpace.mockReturnValue({ - username, - hasAllRequested: true, - }); - const id = savedObject.id; - - const space = await client.get(id); - - expect(space).toEqual(expectedSpace); - expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesAtSpace).toHaveBeenCalledWith(id, { - kibana: mockAuthorization.actions.login, - }); - expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'get', [ - id, - ]); - }); - }); -}); - -describe('#create', () => { - const id = 'foo'; - - const spaceToCreate = { - id, - name: 'foo-name', - description: 'foo-description', - bar: 'foo-bar', - _reserved: true, - disabledFeatures: [], - }; - - const attributes = { - name: 'foo-name', - description: 'foo-description', - bar: 'foo-bar', - disabledFeatures: [], - }; - - const savedObject = { - id, - attributes: { - name: 'foo-name', - description: 'foo-description', - bar: 'foo-bar', - disabledFeatures: [], - }, - }; - - const expectedReturnedSpace = { - id, - name: 'foo-name', - description: 'foo-description', - bar: 'foo-bar', - disabledFeatures: [], - }; - - const setup = (authorization: boolean | null) => { - const result = baseSetup(authorization); - const { mockCallWithRequestRepository, mockInternalRepository } = result; - mockCallWithRequestRepository.create.mockResolvedValue(savedObject as any); - mockInternalRepository.create.mockResolvedValue(savedObject as any); - return result; - }; - - describe(`authorization is null`, () => { - test(`creates space using callWithRequestRepository when we're under the max`, async () => { - const { mockAuditLogger, mockCallWithRequestRepository, mockConfig, client } = setup(null); - mockCallWithRequestRepository.find.mockResolvedValue({ - total: mockConfig.maxSpaces - 1, - } as any); - const actualSpace = await client.create(spaceToCreate); - - expect(actualSpace).toEqual(expectedReturnedSpace); - expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({ - type: 'space', - page: 1, - perPage: 0, - }); - expect(mockCallWithRequestRepository.create).toHaveBeenCalledWith('space', attributes, { - id, - }); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws bad request when we are at the maximum number of spaces`, async () => { - const { mockAuditLogger, mockCallWithRequestRepository, mockConfig, client } = setup(null); - mockCallWithRequestRepository.find.mockResolvedValue({ total: mockConfig.maxSpaces } as any); - await expect(client.create(spaceToCreate)).rejects.toThrowError( - 'Unable to create Space, this exceeds the maximum number of spaces set by the xpack.spaces.maxSpaces setting' - ); - - expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({ - type: 'space', - page: 1, - perPage: 0, - }); - expect(mockCallWithRequestRepository.create).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - }); - - describe(`authorization.mode.useRbacForRequest returns false`, () => { - test(`creates space using callWithRequestRepository when we're under the max`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCallWithRequestRepository, - mockConfig, - request, - client, - } = setup(false); - mockCallWithRequestRepository.find.mockResolvedValue({ - total: mockConfig.maxSpaces - 1, - } as any); - const actualSpace = await client.create(spaceToCreate); - - expect(actualSpace).toEqual(expectedReturnedSpace); - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({ - type: 'space', - page: 1, - perPage: 0, - }); - expect(mockCallWithRequestRepository.create).toHaveBeenCalledWith('space', attributes, { - id, - }); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws bad request when we're at the maximum number of spaces`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCallWithRequestRepository, - mockConfig, - request, - client, - } = setup(false); - mockCallWithRequestRepository.find.mockResolvedValue({ total: mockConfig.maxSpaces } as any); - await expect(client.create(spaceToCreate)).rejects.toThrowError( - 'Unable to create Space, this exceeds the maximum number of spaces set by the xpack.spaces.maxSpaces setting' - ); - - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({ - type: 'space', - page: 1, - perPage: 0, - }); - expect(mockCallWithRequestRepository.create).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - }); - - describe('useRbacForRequest is true', () => { - test(`throws Boom.forbidden if the user isn't authorized at space`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCheckPrivilegesGlobally, - request, - client, - } = setup(true); - const username = Symbol(); - mockCheckPrivilegesGlobally.mockReturnValue({ username, hasAllRequested: false }); - await expect(client.create(spaceToCreate)).rejects.toThrowError( - 'Unauthorized to create spaces' - ); - - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ - kibana: mockAuthorization.actions.space.manage, - }); - expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'create'); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`creates space using internalRepository if the user is authorized and we're under the max`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCheckPrivilegesGlobally, - mockConfig, - mockInternalRepository, - request, - client, - } = setup(true); - mockInternalRepository.find.mockResolvedValue({ - total: mockConfig.maxSpaces - 1, - } as any); - const username = Symbol(); - mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); - mockCheckPrivilegesGlobally.mockReturnValue({ username, hasAllRequested: true }); - const actualSpace = await client.create(spaceToCreate); - - expect(actualSpace).toEqual(expectedReturnedSpace); - expect(mockInternalRepository.find).toHaveBeenCalledWith({ - type: 'space', - page: 1, - perPage: 0, - }); - expect(mockInternalRepository.create).toHaveBeenCalledWith('space', attributes, { - id, - }); - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ - kibana: mockAuthorization.actions.space.manage, - }); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'create'); - }); - - test(`throws bad request when we are at the maximum number of spaces`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCheckPrivilegesGlobally, - mockConfig, - mockInternalRepository, - request, - client, - } = setup(true); - mockInternalRepository.find.mockResolvedValue({ total: mockConfig.maxSpaces } as any); - const username = Symbol(); - mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); - mockCheckPrivilegesGlobally.mockReturnValue({ username, hasAllRequested: true }); - await expect(client.create(spaceToCreate)).rejects.toThrowError( - 'Unable to create Space, this exceeds the maximum number of spaces set by the xpack.spaces.maxSpaces setting' - ); - - expect(mockInternalRepository.find).toHaveBeenCalledWith({ - type: 'space', - page: 1, - perPage: 0, - }); - expect(mockInternalRepository.create).not.toHaveBeenCalled(); - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ - kibana: mockAuthorization.actions.space.manage, - }); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'create'); - }); - }); -}); - -describe('#update', () => { - const spaceToUpdate = { - id: 'foo', - name: 'foo-name', - description: 'foo-description', - bar: 'foo-bar', - _reserved: false, - disabledFeatures: [], - }; - - const attributes = { - name: 'foo-name', - description: 'foo-description', - bar: 'foo-bar', - disabledFeatures: [], - }; - - const savedObject = { - id: 'foo', - attributes: { - name: 'foo-name', - description: 'foo-description', - bar: 'foo-bar', - _reserved: true, - disabledFeatures: [], - }, - }; - - const expectedReturnedSpace = { - id: 'foo', - name: 'foo-name', - description: 'foo-description', - bar: 'foo-bar', - _reserved: true, - disabledFeatures: [], - }; - - const setup = (authorization: boolean | null) => { - const result = baseSetup(authorization); - const { mockCallWithRequestRepository, mockInternalRepository } = result; - mockCallWithRequestRepository.get.mockResolvedValue(savedObject as any); - mockInternalRepository.get.mockResolvedValue(savedObject as any); - return result; - }; - - describe(`authorization is null`, () => { - test(`updates space using callWithRequestRepository`, async () => { - const { mockAuditLogger, mockCallWithRequestRepository, mockConfig, client } = setup(null); - mockCallWithRequestRepository.find.mockResolvedValue({ - total: mockConfig.maxSpaces - 1, - } as any); - const id = savedObject.id; - const actualSpace = await client.update(id, spaceToUpdate); - - expect(actualSpace).toEqual(expectedReturnedSpace); - expect(mockCallWithRequestRepository.update).toHaveBeenCalledWith('space', id, attributes); - expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - }); - describe(`authorization.mode.useRbacForRequest returns false`, () => { - test(`updates space using callWithRequestRepository`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCallWithRequestRepository, - request, - client, - } = setup(false); - const id = savedObject.id; - const actualSpace = await client.update(id, spaceToUpdate); - - expect(actualSpace).toEqual(expectedReturnedSpace); - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockCallWithRequestRepository.update).toHaveBeenCalledWith('space', id, attributes); - expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - }); - - describe('useRbacForRequest is true', () => { - test(`throws Boom.forbidden when user isn't authorized at space`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCheckPrivilegesGlobally, - request, - client, - } = setup(true); - const username = Symbol(); - mockCheckPrivilegesGlobally.mockReturnValue({ hasAllRequested: false, username }); - const id = savedObject.id; - await expect(client.update(id, spaceToUpdate)).rejects.toThrowError( - 'Unauthorized to update spaces' - ); - - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ - kibana: mockAuthorization.actions.space.manage, - }); - expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'update'); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`updates space using internalRepository if user is authorized`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCheckPrivilegesGlobally, - mockInternalRepository, - request, - client, - } = setup(true); - const username = Symbol(); - mockCheckPrivilegesGlobally.mockReturnValue({ hasAllRequested: true, username }); - mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); - const id = savedObject.id; - const actualSpace = await client.update(id, spaceToUpdate); - - expect(actualSpace).toEqual(expectedReturnedSpace); - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ - kibana: mockAuthorization.actions.space.manage, - }); - expect(mockInternalRepository.update).toHaveBeenCalledWith('space', id, attributes); - expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'update'); - }); - }); -}); - -describe('#delete', () => { - const id = 'foo'; - - const reservedSavedObject = { - id, - attributes: { - name: 'foo-name', - description: 'foo-description', - bar: 'foo-bar', - _reserved: true, - }, - }; - - const notReservedSavedObject = { - id, - attributes: { - name: 'foo-name', - description: 'foo-description', - bar: 'foo-bar', - }, - }; - - const setup = (authorization: boolean | null) => { - const result = baseSetup(authorization); - return result; - }; - - describe(`authorization is null`, () => { - test(`throws bad request when the space is reserved`, async () => { - const { mockAuditLogger, mockCallWithRequestRepository, client } = setup(null); - mockCallWithRequestRepository.get.mockResolvedValue(reservedSavedObject as any); - await expect(client.delete(id)).rejects.toThrowError( - 'This Space cannot be deleted because it is reserved.' - ); - - expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`deletes space using callWithRequestRepository when space isn't reserved`, async () => { - const { mockAuditLogger, mockCallWithRequestRepository, client } = setup(null); - mockCallWithRequestRepository.get.mockResolvedValue(notReservedSavedObject as any); - await client.delete(id); - - expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); - expect(mockCallWithRequestRepository.delete).toHaveBeenCalledWith('space', id); - expect(mockCallWithRequestRepository.deleteByNamespace).toHaveBeenCalledWith(id); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - }); - - describe(`authorization.mode.useRbacForRequest returns false`, () => { - test(`throws bad request when the space is reserved`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCallWithRequestRepository, - request, - client, - } = setup(false); - mockCallWithRequestRepository.get.mockResolvedValue(reservedSavedObject as any); - await expect(client.delete(id)).rejects.toThrowError( - 'This Space cannot be deleted because it is reserved.' - ); - - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`deletes space using callWithRequestRepository when space isn't reserved`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCallWithRequestRepository, - request, - client, - } = setup(false); - mockCallWithRequestRepository.get.mockResolvedValue(notReservedSavedObject as any); - await client.delete(id); - - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); - expect(mockCallWithRequestRepository.delete).toHaveBeenCalledWith('space', id); - expect(mockCallWithRequestRepository.deleteByNamespace).toHaveBeenCalledWith(id); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - }); - - describe('authorization.mode.useRbacForRequest returns true', () => { - test(`throws Boom.forbidden if the user isn't authorized`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCheckPrivilegesGlobally, - request, - client, - } = setup(true); - const username = Symbol(); - mockCheckPrivilegesGlobally.mockReturnValue({ username, hasAllRequested: false }); - await expect(client.delete(id)).rejects.toThrowError('Unauthorized to delete spaces'); - - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ - kibana: mockAuthorization.actions.space.manage, - }); - expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'delete'); - expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws bad request if the user is authorized but the space is reserved`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCheckPrivilegesGlobally, - mockInternalRepository, - request, - client, - } = setup(true); - const username = Symbol(); - mockCheckPrivilegesGlobally.mockReturnValue({ username, hasAllRequested: true }); - mockInternalRepository.get.mockResolvedValue(reservedSavedObject as any); - await expect(client.delete(id)).rejects.toThrowError( - 'This Space cannot be deleted because it is reserved.' - ); - - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ - kibana: mockAuthorization.actions.space.manage, - }); - expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'delete'); - }); - - test(`deletes space using internalRepository if the user is authorized and the space isn't reserved`, async () => { - const { - mockAuditLogger, - mockAuthorization, - mockCheckPrivilegesGlobally, - mockInternalRepository, - request, - client, - } = setup(true); - const username = Symbol(); - mockCheckPrivilegesGlobally.mockReturnValue({ username, hasAllRequested: true }); - mockInternalRepository.get.mockResolvedValue(notReservedSavedObject as any); - await client.delete(id); - - expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); - expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ - kibana: mockAuthorization.actions.space.manage, - }); - expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); - expect(mockInternalRepository.delete).toHaveBeenCalledWith('space', id); - expect(mockInternalRepository.deleteByNamespace).toHaveBeenCalledWith(id); - expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'delete'); - }); - }); -}); 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 deleted file mode 100644 index affe8724502d9..0000000000000 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts +++ /dev/null @@ -1,309 +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 Boom from '@hapi/boom'; -import { omit } from 'lodash'; -import { KibanaRequest } from 'src/core/server'; -import { SecurityPluginSetup } from '../../../../security/server'; -import { isReservedSpace } from '../../../common/is_reserved_space'; -import { Space } from '../../../common/model/space'; -import { SpacesAuditLogger } from '../audit_logger'; -import { ConfigType } from '../../config'; -import { GetAllSpacesPurpose, GetSpaceResult } from '../../../common/model/types'; - -interface GetAllSpacesOptions { - purpose?: GetAllSpacesPurpose; - includeAuthorizedPurposes?: boolean; -} - -const SUPPORTED_GET_SPACE_PURPOSES: GetAllSpacesPurpose[] = [ - 'any', - 'copySavedObjectsIntoSpace', - 'findSavedObjects', - 'shareSavedObjectsIntoSpace', -]; -const DEFAULT_PURPOSE = 'any'; - -const PURPOSE_PRIVILEGE_MAP: Record< - GetAllSpacesPurpose, - (authorization: SecurityPluginSetup['authz']) => string[] -> = { - any: (authorization) => [authorization.actions.login], - copySavedObjectsIntoSpace: (authorization) => [ - authorization.actions.ui.get('savedObjectsManagement', 'copyIntoSpace'), - ], - findSavedObjects: (authorization) => [ - authorization.actions.login, - authorization.actions.savedObject.get('config', 'find'), - ], - shareSavedObjectsIntoSpace: (authorization) => [ - authorization.actions.ui.get('savedObjectsManagement', 'shareIntoSpace'), - ], -}; - -function filterUnauthorizedSpaceResults(value: GetSpaceResult | null): value is GetSpaceResult { - return value !== null; -} - -export class SpacesClient { - constructor( - private readonly auditLogger: SpacesAuditLogger, - private readonly debugLogger: (message: string) => void, - private readonly authorization: SecurityPluginSetup['authz'] | null, - private readonly callWithRequestSavedObjectRepository: any, - private readonly config: ConfigType, - private readonly internalSavedObjectRepository: any, - private readonly request: KibanaRequest - ) {} - - public async getAll(options: GetAllSpacesOptions = {}): Promise { - const { purpose = DEFAULT_PURPOSE, includeAuthorizedPurposes = false } = options; - if (!SUPPORTED_GET_SPACE_PURPOSES.includes(purpose)) { - throw Boom.badRequest(`unsupported space purpose: ${purpose}`); - } - - if (options.purpose && includeAuthorizedPurposes) { - throw Boom.badRequest(`'purpose' cannot be supplied with 'includeAuthorizedPurposes'`); - } - - if (this.useRbac()) { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { saved_objects } = await this.internalSavedObjectRepository.find({ - type: 'space', - page: 1, - perPage: this.config.maxSpaces, - sortField: 'name.keyword', - }); - - this.debugLogger(`SpacesClient.getAll(), using RBAC. Found ${saved_objects.length} spaces`); - - const spaces: GetSpaceResult[] = saved_objects.map(this.transformSavedObjectToSpace); - const spaceIds = spaces.map((space: Space) => space.id); - - const checkPrivileges = this.authorization!.checkPrivilegesWithRequest(this.request); - - // Collect all privileges which need to be checked - const allPrivileges = Object.entries(PURPOSE_PRIVILEGE_MAP).reduce( - (acc, [getSpacesPurpose, privilegeFactory]) => - !includeAuthorizedPurposes && getSpacesPurpose !== purpose - ? acc - : { ...acc, [getSpacesPurpose]: privilegeFactory(this.authorization!) }, - {} as Record - ); - - // Check all privileges against all spaces - const { username, privileges } = await checkPrivileges.atSpaces(spaceIds, { - kibana: Object.values(allPrivileges).flat(), - }); - - // Determine which purposes the user is authorized for within each space. - // Remove any spaces for which user is fully unauthorized. - const checkHasAllRequired = (space: Space, actions: string[]) => - actions.every((action) => - privileges.kibana.some( - ({ resource, privilege, authorized }) => - resource === space.id && privilege === action && authorized - ) - ); - const authorizedSpaces = spaces - .map((space: Space) => { - if (!includeAuthorizedPurposes) { - // Check if the user is authorized for a single purpose - const requiredActions = PURPOSE_PRIVILEGE_MAP[purpose](this.authorization!); - return checkHasAllRequired(space, requiredActions) ? space : null; - } - - // Check if the user is authorized for each purpose - let hasAnyAuthorization = false; - const authorizedPurposes = Object.entries(PURPOSE_PRIVILEGE_MAP).reduce( - (acc, [purposeKey, privilegeFactory]) => { - const requiredActions = privilegeFactory(this.authorization!); - const hasAllRequired = checkHasAllRequired(space, requiredActions); - hasAnyAuthorization = hasAnyAuthorization || hasAllRequired; - return { ...acc, [purposeKey]: hasAllRequired }; - }, - {} as Record - ); - - if (!hasAnyAuthorization) { - return null; - } - return { ...space, authorizedPurposes }; - }) - .filter(filterUnauthorizedSpaceResults); - - if (authorizedSpaces.length === 0) { - this.debugLogger( - `SpacesClient.getAll(), using RBAC. returning 403/Forbidden. Not authorized for any spaces for ${purpose} purpose.` - ); - this.auditLogger.spacesAuthorizationFailure(username, 'getAll'); - throw Boom.forbidden(); // Note: there is a catch for this in `SpacesSavedObjectsClient.find`; if we get rid of this error, remove that too - } - - const authorizedSpaceIds = authorizedSpaces.map((s) => s.id); - this.auditLogger.spacesAuthorizationSuccess(username, 'getAll', authorizedSpaceIds); - this.debugLogger( - `SpacesClient.getAll(), using RBAC. returning spaces: ${authorizedSpaceIds.join(',')}` - ); - return authorizedSpaces; - } else { - this.debugLogger(`SpacesClient.getAll(), NOT USING RBAC. querying all spaces`); - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { saved_objects } = await this.callWithRequestSavedObjectRepository.find({ - type: 'space', - page: 1, - perPage: this.config.maxSpaces, - sortField: 'name.keyword', - }); - - this.debugLogger( - `SpacesClient.getAll(), NOT USING RBAC. Found ${saved_objects.length} spaces.` - ); - - return saved_objects.map(this.transformSavedObjectToSpace); - } - } - - public async get(id: string): Promise { - if (this.useRbac()) { - await this.ensureAuthorizedAtSpace( - id, - this.authorization!.actions.login, - 'get', - `Unauthorized to get ${id} space` - ); - } - const repository = this.useRbac() - ? this.internalSavedObjectRepository - : this.callWithRequestSavedObjectRepository; - - const savedObject = await repository.get('space', id); - return this.transformSavedObjectToSpace(savedObject); - } - - public async create(space: Space) { - if (this.useRbac()) { - this.debugLogger(`SpacesClient.create(), using RBAC. Checking if authorized globally`); - - await this.ensureAuthorizedGlobally( - this.authorization!.actions.space.manage, - 'create', - 'Unauthorized to create spaces' - ); - - this.debugLogger(`SpacesClient.create(), using RBAC. Global authorization check succeeded`); - } - const repository = this.useRbac() - ? this.internalSavedObjectRepository - : this.callWithRequestSavedObjectRepository; - - const { total } = await repository.find({ - type: 'space', - page: 1, - perPage: 0, - }); - if (total >= this.config.maxSpaces) { - throw Boom.badRequest( - 'Unable to create Space, this exceeds the maximum number of spaces set by the xpack.spaces.maxSpaces setting' - ); - } - - this.debugLogger(`SpacesClient.create(), using RBAC. Attempting to create space`); - - const attributes = omit(space, ['id', '_reserved']); - const id = space.id; - const createdSavedObject = await repository.create('space', attributes, { id }); - - this.debugLogger(`SpacesClient.create(), created space object`); - - return this.transformSavedObjectToSpace(createdSavedObject); - } - - public async update(id: string, space: Space) { - if (this.useRbac()) { - await this.ensureAuthorizedGlobally( - this.authorization!.actions.space.manage, - 'update', - 'Unauthorized to update spaces' - ); - } - const repository = this.useRbac() - ? this.internalSavedObjectRepository - : this.callWithRequestSavedObjectRepository; - - const attributes = omit(space, 'id', '_reserved'); - await repository.update('space', id, attributes); - const updatedSavedObject = await repository.get('space', id); - return this.transformSavedObjectToSpace(updatedSavedObject); - } - - public async delete(id: string) { - if (this.useRbac()) { - await this.ensureAuthorizedGlobally( - this.authorization!.actions.space.manage, - 'delete', - 'Unauthorized to delete spaces' - ); - } - - const repository = this.useRbac() - ? this.internalSavedObjectRepository - : this.callWithRequestSavedObjectRepository; - - const existingSavedObject = await repository.get('space', id); - if (isReservedSpace(this.transformSavedObjectToSpace(existingSavedObject))) { - throw Boom.badRequest('This Space cannot be deleted because it is reserved.'); - } - - await repository.deleteByNamespace(id); - - await repository.delete('space', id); - } - - private useRbac(): boolean { - return this.authorization != null && this.authorization.mode.useRbacForRequest(this.request); - } - - private async ensureAuthorizedGlobally(action: string, method: string, forbiddenMessage: string) { - const checkPrivileges = this.authorization!.checkPrivilegesWithRequest(this.request); - const { username, hasAllRequested } = await checkPrivileges.globally({ kibana: action }); - - if (hasAllRequested) { - this.auditLogger.spacesAuthorizationSuccess(username, method); - return; - } else { - this.auditLogger.spacesAuthorizationFailure(username, method); - throw Boom.forbidden(forbiddenMessage); - } - } - - private async ensureAuthorizedAtSpace( - spaceId: string, - action: string, - method: string, - forbiddenMessage: string - ) { - const checkPrivileges = this.authorization!.checkPrivilegesWithRequest(this.request); - const { username, hasAllRequested } = await checkPrivileges.atSpace(spaceId, { - kibana: action, - }); - - if (hasAllRequested) { - this.auditLogger.spacesAuthorizationSuccess(username, method, [spaceId]); - return; - } else { - this.auditLogger.spacesAuthorizationFailure(username, method, [spaceId]); - throw Boom.forbidden(forbiddenMessage); - } - } - - private transformSavedObjectToSpace(savedObject: any): Space { - return { - id: savedObject.id, - ...savedObject.attributes, - } as Space; - } -} diff --git a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts index 8ec2e6f978d81..e63850a96900d 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts @@ -4,31 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as Rx from 'rxjs'; import { DEFAULT_SPACE_ID } from '../../common/constants'; import { createSpacesTutorialContextFactory } from './spaces_tutorial_context_factory'; import { SpacesService } from '../spaces_service'; -import { SpacesAuditLogger } from './audit_logger'; -import { coreMock, loggingSystemMock } from '../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../src/core/server/mocks'; import { spacesServiceMock } from '../spaces_service/spaces_service.mock'; -import { spacesConfig } from './__fixtures__'; -import { securityMock } from '../../../security/server/mocks'; +import { spacesClientServiceMock } from '../spaces_client/spaces_client_service.mock'; -const log = loggingSystemMock.createLogger(); - -const service = new SpacesService(log); +const service = new SpacesService(); describe('createSpacesTutorialContextFactory', () => { it('should create a valid context factory', async () => { - const spacesService = spacesServiceMock.createSetupContract(); - expect(typeof createSpacesTutorialContextFactory(spacesService)).toEqual('function'); + const spacesService = spacesServiceMock.createStartContract(); + expect(typeof createSpacesTutorialContextFactory(() => spacesService)).toEqual('function'); }); it('should create context with the current space id for space my-space-id', async () => { - const spacesService = spacesServiceMock.createSetupContract('my-space-id'); - const contextFactory = createSpacesTutorialContextFactory(spacesService); + const spacesService = spacesServiceMock.createStartContract('my-space-id'); + const contextFactory = createSpacesTutorialContextFactory(() => spacesService); - const request = {}; + const request = httpServerMock.createKibanaRequest(); expect(contextFactory(request)).toEqual({ spaceId: 'my-space-id', @@ -37,16 +32,17 @@ describe('createSpacesTutorialContextFactory', () => { }); it('should create context with the current space id for the default space', async () => { - const spacesService = await service.setup({ - http: coreMock.createSetup().http, - getStartServices: async () => [coreMock.createStart(), {}, {}], - authorization: securityMock.createSetup().authz, - auditLogger: {} as SpacesAuditLogger, - config$: Rx.of(spacesConfig), + service.setup({ + basePath: coreMock.createSetup().http.basePath, }); - const contextFactory = createSpacesTutorialContextFactory(spacesService); - - const request = {}; + const contextFactory = createSpacesTutorialContextFactory(() => + service.start({ + basePath: coreMock.createStart().http.basePath, + spacesClientService: spacesClientServiceMock.createStart(), + }) + ); + + const request = httpServerMock.createKibanaRequest(); expect(contextFactory(request)).toEqual({ spaceId: DEFAULT_SPACE_ID, diff --git a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.ts b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.ts index f89681b709949..af5b5490a28ef 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SpacesServiceSetup } from '../spaces_service/spaces_service'; +import { KibanaRequest } from 'src/core/server'; +import { SpacesServiceStart } from '../spaces_service/spaces_service'; -export function createSpacesTutorialContextFactory(spacesService: SpacesServiceSetup) { - return function spacesTutorialContextFactory(request: any) { +export function createSpacesTutorialContextFactory(getSpacesService: () => SpacesServiceStart) { + return function spacesTutorialContextFactory(request: KibanaRequest) { + const spacesService = getSpacesService(); return { spaceId: spacesService.getSpaceId(request), isInDefaultSpace: spacesService.isInDefaultSpace(request), diff --git a/x-pack/plugins/spaces/server/mocks.ts b/x-pack/plugins/spaces/server/mocks.ts index 99d547a92eeb6..3ef3f954b328d 100644 --- a/x-pack/plugins/spaces/server/mocks.ts +++ b/x-pack/plugins/spaces/server/mocks.ts @@ -3,12 +3,25 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { spacesClientServiceMock } from './spaces_client/spaces_client_service.mock'; import { spacesServiceMock } from './spaces_service/spaces_service.mock'; function createSetupMock() { - return { spacesService: spacesServiceMock.createSetupContract() }; + return { + spacesService: spacesServiceMock.createSetupContract(), + spacesClient: spacesClientServiceMock.createSetup(), + }; +} + +function createStartMock() { + return { + spacesService: spacesServiceMock.createStartContract(), + }; } export const spacesMock = { createSetup: createSetupMock, + createStart: createStartMock, }; + +export { spacesClientMock } from './spaces_client/spaces_client.mock'; diff --git a/x-pack/plugins/spaces/server/plugin.test.ts b/x-pack/plugins/spaces/server/plugin.test.ts index b650a114ed978..fad54ceaa882b 100644 --- a/x-pack/plugins/spaces/server/plugin.test.ts +++ b/x-pack/plugins/spaces/server/plugin.test.ts @@ -13,30 +13,30 @@ import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collect describe('Spaces Plugin', () => { describe('#setup', () => { - it('can setup with all optional plugins disabled, exposing the expected contract', async () => { + it('can setup with all optional plugins disabled, exposing the expected contract', () => { const initializerContext = coreMock.createPluginInitializerContext({}); const core = coreMock.createSetup() as CoreSetup; const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); const plugin = new Plugin(initializerContext); - const spacesSetup = await plugin.setup(core, { features, licensing }); + const spacesSetup = plugin.setup(core, { features, licensing }); expect(spacesSetup).toMatchInlineSnapshot(` Object { + "spacesClient": Object { + "registerClientWrapper": [Function], + "setClientRepositoryFactory": [Function], + }, "spacesService": Object { - "getActiveSpace": [Function], - "getBasePath": [Function], "getSpaceId": [Function], - "isInDefaultSpace": [Function], "namespaceToSpaceId": [Function], - "scopedClient": [Function], "spaceIdToNamespace": [Function], }, } `); }); - it('registers the capabilities provider and switcher', async () => { + it('registers the capabilities provider and switcher', () => { const initializerContext = coreMock.createPluginInitializerContext({}); const core = coreMock.createSetup() as CoreSetup; const features = featuresPluginMock.createSetup(); @@ -44,13 +44,13 @@ describe('Spaces Plugin', () => { const plugin = new Plugin(initializerContext); - await plugin.setup(core, { features, licensing }); + plugin.setup(core, { features, licensing }); expect(core.capabilities.registerProvider).toHaveBeenCalledTimes(1); expect(core.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); }); - it('registers the usage collector', async () => { + it('registers the usage collector', () => { const initializerContext = coreMock.createPluginInitializerContext({}); const core = coreMock.createSetup() as CoreSetup; const features = featuresPluginMock.createSetup(); @@ -60,12 +60,12 @@ describe('Spaces Plugin', () => { const plugin = new Plugin(initializerContext); - await plugin.setup(core, { features, licensing, usageCollection }); + plugin.setup(core, { features, licensing, usageCollection }); expect(usageCollection.getCollectorByType('spaces')).toBeDefined(); }); - it('registers the "space" saved object type and client wrapper', async () => { + it('registers the "space" saved object type and client wrapper', () => { const initializerContext = coreMock.createPluginInitializerContext({}); const core = coreMock.createSetup() as CoreSetup; const features = featuresPluginMock.createSetup(); @@ -73,7 +73,7 @@ describe('Spaces Plugin', () => { const plugin = new Plugin(initializerContext); - await plugin.setup(core, { features, licensing }); + plugin.setup(core, { features, licensing }); expect(core.savedObjects.registerType).toHaveBeenCalledWith({ name: 'space', @@ -90,4 +90,32 @@ describe('Spaces Plugin', () => { ); }); }); + + describe('#start', () => { + it('can start with all optional plugins disabled, exposing the expected contract', () => { + const initializerContext = coreMock.createPluginInitializerContext({}); + const coreSetup = coreMock.createSetup() as CoreSetup; + const features = featuresPluginMock.createSetup(); + const licensing = licensingMock.createSetup(); + + const plugin = new Plugin(initializerContext); + plugin.setup(coreSetup, { features, licensing }); + + const coreStart = coreMock.createStart(); + + const spacesStart = plugin.start(coreStart); + expect(spacesStart).toMatchInlineSnapshot(` + Object { + "spacesService": Object { + "createSpacesClient": [Function], + "getActiveSpace": [Function], + "getSpaceId": [Function], + "isInDefaultSpace": [Function], + "namespaceToSpaceId": [Function], + "spaceIdToNamespace": [Function], + }, + } + `); + }); + }); }); diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index a9ba5ac2dc6de..517fde6ecb41a 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -7,17 +7,20 @@ import { Observable } from 'rxjs'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { HomeServerPluginSetup } from 'src/plugins/home/server'; -import { CoreSetup, Logger, PluginInitializerContext } from '../../../../src/core/server'; +import { + CoreSetup, + CoreStart, + Logger, + PluginInitializerContext, +} from '../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetup, PluginStartContract as FeaturesPluginStart, } from '../../features/server'; -import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginSetup } from '../../licensing/server'; -import { SpacesAuditLogger } from './lib/audit_logger'; import { createSpacesTutorialContextFactory } from './lib/spaces_tutorial_context_factory'; import { registerSpacesUsageCollector } from './usage_collection'; -import { SpacesService } from './spaces_service'; +import { SpacesService, SpacesServiceStart } from './spaces_service'; import { SpacesServiceSetup } from './spaces_service'; import { ConfigType } from './config'; import { initSpacesRequestInterceptors } from './lib/request_interceptors'; @@ -28,11 +31,15 @@ import { setupCapabilities } from './capabilities'; import { SpacesSavedObjectsService } from './saved_objects'; import { DefaultSpaceService } from './default_space'; import { SpacesLicenseService } from '../common/licensing'; +import { + SpacesClientRepositoryFactory, + SpacesClientService, + SpacesClientWrapper, +} from './spaces_client'; export interface PluginsSetup { features: FeaturesPluginSetup; licensing: LicensingPluginSetup; - security?: SecurityPluginSetup; usageCollection?: UsageCollectionSetup; home?: HomeServerPluginSetup; } @@ -43,11 +50,17 @@ export interface PluginsStart { export interface SpacesPluginSetup { spacesService: SpacesServiceSetup; + spacesClient: { + setClientRepositoryFactory: (factory: SpacesClientRepositoryFactory) => void; + registerClientWrapper: (wrapper: SpacesClientWrapper) => void; + }; } -export class Plugin { - private readonly pluginId = 'spaces'; +export interface SpacesPluginStart { + spacesService: SpacesServiceStart; +} +export class Plugin { private readonly config$: Observable; private readonly kibanaIndexConfig$: Observable<{ kibana: { index: string } }>; @@ -56,32 +69,38 @@ export class Plugin { private readonly spacesLicenseService = new SpacesLicenseService(); + private readonly spacesClientService: SpacesClientService; + + private readonly spacesService: SpacesService; + + private spacesServiceStart?: SpacesServiceStart; + private defaultSpaceService?: DefaultSpaceService; constructor(initializerContext: PluginInitializerContext) { this.config$ = initializerContext.config.create(); this.kibanaIndexConfig$ = initializerContext.config.legacy.globalConfig$; this.log = initializerContext.logger.get(); + this.spacesService = new SpacesService(); + this.spacesClientService = new SpacesClientService((message) => this.log.debug(message)); } - public async start() {} - - public async setup( - core: CoreSetup, - plugins: PluginsSetup - ): Promise { - const service = new SpacesService(this.log); + public setup(core: CoreSetup, plugins: PluginsSetup): SpacesPluginSetup { + const spacesClientSetup = this.spacesClientService.setup({ config$: this.config$ }); - const spacesService = await service.setup({ - http: core.http, - getStartServices: core.getStartServices, - authorization: plugins.security ? plugins.security.authz : null, - auditLogger: new SpacesAuditLogger(plugins.security?.audit.getLogger(this.pluginId)), - config$: this.config$, + const spacesServiceSetup = this.spacesService.setup({ + basePath: core.http.basePath, }); + const getSpacesService = () => { + if (!this.spacesServiceStart) { + throw new Error('spaces service has not been initialized!'); + } + return this.spacesServiceStart; + }; + const savedObjectsService = new SpacesSavedObjectsService(); - savedObjectsService.setup({ core, spacesService }); + savedObjectsService.setup({ core, getSpacesService }); const { license } = this.spacesLicenseService.setup({ license$: plugins.licensing.license$ }); @@ -106,24 +125,23 @@ export class Plugin { log: this.log, getStartServices: core.getStartServices, getImportExportObjectLimit: core.savedObjects.getImportExportObjectLimit, - spacesService, - authorization: plugins.security ? plugins.security.authz : null, + getSpacesService, }); const internalRouter = core.http.createRouter(); initInternalSpacesApi({ internalRouter, - spacesService, + getSpacesService, }); initSpacesRequestInterceptors({ http: core.http, log: this.log, - spacesService, + getSpacesService, features: plugins.features, }); - setupCapabilities(core, spacesService, this.log); + setupCapabilities(core, getSpacesService, this.log); if (plugins.usageCollection) { registerSpacesUsageCollector(plugins.usageCollection, { @@ -133,18 +151,28 @@ export class Plugin { }); } - if (plugins.security) { - plugins.security.registerSpacesService(spacesService); - } - if (plugins.home) { plugins.home.tutorials.addScopedTutorialContextFactory( - createSpacesTutorialContextFactory(spacesService) + createSpacesTutorialContextFactory(getSpacesService) ); } return { - spacesService, + spacesClient: spacesClientSetup, + spacesService: spacesServiceSetup, + }; + } + + public start(core: CoreStart) { + const spacesClientStart = this.spacesClientService.start(core); + + this.spacesServiceStart = this.spacesService.start({ + basePath: core.http.basePath, + spacesClientService: spacesClientStart, + }); + + return { + spacesService: this.spacesServiceStart, }; } diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_repository.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_repository.ts index 86db8a2eb2000..f1e641382452e 100644 --- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_repository.ts +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_repository.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract, SavedObjectsErrorHelpers } from 'src/core/server'; +import { ISavedObjectsRepository, SavedObjectsErrorHelpers } from 'src/core/server'; export const createMockSavedObjectsRepository = (spaces: any[] = []) => { const mockSavedObjectsClientContract = ({ @@ -37,7 +37,7 @@ export const createMockSavedObjectsRepository = (spaces: any[] = []) => { return {}; }), deleteByNamespace: jest.fn(), - } as unknown) as jest.Mocked; + } as unknown) as jest.Mocked; return 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 341e5cf3bfbe0..a6e1c11d011a0 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 @@ -14,7 +14,7 @@ import { createResolveSavedObjectsImportErrorsMock, createMockSavedObjectsService, } from '../__fixtures__'; -import { CoreSetup, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server'; +import { kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server'; import { loggingSystemMock, httpServiceMock, @@ -22,11 +22,8 @@ import { coreMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; -import { SpacesAuditLogger } from '../../../lib/audit_logger'; -import { SpacesClient } from '../../../lib/spaces_client'; import { initCopyToSpacesApi } from './copy_to_space'; import { spacesConfig } from '../../../lib/__fixtures__'; -import { securityMock } from '../../../../../security/server/mocks'; import { ObjectType } from '@kbn/config-schema'; jest.mock('../../../../../../../src/core/server', () => { return { @@ -41,6 +38,7 @@ import { importSavedObjectsFromStream, resolveSavedObjectsImportErrors, } from '../../../../../../../src/core/server'; +import { SpacesClientService } from '../../../spaces_client'; describe('copy to space', () => { const spacesSavedObjects = createSpaces(); @@ -74,27 +72,21 @@ describe('copy to space', () => { const { savedObjects } = createMockSavedObjectsService(spaces); coreStart.savedObjects = savedObjects; - const service = new SpacesService(log); - const spacesService = await service.setup({ - http: (httpService as unknown) as CoreSetup['http'], - getStartServices: async () => [coreStart, {}, {}], - authorization: securityMock.createSetup().authz, - auditLogger: {} as SpacesAuditLogger, - config$: Rx.of(spacesConfig), + const clientService = new SpacesClientService(jest.fn()); + clientService + .setup({ config$: Rx.of(spacesConfig) }) + .setClientRepositoryFactory(() => savedObjectsRepositoryMock); + + const service = new SpacesService(); + service.setup({ + basePath: httpService.basePath, }); - spacesService.scopedClient = jest.fn((req: any) => { - return Promise.resolve( - new SpacesClient( - null as any, - () => null, - null, - savedObjectsRepositoryMock, - spacesConfig, - savedObjectsRepositoryMock, - req - ) - ); + const clientServiceStart = clientService.start(coreStart); + + const spacesServiceStart = service.start({ + basePath: coreStart.http.basePath, + spacesClientService: clientServiceStart, }); initCopyToSpacesApi({ @@ -102,8 +94,7 @@ describe('copy to space', () => { getStartServices: async () => [coreStart, {}, {}], getImportExportObjectLimit: () => 1000, log, - spacesService, - authorization: null, // not needed for this route + getSpacesService: () => spacesServiceStart, }); const [ 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 fef1646067fde..989c513ac00bc 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 @@ -21,7 +21,7 @@ const areObjectsUnique = (objects: SavedObjectIdentifier[]) => _.uniqBy(objects, (o: SavedObjectIdentifier) => `${o.type}:${o.id}`).length === objects.length; export function initCopyToSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, spacesService, getImportExportObjectLimit, getStartServices } = deps; + const { externalRouter, getSpacesService, getImportExportObjectLimit, getStartServices } = deps; externalRouter.post( { @@ -90,7 +90,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { overwrite, createNewCopies, } = request.body; - const sourceSpaceId = spacesService.getSpaceId(request); + const sourceSpaceId = getSpacesService().getSpaceId(request); const copyResponse = await copySavedObjectsToSpaces(sourceSpaceId, destinationSpaceIds, { objects, includeReferences, @@ -155,7 +155,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { request ); const { objects, includeReferences, retries, createNewCopies } = request.body; - const sourceSpaceId = spacesService.getSpaceId(request); + const sourceSpaceId = getSpacesService().getSpaceId(request); const resolveConflictsResponse = await resolveCopySavedObjectsToSpacesConflicts( sourceSpaceId, { diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts index 4fe81027c3508..c9b5fc96094cb 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts @@ -12,7 +12,6 @@ import { mockRouteContextWithInvalidLicense, } from '../__fixtures__'; import { - CoreSetup, kibanaResponseFactory, RouteValidatorConfig, SavedObjectsErrorHelpers, @@ -24,12 +23,10 @@ import { coreMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; -import { SpacesAuditLogger } from '../../../lib/audit_logger'; -import { SpacesClient } from '../../../lib/spaces_client'; import { initDeleteSpacesApi } from './delete'; import { spacesConfig } from '../../../lib/__fixtures__'; -import { securityMock } from '../../../../../security/server/mocks'; import { ObjectType } from '@kbn/config-schema'; +import { SpacesClientService } from '../../../spaces_client'; describe('Spaces Public API', () => { const spacesSavedObjects = createSpaces(); @@ -44,27 +41,21 @@ describe('Spaces Public API', () => { const coreStart = coreMock.createStart(); - const service = new SpacesService(log); - const spacesService = await service.setup({ - http: (httpService as unknown) as CoreSetup['http'], - getStartServices: async () => [coreStart, {}, {}], - authorization: securityMock.createSetup().authz, - auditLogger: {} as SpacesAuditLogger, - config$: Rx.of(spacesConfig), + const clientService = new SpacesClientService(jest.fn()); + clientService + .setup({ config$: Rx.of(spacesConfig) }) + .setClientRepositoryFactory(() => savedObjectsRepositoryMock); + + const service = new SpacesService(); + service.setup({ + basePath: httpService.basePath, }); - spacesService.scopedClient = jest.fn((req: any) => { - return Promise.resolve( - new SpacesClient( - null as any, - () => null, - null, - savedObjectsRepositoryMock, - spacesConfig, - savedObjectsRepositoryMock, - req - ) - ); + const clientServiceStart = clientService.start(coreStart); + + const spacesServiceStart = service.start({ + basePath: coreStart.http.basePath, + spacesClientService: clientServiceStart, }); initDeleteSpacesApi({ @@ -72,8 +63,7 @@ describe('Spaces Public API', () => { getStartServices: async () => [coreStart, {}, {}], getImportExportObjectLimit: () => 1000, log, - spacesService, - authorization: null, // not needed for this route + getSpacesService: () => spacesServiceStart, }); const [routeDefinition, routeHandler] = router.delete.mock.calls[0]; @@ -186,6 +176,6 @@ describe('Spaces Public API', () => { const { status, payload } = response; expect(status).toEqual(400); - expect(payload.message).toEqual('This Space cannot be deleted because it is reserved.'); + expect(payload.message).toEqual('The default space cannot be deleted because it is reserved.'); }); }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.ts index 81e643bf5ede8..794698fd91cb0 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.ts @@ -8,12 +8,11 @@ import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; import { SavedObjectsErrorHelpers } from '../../../../../../../src/core/server'; import { wrapError } from '../../../lib/errors'; -import { SpacesClient } from '../../../lib/spaces_client'; import { ExternalRouteDeps } from '.'; import { createLicensedRouteHandler } from '../../lib'; export function initDeleteSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, log, spacesService } = deps; + const { externalRouter, log, getSpacesService } = deps; externalRouter.delete( { @@ -25,7 +24,7 @@ export function initDeleteSpacesApi(deps: ExternalRouteDeps) { }, }, createLicensedRouteHandler(async (context, request, response) => { - const spacesClient: SpacesClient = await spacesService.scopedClient(request); + const spacesClient = getSpacesService().createSpacesClient(request); const id = request.params.id; diff --git a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts index 4786399936662..6fa26a7bcd557 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts @@ -11,7 +11,7 @@ import { mockRouteContext, } from '../__fixtures__'; import { initGetSpaceApi } from './get'; -import { CoreSetup, kibanaResponseFactory } from 'src/core/server'; +import { kibanaResponseFactory } from 'src/core/server'; import { loggingSystemMock, httpServiceMock, @@ -19,10 +19,8 @@ import { coreMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; -import { SpacesAuditLogger } from '../../../lib/audit_logger'; -import { SpacesClient } from '../../../lib/spaces_client'; import { spacesConfig } from '../../../lib/__fixtures__'; -import { securityMock } from '../../../../../security/server/mocks'; +import { SpacesClientService } from '../../../spaces_client'; describe('GET space', () => { const spacesSavedObjects = createSpaces(); @@ -38,27 +36,21 @@ describe('GET space', () => { const log = loggingSystemMock.create().get('spaces'); - const service = new SpacesService(log); - const spacesService = await service.setup({ - http: (httpService as unknown) as CoreSetup['http'], - getStartServices: async () => [coreStart, {}, {}], - authorization: securityMock.createSetup().authz, - auditLogger: {} as SpacesAuditLogger, - config$: Rx.of(spacesConfig), + const clientService = new SpacesClientService(jest.fn()); + clientService + .setup({ config$: Rx.of(spacesConfig) }) + .setClientRepositoryFactory(() => savedObjectsRepositoryMock); + + const service = new SpacesService(); + service.setup({ + basePath: httpService.basePath, }); - spacesService.scopedClient = jest.fn((req: any) => { - return Promise.resolve( - new SpacesClient( - null as any, - () => null, - null, - savedObjectsRepositoryMock, - spacesConfig, - savedObjectsRepositoryMock, - req - ) - ); + const clientServiceStart = clientService.start(coreStart); + + const spacesServiceStart = service.start({ + basePath: coreStart.http.basePath, + spacesClientService: clientServiceStart, }); initGetSpaceApi({ @@ -66,8 +58,7 @@ describe('GET space', () => { getStartServices: async () => [coreStart, {}, {}], getImportExportObjectLimit: () => 1000, log, - spacesService, - authorization: null, // not needed for this route + getSpacesService: () => spacesServiceStart, }); return { diff --git a/x-pack/plugins/spaces/server/routes/api/external/get.ts b/x-pack/plugins/spaces/server/routes/api/external/get.ts index 150c9f05156a2..2644e74ec4bf9 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get.ts @@ -11,7 +11,7 @@ import { ExternalRouteDeps } from '.'; import { createLicensedRouteHandler } from '../../lib'; export function initGetSpaceApi(deps: ExternalRouteDeps) { - const { externalRouter, spacesService } = deps; + const { externalRouter, getSpacesService } = deps; externalRouter.get( { @@ -24,7 +24,7 @@ export function initGetSpaceApi(deps: ExternalRouteDeps) { }, createLicensedRouteHandler(async (context, request, response) => { const spaceId = request.params.id; - const spacesClient = await spacesService.scopedClient(request); + const spacesClient = getSpacesService().createSpacesClient(request); try { const space = await spacesClient.get(spaceId); 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 81746c9db53c4..5b24a33cb014d 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 @@ -10,7 +10,7 @@ import { mockRouteContext, mockRouteContextWithInvalidLicense, } from '../__fixtures__'; -import { CoreSetup, kibanaResponseFactory } from 'src/core/server'; +import { kibanaResponseFactory } from 'src/core/server'; import { loggingSystemMock, httpServiceMock, @@ -18,11 +18,10 @@ import { coreMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; -import { SpacesAuditLogger } from '../../../lib/audit_logger'; -import { SpacesClient } from '../../../lib/spaces_client'; import { initGetAllSpacesApi } from './get_all'; import { spacesConfig } from '../../../lib/__fixtures__'; -import { securityMock } from '../../../../../security/server/mocks'; +import { ObjectType } from '@kbn/config-schema'; +import { SpacesClientService } from '../../../spaces_client'; describe('GET /spaces/space', () => { const spacesSavedObjects = createSpaces(); @@ -38,27 +37,21 @@ describe('GET /spaces/space', () => { const log = loggingSystemMock.create().get('spaces'); - const service = new SpacesService(log); - const spacesService = await service.setup({ - http: (httpService as unknown) as CoreSetup['http'], - getStartServices: async () => [coreStart, {}, {}], - authorization: securityMock.createSetup().authz, - auditLogger: {} as SpacesAuditLogger, - config$: Rx.of(spacesConfig), + const clientService = new SpacesClientService(jest.fn()); + clientService + .setup({ config$: Rx.of(spacesConfig) }) + .setClientRepositoryFactory(() => savedObjectsRepositoryMock); + + const service = new SpacesService(); + service.setup({ + basePath: httpService.basePath, }); - spacesService.scopedClient = jest.fn((req: any) => { - return Promise.resolve( - new SpacesClient( - null as any, - () => null, - null, - savedObjectsRepositoryMock, - spacesConfig, - savedObjectsRepositoryMock, - req - ) - ); + const clientServiceStart = clientService.start(coreStart); + + const spacesServiceStart = service.start({ + basePath: coreStart.http.basePath, + spacesClientService: clientServiceStart, }); initGetAllSpacesApi({ @@ -66,11 +59,11 @@ describe('GET /spaces/space', () => { getStartServices: async () => [coreStart, {}, {}], getImportExportObjectLimit: () => 1000, log, - spacesService, - authorization: null, // not needed for this route + getSpacesService: () => spacesServiceStart, }); return { + routeConfig: router.get.mock.calls[0][0], routeHandler: router.get.mock.calls[0][1], }; }; @@ -89,21 +82,27 @@ describe('GET /spaces/space', () => { }); it(`returns expected result when specifying include_authorized_purposes=true`, async () => { - const { routeHandler } = await setup(); + const { routeConfig, routeHandler } = await setup(); const request = httpServerMock.createKibanaRequest({ method: 'get', query: { purpose, include_authorized_purposes: true }, }); + + if (routeConfig.validate === false) { + throw new Error('Test setup failure. Expected route validation'); + } + const queryParamsValidation = routeConfig.validate.query! as ObjectType; + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); if (purpose === undefined) { + expect(() => queryParamsValidation.validate(request.query)).not.toThrow(); expect(response.status).toEqual(200); expect(response.payload).toEqual(spaces); } else { - expect(response.status).toEqual(400); - expect(response.payload).toEqual( - new Error(`'purpose' cannot be supplied with 'includeAuthorizedPurposes'`) + expect(() => queryParamsValidation.validate(request.query)).toThrowError( + '[include_authorized_purposes]: expected value to equal [false]' ); } }); 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 2ee1146250b49..20ad5e730db6b 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 @@ -11,7 +11,7 @@ import { ExternalRouteDeps } from '.'; import { createLicensedRouteHandler } from '../../lib'; export function initGetAllSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, log, spacesService } = deps; + const { externalRouter, log, getSpacesService } = deps; externalRouter.get( { @@ -39,7 +39,7 @@ export function initGetAllSpacesApi(deps: ExternalRouteDeps) { const { purpose, include_authorized_purposes: includeAuthorizedPurposes } = request.query; - const spacesClient = await spacesService.scopedClient(request); + const spacesClient = getSpacesService().createSpacesClient(request); let spaces: Space[]; diff --git a/x-pack/plugins/spaces/server/routes/api/external/index.ts b/x-pack/plugins/spaces/server/routes/api/external/index.ts index f093f26b4bdee..e34f67adc04ac 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/index.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/index.ts @@ -5,13 +5,12 @@ */ import { Logger, IRouter, CoreSetup } from 'src/core/server'; -import { SecurityPluginSetup } from '../../../../../security/server'; import { initDeleteSpacesApi } from './delete'; import { initGetSpaceApi } from './get'; import { initGetAllSpacesApi } from './get_all'; import { initPostSpacesApi } from './post'; import { initPutSpacesApi } from './put'; -import { SpacesServiceSetup } from '../../../spaces_service/spaces_service'; +import { SpacesServiceStart } from '../../../spaces_service/spaces_service'; import { initCopyToSpacesApi } from './copy_to_space'; import { initShareToSpacesApi } from './share_to_space'; @@ -19,9 +18,8 @@ export interface ExternalRouteDeps { externalRouter: IRouter; getStartServices: CoreSetup['getStartServices']; getImportExportObjectLimit: () => number; - spacesService: SpacesServiceSetup; + getSpacesService: () => SpacesServiceStart; log: Logger; - authorization: SecurityPluginSetup['authz'] | null; } export function initExternalSpacesApi(deps: ExternalRouteDeps) { diff --git a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts index 6aeec251e33e4..bd8b4f2119109 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts @@ -10,7 +10,7 @@ import { mockRouteContext, mockRouteContextWithInvalidLicense, } from '../__fixtures__'; -import { CoreSetup, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server'; +import { kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server'; import { loggingSystemMock, httpServerMock, @@ -18,12 +18,10 @@ import { coreMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; -import { SpacesAuditLogger } from '../../../lib/audit_logger'; -import { SpacesClient } from '../../../lib/spaces_client'; import { initPostSpacesApi } from './post'; import { spacesConfig } from '../../../lib/__fixtures__'; -import { securityMock } from '../../../../../security/server/mocks'; import { ObjectType } from '@kbn/config-schema'; +import { SpacesClientService } from '../../../spaces_client'; describe('Spaces Public API', () => { const spacesSavedObjects = createSpaces(); @@ -38,27 +36,21 @@ describe('Spaces Public API', () => { const log = loggingSystemMock.create().get('spaces'); - const service = new SpacesService(log); - const spacesService = await service.setup({ - http: (httpService as unknown) as CoreSetup['http'], - getStartServices: async () => [coreStart, {}, {}], - authorization: securityMock.createSetup().authz, - auditLogger: {} as SpacesAuditLogger, - config$: Rx.of(spacesConfig), + const clientService = new SpacesClientService(jest.fn()); + clientService + .setup({ config$: Rx.of(spacesConfig) }) + .setClientRepositoryFactory(() => savedObjectsRepositoryMock); + + const service = new SpacesService(); + service.setup({ + basePath: httpService.basePath, }); - spacesService.scopedClient = jest.fn((req: any) => { - return Promise.resolve( - new SpacesClient( - null as any, - () => null, - null, - savedObjectsRepositoryMock, - spacesConfig, - savedObjectsRepositoryMock, - req - ) - ); + const clientServiceStart = clientService.start(coreStart); + + const spacesServiceStart = service.start({ + basePath: coreStart.http.basePath, + spacesClientService: clientServiceStart, }); initPostSpacesApi({ @@ -66,8 +58,7 @@ describe('Spaces Public API', () => { getStartServices: async () => [coreStart, {}, {}], getImportExportObjectLimit: () => 1000, log, - spacesService, - authorization: null, // not needed for this route + getSpacesService: () => spacesServiceStart, }); const [routeDefinition, routeHandler] = router.post.mock.calls[0]; diff --git a/x-pack/plugins/spaces/server/routes/api/external/post.ts b/x-pack/plugins/spaces/server/routes/api/external/post.ts index 0c77bcc74bb50..a6a1f26c7955c 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/post.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/post.ts @@ -11,7 +11,7 @@ import { ExternalRouteDeps } from '.'; import { createLicensedRouteHandler } from '../../lib'; export function initPostSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, log, spacesService } = deps; + const { externalRouter, log, getSpacesService } = deps; externalRouter.post( { @@ -22,7 +22,7 @@ export function initPostSpacesApi(deps: ExternalRouteDeps) { }, createLicensedRouteHandler(async (context, request, response) => { log.debug(`Inside POST /api/spaces/space`); - const spacesClient = await spacesService.scopedClient(request); + const spacesClient = getSpacesService().createSpacesClient(request); const space = request.body; diff --git a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts index 326837f8995f0..d87cfd96e2429 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts @@ -11,7 +11,7 @@ import { mockRouteContext, mockRouteContextWithInvalidLicense, } from '../__fixtures__'; -import { CoreSetup, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server'; +import { kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server'; import { loggingSystemMock, httpServiceMock, @@ -19,12 +19,10 @@ import { coreMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; -import { SpacesAuditLogger } from '../../../lib/audit_logger'; -import { SpacesClient } from '../../../lib/spaces_client'; import { initPutSpacesApi } from './put'; import { spacesConfig } from '../../../lib/__fixtures__'; -import { securityMock } from '../../../../../security/server/mocks'; import { ObjectType } from '@kbn/config-schema'; +import { SpacesClientService } from '../../../spaces_client'; describe('PUT /api/spaces/space', () => { const spacesSavedObjects = createSpaces(); @@ -39,27 +37,21 @@ describe('PUT /api/spaces/space', () => { const log = loggingSystemMock.create().get('spaces'); - const service = new SpacesService(log); - const spacesService = await service.setup({ - http: (httpService as unknown) as CoreSetup['http'], - getStartServices: async () => [coreStart, {}, {}], - authorization: securityMock.createSetup().authz, - auditLogger: {} as SpacesAuditLogger, - config$: Rx.of(spacesConfig), + const clientService = new SpacesClientService(jest.fn()); + clientService + .setup({ config$: Rx.of(spacesConfig) }) + .setClientRepositoryFactory(() => savedObjectsRepositoryMock); + + const service = new SpacesService(); + service.setup({ + basePath: httpService.basePath, }); - spacesService.scopedClient = jest.fn((req: any) => { - return Promise.resolve( - new SpacesClient( - null as any, - () => null, - null, - savedObjectsRepositoryMock, - spacesConfig, - savedObjectsRepositoryMock, - req - ) - ); + const clientServiceStart = clientService.start(coreStart); + + const spacesServiceStart = service.start({ + basePath: coreStart.http.basePath, + spacesClientService: clientServiceStart, }); initPutSpacesApi({ @@ -67,8 +59,7 @@ describe('PUT /api/spaces/space', () => { getStartServices: async () => [coreStart, {}, {}], getImportExportObjectLimit: () => 1000, log, - spacesService, - authorization: null, // not needed for this route + getSpacesService: () => spacesServiceStart, }); const [routeDefinition, routeHandler] = router.put.mock.calls[0]; diff --git a/x-pack/plugins/spaces/server/routes/api/external/put.ts b/x-pack/plugins/spaces/server/routes/api/external/put.ts index 2054cf5d1c829..68ebdb55af1e3 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/put.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/put.ts @@ -13,7 +13,7 @@ import { ExternalRouteDeps } from '.'; import { createLicensedRouteHandler } from '../../lib'; export function initPutSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, spacesService } = deps; + const { externalRouter, getSpacesService } = deps; externalRouter.put( { @@ -26,7 +26,7 @@ export function initPutSpacesApi(deps: ExternalRouteDeps) { }, }, createLicensedRouteHandler(async (context, request, response) => { - const spacesClient = await spacesService.scopedClient(request); + const spacesClient = getSpacesService().createSpacesClient(request); const space = request.body; const id = request.params.id; diff --git a/x-pack/plugins/spaces/server/routes/api/external/share_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/share_to_space.test.ts index 3af1d9d245d10..b376e56a87fd8 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/share_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/share_to_space.test.ts @@ -11,7 +11,7 @@ import { mockRouteContextWithInvalidLicense, createMockSavedObjectsService, } from '../__fixtures__'; -import { CoreSetup, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server'; +import { kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server'; import { loggingSystemMock, httpServiceMock, @@ -19,21 +19,16 @@ import { coreMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; -import { SpacesAuditLogger } from '../../../lib/audit_logger'; -import { SpacesClient } from '../../../lib/spaces_client'; import { initShareToSpacesApi } from './share_to_space'; import { spacesConfig } from '../../../lib/__fixtures__'; -import { securityMock } from '../../../../../security/server/mocks'; import { ObjectType } from '@kbn/config-schema'; -import { SecurityPluginSetup } from '../../../../../security/server'; +import { SpacesClientService } from '../../../spaces_client'; describe('share to space', () => { const spacesSavedObjects = createSpaces(); const spaces = spacesSavedObjects.map((s) => ({ id: s.id, ...s.attributes })); - const setup = async ({ - authorization = null, - }: { authorization?: SecurityPluginSetup['authz'] | null } = {}) => { + const setup = async () => { const httpService = httpServiceMock.createSetupContract(); const router = httpService.createRouter(); const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects); @@ -42,36 +37,28 @@ describe('share to space', () => { const { savedObjects, savedObjectsClient } = createMockSavedObjectsService(spaces); coreStart.savedObjects = savedObjects; - const service = new SpacesService(log); - const spacesService = await service.setup({ - http: (httpService as unknown) as CoreSetup['http'], - getStartServices: async () => [coreStart, {}, {}], - authorization, - auditLogger: {} as SpacesAuditLogger, - config$: Rx.of(spacesConfig), - }); + const clientService = new SpacesClientService(jest.fn()); + clientService + .setup({ config$: Rx.of(spacesConfig) }) + .setClientRepositoryFactory(() => savedObjectsRepositoryMock); - spacesService.scopedClient = jest.fn((req: any) => { - return Promise.resolve( - new SpacesClient( - null as any, - () => null, - null, - savedObjectsRepositoryMock, - spacesConfig, - savedObjectsRepositoryMock, - req - ) - ); + const service = new SpacesService(); + service.setup({ + basePath: httpService.basePath, }); + const clientServiceStart = clientService.start(coreStart); + + const spacesServiceStart = service.start({ + basePath: coreStart.http.basePath, + spacesClientService: clientServiceStart, + }); initShareToSpacesApi({ externalRouter: router, getStartServices: async () => [coreStart, {}, {}], getImportExportObjectLimit: () => 1000, log, - spacesService, - authorization, + getSpacesService: () => spacesServiceStart, }); const [ @@ -79,8 +66,6 @@ describe('share to space', () => { [shareRemove, resolveRouteHandler], ] = router.post.mock.calls; - const [[, permissionsRouteHandler]] = router.get.mock.calls; - return { coreStart, savedObjectsClient, @@ -92,76 +77,10 @@ describe('share to space', () => { routeValidation: shareRemove.validate as RouteValidatorConfig<{}, {}, {}>, routeHandler: resolveRouteHandler, }, - sharePermissions: { - routeHandler: permissionsRouteHandler, - }, savedObjectsRepositoryMock, }; }; - describe('GET /internal/spaces/_share_saved_object_permissions', () => { - it('returns true when security is not enabled', async () => { - const { sharePermissions } = await setup(); - - const request = httpServerMock.createKibanaRequest({ query: { type: 'foo' }, method: 'get' }); - const response = await sharePermissions.routeHandler( - mockRouteContext, - request, - kibanaResponseFactory - ); - - const { status, payload } = response; - expect(status).toEqual(200); - expect(payload).toEqual({ shareToAllSpaces: true }); - }); - - it('returns false when the user is not authorized globally', async () => { - const authorization = securityMock.createSetup().authz; - const globalPrivilegesCheck = jest.fn().mockResolvedValue({ hasAllRequested: false }); - authorization.checkPrivilegesWithRequest.mockReturnValue({ - globally: globalPrivilegesCheck, - }); - const { sharePermissions } = await setup({ authorization }); - - const request = httpServerMock.createKibanaRequest({ query: { type: 'foo' }, method: 'get' }); - const response = await sharePermissions.routeHandler( - mockRouteContext, - request, - kibanaResponseFactory - ); - - const { status, payload } = response; - expect(status).toEqual(200); - expect(payload).toEqual({ shareToAllSpaces: false }); - - expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledTimes(1); - expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - }); - - it('returns true when the user is authorized globally', async () => { - const authorization = securityMock.createSetup().authz; - const globalPrivilegesCheck = jest.fn().mockResolvedValue({ hasAllRequested: true }); - authorization.checkPrivilegesWithRequest.mockReturnValue({ - globally: globalPrivilegesCheck, - }); - const { sharePermissions } = await setup({ authorization }); - - const request = httpServerMock.createKibanaRequest({ query: { type: 'foo' }, method: 'get' }); - const response = await sharePermissions.routeHandler( - mockRouteContext, - request, - kibanaResponseFactory - ); - - const { status, payload } = response; - expect(status).toEqual(200); - expect(payload).toEqual({ shareToAllSpaces: true }); - - expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledTimes(1); - expect(authorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - }); - }); - describe('POST /api/spaces/_share_saved_object_add', () => { const object = { id: 'foo', type: 'bar' }; diff --git a/x-pack/plugins/spaces/server/routes/api/external/share_to_space.ts b/x-pack/plugins/spaces/server/routes/api/external/share_to_space.ts index 7acf9e3e6e3d0..adb4708d52ab0 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/share_to_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/share_to_space.ts @@ -13,7 +13,7 @@ import { createLicensedRouteHandler } from '../../lib'; const uniq = (arr: T[]): T[] => Array.from(new Set(arr)); export function initShareToSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, getStartServices, authorization } = deps; + const { externalRouter, getStartServices } = deps; const shareSchema = schema.object({ spaces: schema.arrayOf( @@ -37,31 +37,6 @@ export function initShareToSpacesApi(deps: ExternalRouteDeps) { object: schema.object({ type: schema.string(), id: schema.string() }), }); - externalRouter.get( - { - path: '/internal/spaces/_share_saved_object_permissions', - validate: { query: schema.object({ type: schema.string() }) }, - }, - createLicensedRouteHandler(async (_context, request, response) => { - let shareToAllSpaces = true; - const { type } = request.query; - - if (authorization) { - try { - const checkPrivileges = authorization.checkPrivilegesWithRequest(request); - shareToAllSpaces = ( - await checkPrivileges.globally({ - kibana: authorization.actions.savedObject.get(type, 'share_to_space'), - }) - ).hasAllRequested; - } catch (error) { - return response.customError(wrapError(error)); - } - } - return response.ok({ body: { shareToAllSpaces } }); - }) - ); - externalRouter.post( { path: '/api/spaces/_share_saved_object_add', validate: { body: shareSchema } }, createLicensedRouteHandler(async (_context, request, response) => { diff --git a/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts b/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts index 086d5f5bc94bb..4f1d8fa912572 100644 --- a/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts @@ -3,14 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import * as Rx from 'rxjs'; import { mockRouteContextWithInvalidLicense } from '../__fixtures__'; -import { CoreSetup, kibanaResponseFactory } from 'src/core/server'; +import { kibanaResponseFactory } from 'src/core/server'; import { httpServiceMock, httpServerMock, coreMock } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; -import { SpacesAuditLogger } from '../../../lib/audit_logger'; -import { spacesConfig } from '../../../lib/__fixtures__'; import { initGetActiveSpaceApi } from './get_active_space'; +import { spacesClientServiceMock } from '../../../spaces_client/spaces_client_service.mock'; describe('GET /internal/spaces/_active_space', () => { const setup = async () => { @@ -19,18 +17,18 @@ describe('GET /internal/spaces/_active_space', () => { const coreStart = coreMock.createStart(); - const service = new SpacesService(null as any); - const spacesService = await service.setup({ - http: (httpService as unknown) as CoreSetup['http'], - getStartServices: async () => [coreStart, {}, {}], - authorization: null, - auditLogger: {} as SpacesAuditLogger, - config$: Rx.of(spacesConfig), + const service = new SpacesService(); + service.setup({ + basePath: httpService.basePath, }); initGetActiveSpaceApi({ internalRouter: router, - spacesService, + getSpacesService: () => + service.start({ + basePath: coreStart.http.basePath, + spacesClientService: spacesClientServiceMock.createStart(), + }), }); return { diff --git a/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.ts b/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.ts index fa9dafa526da8..9a73704e2ea77 100644 --- a/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.ts @@ -9,7 +9,7 @@ import { InternalRouteDeps } from '.'; import { createLicensedRouteHandler } from '../../lib'; export function initGetActiveSpaceApi(deps: InternalRouteDeps) { - const { internalRouter, spacesService } = deps; + const { internalRouter, getSpacesService } = deps; internalRouter.get( { @@ -18,7 +18,7 @@ export function initGetActiveSpaceApi(deps: InternalRouteDeps) { }, createLicensedRouteHandler(async (context, request, response) => { try { - const space = await spacesService.getActiveSpace(request); + const space = await getSpacesService().getActiveSpace(request); return response.ok({ body: space }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/plugins/spaces/server/routes/api/internal/index.ts b/x-pack/plugins/spaces/server/routes/api/internal/index.ts index 12ce50f228bfc..675cdb548543d 100644 --- a/x-pack/plugins/spaces/server/routes/api/internal/index.ts +++ b/x-pack/plugins/spaces/server/routes/api/internal/index.ts @@ -5,12 +5,12 @@ */ import { IRouter } from 'src/core/server'; -import { SpacesServiceSetup } from '../../../spaces_service/spaces_service'; +import { SpacesServiceStart } from '../../../spaces_service/spaces_service'; import { initGetActiveSpaceApi } from './get_active_space'; export interface InternalRouteDeps { internalRouter: IRouter; - spacesService: SpacesServiceSetup; + getSpacesService: () => SpacesServiceStart; } export function initInternalSpacesApi(deps: InternalRouteDeps) { diff --git a/x-pack/plugins/spaces/server/saved_objects/saved_objects_client_wrapper_factory.ts b/x-pack/plugins/spaces/server/saved_objects/saved_objects_client_wrapper_factory.ts index e545cccfeadd7..7e19deae0092e 100644 --- a/x-pack/plugins/spaces/server/saved_objects/saved_objects_client_wrapper_factory.ts +++ b/x-pack/plugins/spaces/server/saved_objects/saved_objects_client_wrapper_factory.ts @@ -9,16 +9,16 @@ import { SavedObjectsClientWrapperOptions, } from 'src/core/server'; import { SpacesSavedObjectsClient } from './spaces_saved_objects_client'; -import { SpacesServiceSetup } from '../spaces_service/spaces_service'; +import { SpacesServiceStart } from '../spaces_service/spaces_service'; export function spacesSavedObjectsClientWrapperFactory( - spacesService: SpacesServiceSetup + getSpacesService: () => SpacesServiceStart ): SavedObjectsClientWrapperFactory { return (options: SavedObjectsClientWrapperOptions) => new SpacesSavedObjectsClient({ baseClient: options.client, request: options.request, - spacesService, + getSpacesService, typeRegistry: options.typeRegistry, }); } diff --git a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.test.ts b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.test.ts index 31f2c98d74c96..a0b0ab41e9d89 100644 --- a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.test.ts @@ -12,10 +12,10 @@ describe('SpacesSavedObjectsService', () => { describe('#setup', () => { it('registers the "space" saved object type with appropriate mappings and migrations', () => { const core = coreMock.createSetup(); - const spacesService = spacesServiceMock.createSetupContract(); + const spacesService = spacesServiceMock.createStartContract(); const service = new SpacesSavedObjectsService(); - service.setup({ core, spacesService }); + service.setup({ core, getSpacesService: () => spacesService }); expect(core.savedObjects.registerType).toHaveBeenCalledTimes(1); expect(core.savedObjects.registerType.mock.calls[0]).toMatchInlineSnapshot(` @@ -66,10 +66,10 @@ describe('SpacesSavedObjectsService', () => { it('registers the client wrapper', () => { const core = coreMock.createSetup(); - const spacesService = spacesServiceMock.createSetupContract(); + const spacesService = spacesServiceMock.createStartContract(); const service = new SpacesSavedObjectsService(); - service.setup({ core, spacesService }); + service.setup({ core, getSpacesService: () => spacesService }); expect(core.savedObjects.addClientWrapper).toHaveBeenCalledTimes(1); expect(core.savedObjects.addClientWrapper).toHaveBeenCalledWith( diff --git a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts index 58aa1fe08558a..b52f1eda1b6ac 100644 --- a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts +++ b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts @@ -8,15 +8,15 @@ import { CoreSetup } from 'src/core/server'; import { SpacesSavedObjectMappings } from './mappings'; import { migrateToKibana660 } from './migrations'; import { spacesSavedObjectsClientWrapperFactory } from './saved_objects_client_wrapper_factory'; -import { SpacesServiceSetup } from '../spaces_service'; +import { SpacesServiceStart } from '../spaces_service'; interface SetupDeps { core: Pick; - spacesService: SpacesServiceSetup; + getSpacesService: () => SpacesServiceStart; } export class SpacesSavedObjectsService { - public setup({ core, spacesService }: SetupDeps) { + public setup({ core, getSpacesService }: SetupDeps) { core.savedObjects.registerType({ name: 'space', hidden: true, @@ -30,7 +30,7 @@ export class SpacesSavedObjectsService { core.savedObjects.addClientWrapper( Number.MIN_SAFE_INTEGER, 'spaces', - spacesSavedObjectsClientWrapperFactory(spacesService) + spacesSavedObjectsClientWrapperFactory(getSpacesService) ); } } 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 65413a5b5042f..88adf98248d2c 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 @@ -9,8 +9,8 @@ import { SpacesSavedObjectsClient } from './spaces_saved_objects_client'; import { spacesServiceMock } from '../spaces_service/spaces_service.mock'; import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { SavedObjectTypeRegistry } from 'src/core/server'; -import { SpacesClient } from '../lib/spaces_client'; -import { spacesClientMock } from '../lib/spaces_client/spaces_client.mock'; +import { SpacesClient } from '../spaces_client'; +import { spacesClientMock } from '../spaces_client/spaces_client.mock'; import Boom from '@hapi/boom'; const typeRegistry = new SavedObjectTypeRegistry(); @@ -39,8 +39,8 @@ const createMockRequest = () => ({}); const createMockClient = () => savedObjectsClientMock.create(); -const createSpacesService = async (spaceId: string) => { - return spacesServiceMock.createSetupContract(spaceId); +const createSpacesService = (spaceId: string) => { + return spacesServiceMock.createStartContract(spaceId); }; const createMockResponse = () => ({ @@ -61,15 +61,15 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; { id: 'space_1', expectedNamespace: 'space_1' }, ].forEach((currentSpace) => { describe(`${currentSpace.id} space`, () => { - const createSpacesSavedObjectsClient = async () => { + const createSpacesSavedObjectsClient = () => { const request = createMockRequest(); const baseClient = createMockClient(); - const spacesService = await createSpacesService(currentSpace.id); + const spacesService = createSpacesService(currentSpace.id); const client = new SpacesSavedObjectsClient({ request, baseClient, - spacesService, + getSpacesService: () => spacesService, typeRegistry, }); return { client, baseClient, spacesService }; @@ -77,7 +77,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; describe('#get', () => { test(`throws error if options.namespace is specified`, async () => { - const { client } = await createSpacesSavedObjectsClient(); + const { client } = createSpacesSavedObjectsClient(); await expect(client.get('foo', '', { namespace: 'bar' })).rejects.toThrow( ERROR_NAMESPACE_SPECIFIED @@ -85,7 +85,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); test(`supplements options with the current namespace`, async () => { - const { client, baseClient } = await createSpacesSavedObjectsClient(); + const { client, baseClient } = createSpacesSavedObjectsClient(); const expectedReturnValue = createMockResponse(); baseClient.get.mockReturnValue(Promise.resolve(expectedReturnValue)); @@ -105,7 +105,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; describe('#bulkGet', () => { test(`throws error if options.namespace is specified`, async () => { - const { client } = await createSpacesSavedObjectsClient(); + const { client } = createSpacesSavedObjectsClient(); await expect( client.bulkGet([{ id: '', type: 'foo' }], { namespace: 'bar' }) @@ -113,7 +113,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); test(`supplements options with the current namespace`, async () => { - const { client, baseClient } = await createSpacesSavedObjectsClient(); + const { client, baseClient } = createSpacesSavedObjectsClient(); const expectedReturnValue = { saved_objects: [createMockResponse()] }; baseClient.bulkGet.mockReturnValue(Promise.resolve(expectedReturnValue)); @@ -134,10 +134,10 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; const EMPTY_RESPONSE = { saved_objects: [], total: 0, per_page: 20, page: 1 }; test(`returns empty result if user is unauthorized in this space`, async () => { - const { client, baseClient, spacesService } = await createSpacesSavedObjectsClient(); + const { client, baseClient, spacesService } = createSpacesSavedObjectsClient(); const spacesClient = spacesClientMock.create(); spacesClient.getAll.mockResolvedValue([]); - spacesService.scopedClient.mockResolvedValue(spacesClient); + spacesService.createSpacesClient.mockReturnValue(spacesClient); const options = Object.freeze({ type: 'foo', namespaces: ['some-ns'] }); const actualReturnValue = await client.find(options); @@ -147,10 +147,10 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); test(`returns empty result if user is unauthorized in any space`, async () => { - const { client, baseClient, spacesService } = await createSpacesSavedObjectsClient(); + const { client, baseClient, spacesService } = createSpacesSavedObjectsClient(); const spacesClient = spacesClientMock.create(); spacesClient.getAll.mockRejectedValue(Boom.unauthorized()); - spacesService.scopedClient.mockResolvedValue(spacesClient); + spacesService.createSpacesClient.mockReturnValue(spacesClient); const options = Object.freeze({ type: 'foo', namespaces: ['some-ns'] }); const actualReturnValue = await client.find(options); @@ -160,7 +160,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); test(`passes options.type to baseClient if valid singular type specified`, async () => { - const { client, baseClient } = await createSpacesSavedObjectsClient(); + const { client, baseClient } = createSpacesSavedObjectsClient(); const expectedReturnValue = { saved_objects: [createMockResponse()].map((obj) => ({ ...obj, score: 1 })), total: 1, @@ -180,7 +180,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); test(`supplements options with the current namespace`, async () => { - const { client, baseClient } = await createSpacesSavedObjectsClient(); + const { client, baseClient } = createSpacesSavedObjectsClient(); const expectedReturnValue = { saved_objects: [createMockResponse()].map((obj) => ({ ...obj, score: 1 })), total: 1, @@ -200,7 +200,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); test(`passes options.namespaces along`, async () => { - const { client, baseClient, spacesService } = await createSpacesSavedObjectsClient(); + const { client, baseClient, spacesService } = createSpacesSavedObjectsClient(); const expectedReturnValue = { saved_objects: [createMockResponse()], total: 1, @@ -209,7 +209,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }; baseClient.find.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesClient = (await spacesService.scopedClient(null as any)) as jest.Mocked< + const spacesClient = spacesService.createSpacesClient(null as any) as jest.Mocked< SpacesClient >; spacesClient.getAll.mockImplementation(() => @@ -231,7 +231,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); test(`filters options.namespaces based on authorization`, async () => { - const { client, baseClient, spacesService } = await createSpacesSavedObjectsClient(); + const { client, baseClient, spacesService } = createSpacesSavedObjectsClient(); const expectedReturnValue = { saved_objects: [createMockResponse()], total: 1, @@ -240,7 +240,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }; baseClient.find.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesClient = (await spacesService.scopedClient(null as any)) as jest.Mocked< + const spacesClient = spacesService.createSpacesClient(null as any) as jest.Mocked< SpacesClient >; spacesClient.getAll.mockImplementation(() => @@ -262,7 +262,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); test(`translates options.namespace: ['*']`, async () => { - const { client, baseClient, spacesService } = await createSpacesSavedObjectsClient(); + const { client, baseClient, spacesService } = createSpacesSavedObjectsClient(); const expectedReturnValue = { saved_objects: [createMockResponse()], total: 1, @@ -271,7 +271,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }; baseClient.find.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesClient = (await spacesService.scopedClient(null as any)) as jest.Mocked< + const spacesClient = spacesService.createSpacesClient(null as any) as jest.Mocked< SpacesClient >; spacesClient.getAll.mockImplementation(() => @@ -295,7 +295,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; describe('#checkConflicts', () => { test(`throws error if options.namespace is specified`, async () => { - const { client } = await createSpacesSavedObjectsClient(); + const { client } = createSpacesSavedObjectsClient(); await expect( // @ts-expect-error @@ -304,7 +304,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); test(`supplements options with the current namespace`, async () => { - const { client, baseClient } = await createSpacesSavedObjectsClient(); + const { client, baseClient } = createSpacesSavedObjectsClient(); const expectedReturnValue = { errors: [] }; baseClient.checkConflicts.mockReturnValue(Promise.resolve(expectedReturnValue)); @@ -323,7 +323,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; describe('#create', () => { test(`throws error if options.namespace is specified`, async () => { - const { client } = await createSpacesSavedObjectsClient(); + const { client } = createSpacesSavedObjectsClient(); await expect(client.create('foo', {}, { namespace: 'bar' })).rejects.toThrow( ERROR_NAMESPACE_SPECIFIED @@ -331,7 +331,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); test(`supplements options with the current namespace`, async () => { - const { client, baseClient } = await createSpacesSavedObjectsClient(); + const { client, baseClient } = createSpacesSavedObjectsClient(); const expectedReturnValue = createMockResponse(); baseClient.create.mockReturnValue(Promise.resolve(expectedReturnValue)); @@ -351,7 +351,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; describe('#bulkCreate', () => { test(`throws error if options.namespace is specified`, async () => { - const { client } = await createSpacesSavedObjectsClient(); + const { client } = createSpacesSavedObjectsClient(); await expect( client.bulkCreate([{ id: '', type: 'foo', attributes: {} }], { namespace: 'bar' }) @@ -359,7 +359,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); test(`supplements options with the current namespace`, async () => { - const { client, baseClient } = await createSpacesSavedObjectsClient(); + const { client, baseClient } = createSpacesSavedObjectsClient(); const expectedReturnValue = { saved_objects: [createMockResponse()] }; baseClient.bulkCreate.mockReturnValue(Promise.resolve(expectedReturnValue)); @@ -378,7 +378,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; describe('#update', () => { test(`throws error if options.namespace is specified`, async () => { - const { client } = await createSpacesSavedObjectsClient(); + const { client } = createSpacesSavedObjectsClient(); await expect( // @ts-expect-error @@ -387,7 +387,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); test(`supplements options with the current namespace`, async () => { - const { client, baseClient } = await createSpacesSavedObjectsClient(); + const { client, baseClient } = createSpacesSavedObjectsClient(); const expectedReturnValue = createMockResponse(); baseClient.update.mockReturnValue(Promise.resolve(expectedReturnValue)); @@ -408,7 +408,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; describe('#bulkUpdate', () => { test(`throws error if options.namespace is specified`, async () => { - const { client } = await createSpacesSavedObjectsClient(); + const { client } = createSpacesSavedObjectsClient(); await expect( // @ts-expect-error @@ -417,7 +417,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); test(`supplements options with the current namespace`, async () => { - const { client, baseClient } = await createSpacesSavedObjectsClient(); + const { client, baseClient } = createSpacesSavedObjectsClient(); const expectedReturnValue = { saved_objects: [createMockResponse()] }; baseClient.bulkUpdate.mockReturnValue(Promise.resolve(expectedReturnValue)); @@ -442,7 +442,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; describe('#delete', () => { test(`throws error if options.namespace is specified`, async () => { - const { client } = await createSpacesSavedObjectsClient(); + const { client } = createSpacesSavedObjectsClient(); await expect( // @ts-expect-error @@ -451,7 +451,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); test(`supplements options with the current namespace`, async () => { - const { client, baseClient } = await createSpacesSavedObjectsClient(); + const { client, baseClient } = createSpacesSavedObjectsClient(); const expectedReturnValue = createMockResponse(); baseClient.delete.mockReturnValue(Promise.resolve(expectedReturnValue)); @@ -471,7 +471,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; describe('#addToNamespaces', () => { test(`throws error if options.namespace is specified`, async () => { - const { client } = await createSpacesSavedObjectsClient(); + const { client } = createSpacesSavedObjectsClient(); await expect( // @ts-expect-error @@ -480,7 +480,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); test(`supplements options with the current namespace`, async () => { - const { client, baseClient } = await createSpacesSavedObjectsClient(); + const { client, baseClient } = createSpacesSavedObjectsClient(); const expectedReturnValue = { namespaces: ['foo', 'bar'] }; baseClient.addToNamespaces.mockReturnValue(Promise.resolve(expectedReturnValue)); @@ -501,7 +501,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; describe('#deleteFromNamespaces', () => { test(`throws error if options.namespace is specified`, async () => { - const { client } = await createSpacesSavedObjectsClient(); + const { client } = createSpacesSavedObjectsClient(); await expect( // @ts-expect-error @@ -510,7 +510,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); test(`supplements options with the current namespace`, async () => { - const { client, baseClient } = await createSpacesSavedObjectsClient(); + const { client, baseClient } = createSpacesSavedObjectsClient(); const expectedReturnValue = { namespaces: ['foo', 'bar'] }; baseClient.deleteFromNamespaces.mockReturnValue(Promise.resolve(expectedReturnValue)); @@ -531,7 +531,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; describe('#removeReferencesTo', () => { test(`throws error if options.namespace is specified`, async () => { - const { client } = await createSpacesSavedObjectsClient(); + const { client } = createSpacesSavedObjectsClient(); await expect( // @ts-expect-error @@ -540,7 +540,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); test(`supplements options with the current namespace`, async () => { - const { client, baseClient } = await createSpacesSavedObjectsClient(); + const { client, baseClient } = createSpacesSavedObjectsClient(); const expectedReturnValue = { updated: 12 }; baseClient.removeReferencesTo.mockReturnValue(Promise.resolve(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 183aea26edab7..049bd88085ed5 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 @@ -22,14 +22,14 @@ import { ISavedObjectTypeRegistry, } from '../../../../../src/core/server'; import { ALL_SPACES_ID } from '../../common/constants'; -import { SpacesServiceSetup } from '../spaces_service/spaces_service'; +import { SpacesServiceStart } from '../spaces_service/spaces_service'; import { spaceIdToNamespace } from '../lib/utils/namespace'; -import { SpacesClient } from '../lib/spaces_client'; +import { ISpacesClient } from '../spaces_client'; interface SpacesSavedObjectsClientOptions { baseClient: SavedObjectsClientContract; request: any; - spacesService: SpacesServiceSetup; + getSpacesService: () => SpacesServiceStart; typeRegistry: ISavedObjectTypeRegistry; } @@ -51,14 +51,16 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { private readonly client: SavedObjectsClientContract; private readonly spaceId: string; private readonly types: string[]; - private readonly getSpacesClient: Promise; + private readonly spacesClient: ISpacesClient; public readonly errors: SavedObjectsClientContract['errors']; constructor(options: SpacesSavedObjectsClientOptions) { - const { baseClient, request, spacesService, typeRegistry } = options; + const { baseClient, request, getSpacesService, typeRegistry } = options; + + const spacesService = getSpacesService(); this.client = baseClient; - this.getSpacesClient = spacesService.scopedClient(request); + this.spacesClient = spacesService.createSpacesClient(request); this.spaceId = spacesService.getSpaceId(request); this.types = typeRegistry.getAllTypes().map((t) => t.name); this.errors = baseClient.errors; @@ -167,10 +169,8 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { let namespaces = options.namespaces; if (namespaces) { - const spacesClient = await this.getSpacesClient; - try { - const availableSpaces = await spacesClient.getAll({ purpose: 'findSavedObjects' }); + const availableSpaces = await this.spacesClient.getAll({ purpose: 'findSavedObjects' }); if (namespaces.includes(ALL_SPACES_ID)) { namespaces = availableSpaces.map((space) => space.id); } else { diff --git a/x-pack/plugins/spaces/server/spaces_client/index.ts b/x-pack/plugins/spaces/server/spaces_client/index.ts new file mode 100644 index 0000000000000..05c9dbd3fdb95 --- /dev/null +++ b/x-pack/plugins/spaces/server/spaces_client/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 { SpacesClient, ISpacesClient } from './spaces_client'; +export { + SpacesClientService, + SpacesClientServiceSetup, + SpacesClientServiceStart, + SpacesClientRepositoryFactory, + SpacesClientWrapper, +} from './spaces_client_service'; diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.mock.ts b/x-pack/plugins/spaces/server/spaces_client/spaces_client.mock.ts similarity index 90% rename from x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.mock.ts rename to x-pack/plugins/spaces/server/spaces_client/spaces_client.mock.ts index e38842b8799ac..8383d32cc6517 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.mock.ts +++ b/x-pack/plugins/spaces/server/spaces_client/spaces_client.mock.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_SPACE_ID } from '../../../common/constants'; -import { Space } from '../../../common/model/space'; +import { DEFAULT_SPACE_ID } from '../../common/constants'; +import { Space } from '../../common/model/space'; import { SpacesClient } from './spaces_client'; const createSpacesClientMock = () => diff --git a/x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts b/x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts new file mode 100644 index 0000000000000..7c2f90f5dfb2c --- /dev/null +++ b/x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts @@ -0,0 +1,341 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { SpacesClient } from './spaces_client'; +import { ConfigType, ConfigSchema } from '../config'; +import { GetAllSpacesPurpose } from '../../common/model/types'; +import { savedObjectsRepositoryMock } from '../../../../../src/core/server/mocks'; + +const createMockDebugLogger = () => { + return jest.fn(); +}; + +const createMockConfig = (mockConfig: ConfigType = { maxSpaces: 1000, enabled: true }) => { + return ConfigSchema.validate(mockConfig); +}; + +describe('#getAll', () => { + const savedObjects = [ + { + id: 'foo', + attributes: { + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + }, + }, + { + id: 'bar', + attributes: { + name: 'bar-name', + description: 'bar-description', + bar: 'bar-bar', + }, + }, + { + id: 'baz', + attributes: { + name: 'baz-name', + description: 'baz-description', + bar: 'baz-bar', + }, + }, + ]; + + const expectedSpaces = [ + { + id: 'foo', + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + }, + { + id: 'bar', + name: 'bar-name', + description: 'bar-description', + bar: 'bar-bar', + }, + { + id: 'baz', + name: 'baz-name', + description: 'baz-description', + bar: 'baz-bar', + }, + ]; + + test(`finds spaces using callWithRequestRepository`, async () => { + const mockDebugLogger = createMockDebugLogger(); + const mockCallWithRequestRepository = savedObjectsRepositoryMock.create(); + mockCallWithRequestRepository.find.mockResolvedValue({ + saved_objects: savedObjects, + } as any); + const mockConfig = createMockConfig({ + maxSpaces: 1234, + enabled: true, + }); + + const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository); + const actualSpaces = await client.getAll(); + + expect(actualSpaces).toEqual(expectedSpaces); + expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({ + type: 'space', + page: 1, + perPage: mockConfig.maxSpaces, + sortField: 'name.keyword', + }); + }); + + test(`throws Boom.badRequest when an invalid purpose is provided'`, async () => { + const client = new SpacesClient(null as any, null as any, null as any); + await expect( + client.getAll({ purpose: 'invalid_purpose' as GetAllSpacesPurpose }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"unsupported space purpose: invalid_purpose"`); + }); +}); + +describe('#get', () => { + const savedObject = { + id: 'foo', + type: 'foo', + references: [], + attributes: { + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + }, + }; + + const expectedSpace = { + id: 'foo', + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + }; + + test(`gets space using callWithRequestRepository`, async () => { + const mockDebugLogger = createMockDebugLogger(); + const mockConfig = createMockConfig(); + const mockCallWithRequestRepository = savedObjectsRepositoryMock.create(); + mockCallWithRequestRepository.get.mockResolvedValue(savedObject); + + const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository); + const id = savedObject.id; + const actualSpace = await client.get(id); + + expect(actualSpace).toEqual(expectedSpace); + expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); + }); +}); + +describe('#create', () => { + const id = 'foo'; + + const spaceToCreate = { + id, + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + _reserved: true, + disabledFeatures: [], + }; + + const attributes = { + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + disabledFeatures: [], + }; + + const savedObject = { + id, + type: 'foo', + references: [], + attributes: { + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + disabledFeatures: [], + }, + }; + + const expectedReturnedSpace = { + id, + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + disabledFeatures: [], + }; + + test(`creates space using callWithRequestRepository when we're under the max`, async () => { + const maxSpaces = 5; + const mockDebugLogger = createMockDebugLogger(); + const mockCallWithRequestRepository = savedObjectsRepositoryMock.create(); + mockCallWithRequestRepository.create.mockResolvedValue(savedObject); + mockCallWithRequestRepository.find.mockResolvedValue({ + total: maxSpaces - 1, + } as any); + + const mockConfig = createMockConfig({ + maxSpaces, + enabled: true, + }); + + const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository); + + const actualSpace = await client.create(spaceToCreate); + + expect(actualSpace).toEqual(expectedReturnedSpace); + expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({ + type: 'space', + page: 1, + perPage: 0, + }); + expect(mockCallWithRequestRepository.create).toHaveBeenCalledWith('space', attributes, { + id, + }); + }); + + test(`throws bad request when we are at the maximum number of spaces`, async () => { + const maxSpaces = 5; + const mockDebugLogger = createMockDebugLogger(); + const mockCallWithRequestRepository = savedObjectsRepositoryMock.create(); + mockCallWithRequestRepository.create.mockResolvedValue(savedObject); + mockCallWithRequestRepository.find.mockResolvedValue({ + total: maxSpaces, + } as any); + + const mockConfig = createMockConfig({ + maxSpaces, + enabled: true, + }); + + const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository); + + expect(client.create(spaceToCreate)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unable to create Space, this exceeds the maximum number of spaces set by the xpack.spaces.maxSpaces setting"` + ); + + expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({ + type: 'space', + page: 1, + perPage: 0, + }); + expect(mockCallWithRequestRepository.create).not.toHaveBeenCalled(); + }); +}); + +describe('#update', () => { + const spaceToUpdate = { + id: 'foo', + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + _reserved: false, + disabledFeatures: [], + }; + + const attributes = { + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + disabledFeatures: [], + }; + + const savedObject = { + id: 'foo', + type: 'foo', + references: [], + attributes: { + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + _reserved: true, + disabledFeatures: [], + }, + }; + + const expectedReturnedSpace = { + id: 'foo', + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + _reserved: true, + disabledFeatures: [], + }; + + test(`updates space using callWithRequestRepository`, async () => { + const mockDebugLogger = createMockDebugLogger(); + const mockConfig = createMockConfig(); + const mockCallWithRequestRepository = savedObjectsRepositoryMock.create(); + mockCallWithRequestRepository.get.mockResolvedValue(savedObject); + + const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository); + const id = savedObject.id; + const actualSpace = await client.update(id, spaceToUpdate); + + expect(actualSpace).toEqual(expectedReturnedSpace); + expect(mockCallWithRequestRepository.update).toHaveBeenCalledWith('space', id, attributes); + expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); + }); +}); + +describe('#delete', () => { + const id = 'foo'; + + const reservedSavedObject = { + id, + type: 'foo', + references: [], + attributes: { + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + _reserved: true, + }, + }; + + const notReservedSavedObject = { + id, + type: 'foo', + references: [], + attributes: { + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + }, + }; + + test(`throws bad request when the space is reserved`, async () => { + const mockDebugLogger = createMockDebugLogger(); + const mockConfig = createMockConfig(); + const mockCallWithRequestRepository = savedObjectsRepositoryMock.create(); + mockCallWithRequestRepository.get.mockResolvedValue(reservedSavedObject); + + const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository); + + expect(client.delete(id)).rejects.toThrowErrorMatchingInlineSnapshot( + `"The foo space cannot be deleted because it is reserved."` + ); + + expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); + }); + + test(`deletes space using callWithRequestRepository when space isn't reserved`, async () => { + const mockDebugLogger = createMockDebugLogger(); + const mockConfig = createMockConfig(); + const mockCallWithRequestRepository = savedObjectsRepositoryMock.create(); + mockCallWithRequestRepository.get.mockResolvedValue(notReservedSavedObject); + + const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository); + + await client.delete(id); + + expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); + expect(mockCallWithRequestRepository.delete).toHaveBeenCalledWith('space', id); + expect(mockCallWithRequestRepository.deleteByNamespace).toHaveBeenCalledWith(id); + }); +}); diff --git a/x-pack/plugins/spaces/server/spaces_client/spaces_client.ts b/x-pack/plugins/spaces/server/spaces_client/spaces_client.ts new file mode 100644 index 0000000000000..7142ec8dc2fba --- /dev/null +++ b/x-pack/plugins/spaces/server/spaces_client/spaces_client.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 Boom from '@hapi/boom'; +import { omit } from 'lodash'; +import { ISavedObjectsRepository, SavedObject } from 'src/core/server'; +import { PublicMethodsOf } from '@kbn/utility-types'; +import { isReservedSpace } from '../../common'; +import { Space } from '../../common/model/space'; +import { ConfigType } from '../config'; +import { GetAllSpacesPurpose, GetSpaceResult } from '../../common/model/types'; + +export interface GetAllSpacesOptions { + purpose?: GetAllSpacesPurpose; + includeAuthorizedPurposes?: boolean; +} + +const SUPPORTED_GET_SPACE_PURPOSES: GetAllSpacesPurpose[] = [ + 'any', + 'copySavedObjectsIntoSpace', + 'findSavedObjects', + 'shareSavedObjectsIntoSpace', +]; +const DEFAULT_PURPOSE = 'any'; + +export type ISpacesClient = PublicMethodsOf; + +export class SpacesClient { + constructor( + private readonly debugLogger: (message: string) => void, + private readonly config: ConfigType, + private readonly repository: ISavedObjectsRepository + ) {} + + public async getAll(options: GetAllSpacesOptions = {}): Promise { + const { purpose = DEFAULT_PURPOSE } = options; + if (!SUPPORTED_GET_SPACE_PURPOSES.includes(purpose)) { + throw Boom.badRequest(`unsupported space purpose: ${purpose}`); + } + + this.debugLogger(`SpacesClient.getAll(). querying all spaces`); + + const { saved_objects: savedObjects } = await this.repository.find({ + type: 'space', + page: 1, + perPage: this.config.maxSpaces, + sortField: 'name.keyword', + }); + + this.debugLogger(`SpacesClient.getAll(). Found ${savedObjects.length} spaces.`); + + return savedObjects.map(this.transformSavedObjectToSpace); + } + + public async get(id: string) { + const savedObject = await this.repository.get('space', id); + return this.transformSavedObjectToSpace(savedObject); + } + + public async create(space: Space) { + const { total } = await this.repository.find({ + type: 'space', + page: 1, + perPage: 0, + }); + if (total >= this.config.maxSpaces) { + throw Boom.badRequest( + 'Unable to create Space, this exceeds the maximum number of spaces set by the xpack.spaces.maxSpaces setting' + ); + } + + this.debugLogger(`SpacesClient.create(), using RBAC. Attempting to create space`); + + const attributes = omit(space, ['id', '_reserved']); + const id = space.id; + const createdSavedObject = await this.repository.create('space', attributes, { id }); + + this.debugLogger(`SpacesClient.create(), created space object`); + + return this.transformSavedObjectToSpace(createdSavedObject); + } + + public async update(id: string, space: Space) { + const attributes = omit(space, 'id', '_reserved'); + await this.repository.update('space', id, attributes); + const updatedSavedObject = await this.repository.get('space', id); + return this.transformSavedObjectToSpace(updatedSavedObject); + } + + public async delete(id: string) { + const existingSavedObject = await this.repository.get('space', id); + if (isReservedSpace(this.transformSavedObjectToSpace(existingSavedObject))) { + throw Boom.badRequest(`The ${id} space cannot be deleted because it is reserved.`); + } + + await this.repository.deleteByNamespace(id); + + await this.repository.delete('space', id); + } + + private transformSavedObjectToSpace(savedObject: SavedObject) { + return { + id: savedObject.id, + ...savedObject.attributes, + } as Space; + } +} diff --git a/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.mock.ts b/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.mock.ts new file mode 100644 index 0000000000000..d80fadd7652c2 --- /dev/null +++ b/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.mock.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { spacesClientMock } from '../mocks'; + +import { SpacesClientServiceSetup, SpacesClientServiceStart } from './spaces_client_service'; + +const createSpacesClientServiceSetupMock = () => + ({ + registerClientWrapper: jest.fn(), + setClientRepositoryFactory: jest.fn(), + } as jest.Mocked); + +const createSpacesClientServiceStartMock = () => + ({ + createSpacesClient: jest.fn().mockReturnValue(spacesClientMock.create()), + } as jest.Mocked); + +export const spacesClientServiceMock = { + createSetup: createSpacesClientServiceSetupMock, + createStart: createSpacesClientServiceStartMock, +}; diff --git a/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.test.ts b/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.test.ts new file mode 100644 index 0000000000000..77733a4d7d472 --- /dev/null +++ b/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.test.ts @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 Rx from 'rxjs'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import { ConfigType } from '../config'; +import { spacesConfig } from '../lib/__fixtures__'; +import { ISpacesClient, SpacesClient } from './spaces_client'; +import { SpacesClientService } from './spaces_client_service'; + +const debugLogger = jest.fn(); + +describe('SpacesClientService', () => { + describe('#setup', () => { + it('allows a single repository factory to be set', () => { + const service = new SpacesClientService(debugLogger); + const setup = service.setup({ config$: Rx.of(spacesConfig) }); + + const repositoryFactory = jest.fn(); + setup.setClientRepositoryFactory(repositoryFactory); + + expect(() => + setup.setClientRepositoryFactory(repositoryFactory) + ).toThrowErrorMatchingInlineSnapshot(`"Repository factory has already been set"`); + }); + + it('allows a single client wrapper to be set', () => { + const service = new SpacesClientService(debugLogger); + const setup = service.setup({ config$: Rx.of(spacesConfig) }); + + const clientWrapper = jest.fn(); + setup.registerClientWrapper(clientWrapper); + + expect(() => setup.registerClientWrapper(clientWrapper)).toThrowErrorMatchingInlineSnapshot( + `"Client wrapper has already been set"` + ); + }); + }); + + describe('#start', () => { + it('throws if config is not available', () => { + const service = new SpacesClientService(debugLogger); + service.setup({ config$: new Rx.Observable() }); + const coreStart = coreMock.createStart(); + const start = service.start(coreStart); + + const request = httpServerMock.createKibanaRequest(); + + expect(() => start.createSpacesClient(request)).toThrowErrorMatchingInlineSnapshot( + `"Initialization error: spaces config is not available"` + ); + }); + + describe('without a custom repository factory or wrapper', () => { + it('returns an instance of the spaces client using the scoped repository', () => { + const service = new SpacesClientService(debugLogger); + service.setup({ config$: Rx.of(spacesConfig) }); + + const coreStart = coreMock.createStart(); + const start = service.start(coreStart); + + const request = httpServerMock.createKibanaRequest(); + const client = start.createSpacesClient(request); + expect(client).toBeInstanceOf(SpacesClient); + + expect(coreStart.savedObjects.createScopedRepository).toHaveBeenCalledWith(request, [ + 'space', + ]); + expect(coreStart.savedObjects.createInternalRepository).not.toHaveBeenCalled(); + }); + }); + + it('uses the custom repository factory when set', () => { + const service = new SpacesClientService(debugLogger); + const setup = service.setup({ config$: Rx.of(spacesConfig) }); + + const customRepositoryFactory = jest.fn(); + setup.setClientRepositoryFactory(customRepositoryFactory); + + const coreStart = coreMock.createStart(); + const start = service.start(coreStart); + + const request = httpServerMock.createKibanaRequest(); + const client = start.createSpacesClient(request); + expect(client).toBeInstanceOf(SpacesClient); + + expect(coreStart.savedObjects.createScopedRepository).not.toHaveBeenCalled(); + expect(coreStart.savedObjects.createInternalRepository).not.toHaveBeenCalled(); + + expect(customRepositoryFactory).toHaveBeenCalledWith(request, coreStart.savedObjects); + }); + + it('wraps the client in the wrapper when registered', () => { + const service = new SpacesClientService(debugLogger); + const setup = service.setup({ config$: Rx.of(spacesConfig) }); + + const wrapper = (Symbol() as unknown) as ISpacesClient; + + const clientWrapper = jest.fn().mockReturnValue(wrapper); + setup.registerClientWrapper(clientWrapper); + + const coreStart = coreMock.createStart(); + const start = service.start(coreStart); + + const request = httpServerMock.createKibanaRequest(); + const client = start.createSpacesClient(request); + + expect(client).toBe(wrapper); + expect(clientWrapper).toHaveBeenCalledTimes(1); + expect(clientWrapper).toHaveBeenCalledWith(request, expect.any(SpacesClient)); + + expect(coreStart.savedObjects.createScopedRepository).toHaveBeenCalledWith(request, [ + 'space', + ]); + expect(coreStart.savedObjects.createInternalRepository).not.toHaveBeenCalled(); + }); + + it('wraps the client in the wrapper when registered, using the custom repository factory when configured', () => { + const service = new SpacesClientService(debugLogger); + const setup = service.setup({ config$: Rx.of(spacesConfig) }); + + const customRepositoryFactory = jest.fn(); + setup.setClientRepositoryFactory(customRepositoryFactory); + + const wrapper = (Symbol() as unknown) as ISpacesClient; + + const clientWrapper = jest.fn().mockReturnValue(wrapper); + setup.registerClientWrapper(clientWrapper); + + const coreStart = coreMock.createStart(); + const start = service.start(coreStart); + + const request = httpServerMock.createKibanaRequest(); + const client = start.createSpacesClient(request); + + expect(client).toBe(wrapper); + expect(clientWrapper).toHaveBeenCalledTimes(1); + expect(clientWrapper).toHaveBeenCalledWith(request, expect.any(SpacesClient)); + + expect(coreStart.savedObjects.createScopedRepository).not.toHaveBeenCalled(); + expect(coreStart.savedObjects.createInternalRepository).not.toHaveBeenCalled(); + + expect(customRepositoryFactory).toHaveBeenCalledWith(request, coreStart.savedObjects); + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.ts b/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.ts new file mode 100644 index 0000000000000..d2a25c28cf192 --- /dev/null +++ b/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { Observable } from 'rxjs'; +import { + KibanaRequest, + CoreStart, + ISavedObjectsRepository, + SavedObjectsServiceStart, +} from 'src/core/server'; +import { ConfigType } from '../config'; +import { SpacesClient, ISpacesClient } from './spaces_client'; + +export type SpacesClientWrapper = ( + request: KibanaRequest, + baseClient: ISpacesClient +) => ISpacesClient; + +export type SpacesClientRepositoryFactory = ( + request: KibanaRequest, + savedObjectsStart: SavedObjectsServiceStart +) => ISavedObjectsRepository; + +export interface SpacesClientServiceSetup { + /** + * Sets the factory that should be used to create the Saved Objects Repository + * whenever a new instance of the SpacesClient is created. By default, a repository + * scoped to the current user will be created. + */ + setClientRepositoryFactory: (factory: SpacesClientRepositoryFactory) => void; + + /** + * Sets the client wrapper that should be used to optionally "wrap" each instance of the SpacesClient. + * By default, an unwrapped client will be created. + * + * Unlike the SavedObjectsClientWrappers, this service only supports a single wrapper. It is not possible + * to register multiple wrappers at this time. + */ + registerClientWrapper: (wrapper: SpacesClientWrapper) => void; +} + +export interface SpacesClientServiceStart { + /** + * Creates an instance of the SpacesClient scoped to the provided request. + */ + createSpacesClient: (request: KibanaRequest) => ISpacesClient; +} + +interface SetupDeps { + config$: Observable; +} + +export class SpacesClientService { + private repositoryFactory?: SpacesClientRepositoryFactory; + + private config?: ConfigType; + + private clientWrapper?: SpacesClientWrapper; + + constructor(private readonly debugLogger: (message: string) => void) {} + + public setup({ config$ }: SetupDeps): SpacesClientServiceSetup { + config$.subscribe((nextConfig) => { + this.config = nextConfig; + }); + + return { + setClientRepositoryFactory: (repositoryFactory: SpacesClientRepositoryFactory) => { + if (this.repositoryFactory) { + throw new Error(`Repository factory has already been set`); + } + this.repositoryFactory = repositoryFactory; + }, + registerClientWrapper: (wrapper: SpacesClientWrapper) => { + if (this.clientWrapper) { + throw new Error(`Client wrapper has already been set`); + } + this.clientWrapper = wrapper; + }, + }; + } + + public start(coreStart: CoreStart): SpacesClientServiceStart { + if (!this.repositoryFactory) { + this.repositoryFactory = (request, savedObjectsStart) => + savedObjectsStart.createScopedRepository(request, ['space']); + } + return { + createSpacesClient: (request: KibanaRequest) => { + if (!this.config) { + throw new Error('Initialization error: spaces config is not available'); + } + + const baseClient = new SpacesClient( + this.debugLogger, + this.config, + this.repositoryFactory!(request, coreStart.savedObjects) + ); + if (this.clientWrapper) { + return this.clientWrapper(request, baseClient); + } + return baseClient; + }, + }; + } +} diff --git a/x-pack/plugins/spaces/server/spaces_service/index.ts b/x-pack/plugins/spaces/server/spaces_service/index.ts index 69a7e171a5186..ee3f1505ebaad 100644 --- a/x-pack/plugins/spaces/server/spaces_service/index.ts +++ b/x-pack/plugins/spaces/server/spaces_service/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { SpacesService, SpacesServiceSetup } from './spaces_service'; +export { SpacesService, SpacesServiceSetup, SpacesServiceStart } from './spaces_service'; diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.mock.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.mock.ts index 6f21330368f8d..18a2f20a4ee14 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.mock.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.mock.ts @@ -4,24 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SpacesServiceSetup } from './spaces_service'; -import { spacesClientMock } from '../lib/spaces_client/spaces_client.mock'; +import { SpacesServiceSetup, SpacesServiceStart } from './spaces_service'; +import { spacesClientMock } from '../spaces_client/spaces_client.mock'; import { DEFAULT_SPACE_ID } from '../../common/constants'; import { namespaceToSpaceId, spaceIdToNamespace } from '../lib/utils/namespace'; const createSetupContractMock = (spaceId = DEFAULT_SPACE_ID) => { const setupContract: jest.Mocked = { + namespaceToSpaceId: jest.fn().mockImplementation(namespaceToSpaceId), + spaceIdToNamespace: jest.fn().mockImplementation(spaceIdToNamespace), getSpaceId: jest.fn().mockReturnValue(spaceId), - isInDefaultSpace: jest.fn().mockReturnValue(spaceId === DEFAULT_SPACE_ID), - getBasePath: jest.fn().mockReturnValue(''), - scopedClient: jest.fn().mockResolvedValue(spacesClientMock.create()), + }; + return setupContract; +}; + +const createStartContractMock = (spaceId = DEFAULT_SPACE_ID) => { + const startContract: jest.Mocked = { namespaceToSpaceId: jest.fn().mockImplementation(namespaceToSpaceId), spaceIdToNamespace: jest.fn().mockImplementation(spaceIdToNamespace), + createSpacesClient: jest.fn().mockReturnValue(spacesClientMock.create()), + getSpaceId: jest.fn().mockReturnValue(spaceId), + isInDefaultSpace: jest.fn().mockReturnValue(spaceId === DEFAULT_SPACE_ID), getActiveSpace: jest.fn(), }; - return setupContract; + return startContract; }; export const spacesServiceMock = { createSetupContract: createSetupContractMock, + createStartContract: createStartContractMock, }; diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts index d1e1d81134940..c7a65ec807b60 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts @@ -5,8 +5,7 @@ */ import * as Rx from 'rxjs'; import { SpacesService } from './spaces_service'; -import { coreMock, httpServerMock, loggingSystemMock } from 'src/core/server/mocks'; -import { SpacesAuditLogger } from '../lib/audit_logger'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; import { KibanaRequest, SavedObjectsErrorHelpers, @@ -16,12 +15,10 @@ import { import { DEFAULT_SPACE_ID } from '../../common/constants'; import { getSpaceIdFromPath } from '../../common/lib/spaces_url_parser'; import { spacesConfig } from '../lib/__fixtures__'; -import { securityMock } from '../../../security/server/mocks'; +import { SpacesClientService } from '../spaces_client'; -const mockLogger = loggingSystemMock.createLogger(); - -const createService = async (serverBasePath: string = '') => { - const spacesService = new SpacesService(mockLogger); +const createService = (serverBasePath: string = '') => { + const spacesService = new SpacesService(); const coreStart = coreMock.createStart(); @@ -66,117 +63,95 @@ const createService = async (serverBasePath: string = '') => { return '/'; }); - const spacesServiceSetup = await spacesService.setup({ - http: httpSetup, - getStartServices: async () => [coreStart, {}, {}], + coreStart.http.basePath = httpSetup.basePath; + + const spacesServiceSetup = spacesService.setup({ + basePath: httpSetup.basePath, + }); + + const spacesClientService = new SpacesClientService(jest.fn()); + spacesClientService.setup({ config$: Rx.of(spacesConfig), - authorization: securityMock.createSetup().authz, - auditLogger: new SpacesAuditLogger(), }); - return spacesServiceSetup; + const spacesClientServiceStart = spacesClientService.start(coreStart); + + const spacesServiceStart = spacesService.start({ + basePath: coreStart.http.basePath, + spacesClientService: spacesClientServiceStart, + }); + + return { + spacesServiceSetup, + spacesServiceStart, + }; }; describe('SpacesService', () => { describe('#getSpaceId', () => { it('returns the default space id when no identifier is present', async () => { - const spacesServiceSetup = await createService(); + const { spacesServiceStart } = createService(); const request: KibanaRequest = { url: { pathname: '/app/kibana' }, } as KibanaRequest; - expect(spacesServiceSetup.getSpaceId(request)).toEqual(DEFAULT_SPACE_ID); + expect(spacesServiceStart.getSpaceId(request)).toEqual(DEFAULT_SPACE_ID); }); it('returns the space id when identifier is present', async () => { - const spacesServiceSetup = await createService(); + const { spacesServiceStart } = createService(); const request: KibanaRequest = { url: { pathname: '/s/foo/app/kibana' }, } as KibanaRequest; - expect(spacesServiceSetup.getSpaceId(request)).toEqual('foo'); - }); - }); - - describe('#getBasePath', () => { - it(`throws when a space id is not provided`, async () => { - const spacesServiceSetup = await createService(); - - // @ts-ignore TS knows this isn't right - expect(() => spacesServiceSetup.getBasePath()).toThrowErrorMatchingInlineSnapshot( - `"spaceId is required to retrieve base path"` - ); - - expect(() => spacesServiceSetup.getBasePath('')).toThrowErrorMatchingInlineSnapshot( - `"spaceId is required to retrieve base path"` - ); - }); - - it('returns "" for the default space and no server base path', async () => { - const spacesServiceSetup = await createService(); - expect(spacesServiceSetup.getBasePath(DEFAULT_SPACE_ID)).toEqual(''); - }); - - it('returns /sbp for the default space and the "/sbp" server base path', async () => { - const spacesServiceSetup = await createService('/sbp'); - expect(spacesServiceSetup.getBasePath(DEFAULT_SPACE_ID)).toEqual('/sbp'); - }); - - it('returns /s/foo for the foo space and no server base path', async () => { - const spacesServiceSetup = await createService(); - expect(spacesServiceSetup.getBasePath('foo')).toEqual('/s/foo'); - }); - - it('returns /sbp/s/foo for the foo space and the "/sbp" server base path', async () => { - const spacesServiceSetup = await createService('/sbp'); - expect(spacesServiceSetup.getBasePath('foo')).toEqual('/sbp/s/foo'); + expect(spacesServiceStart.getSpaceId(request)).toEqual('foo'); }); }); describe('#isInDefaultSpace', () => { it('returns true when in the default space', async () => { - const spacesServiceSetup = await createService(); + const { spacesServiceStart } = createService(); const request: KibanaRequest = { url: { pathname: '/app/kibana' }, } as KibanaRequest; - expect(spacesServiceSetup.isInDefaultSpace(request)).toEqual(true); + expect(spacesServiceStart.isInDefaultSpace(request)).toEqual(true); }); it('returns false when not in the default space', async () => { - const spacesServiceSetup = await createService(); + const { spacesServiceStart } = createService(); const request: KibanaRequest = { url: { pathname: '/s/foo/app/kibana' }, } as KibanaRequest; - expect(spacesServiceSetup.isInDefaultSpace(request)).toEqual(false); + expect(spacesServiceStart.isInDefaultSpace(request)).toEqual(false); }); }); describe('#spaceIdToNamespace', () => { it('returns the namespace for the given space', async () => { - const spacesServiceSetup = await createService(); + const { spacesServiceSetup } = createService(); expect(spacesServiceSetup.spaceIdToNamespace('foo')).toEqual('foo'); }); }); describe('#namespaceToSpaceId', () => { it('returns the space id for the given namespace', async () => { - const spacesServiceSetup = await createService(); + const { spacesServiceSetup } = createService(); expect(spacesServiceSetup.namespaceToSpaceId('foo')).toEqual('foo'); }); }); describe('#getActiveSpace', () => { it('returns the default space when in the default space', async () => { - const spacesServiceSetup = await createService(); + const { spacesServiceStart } = createService(); const request = httpServerMock.createKibanaRequest({ path: 'app/kibana' }); - const activeSpace = await spacesServiceSetup.getActiveSpace(request); + const activeSpace = await spacesServiceStart.getActiveSpace(request); expect(activeSpace).toEqual({ id: 'space:default', name: 'Default Space', @@ -186,10 +161,10 @@ describe('SpacesService', () => { }); it('returns the space for the current (non-default) space', async () => { - const spacesServiceSetup = await createService(); + const { spacesServiceStart } = createService(); const request = httpServerMock.createKibanaRequest({ path: '/s/foo/app/kibana' }); - const activeSpace = await spacesServiceSetup.getActiveSpace(request); + const activeSpace = await spacesServiceStart.getActiveSpace(request); expect(activeSpace).toEqual({ id: 'space:foo', name: 'Foo Space', @@ -198,11 +173,11 @@ describe('SpacesService', () => { }); it('propagates errors from the repository', async () => { - const spacesServiceSetup = await createService(); + const { spacesServiceStart } = createService(); const request = httpServerMock.createKibanaRequest({ path: '/s/unknown-space/app/kibana' }); await expect( - spacesServiceSetup.getActiveSpace(request) + spacesServiceStart.getActiveSpace(request) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Saved object [space/unknown-space] not found"` ); diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts index 3630675a7ed3f..d1e02c4162838 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts @@ -4,133 +4,128 @@ * you may not use this file except in compliance with the Elastic License. */ -import { map, take } from 'rxjs/operators'; -import { Observable, Subscription } from 'rxjs'; -import { Legacy } from 'kibana'; -import { Logger, KibanaRequest, CoreSetup } from '../../../../../src/core/server'; -import { SecurityPluginSetup } from '../../../security/server'; -import { SpacesClient } from '../lib/spaces_client'; -import { ConfigType } from '../config'; -import { getSpaceIdFromPath, addSpaceIdToPath } from '../../common/lib/spaces_url_parser'; +import type { KibanaRequest, IBasePath } from 'src/core/server'; +import { SpacesClientServiceStart } from '../spaces_client'; +import { getSpaceIdFromPath } from '../../common'; import { DEFAULT_SPACE_ID } from '../../common/constants'; import { spaceIdToNamespace, namespaceToSpaceId } from '../lib/utils/namespace'; -import { Space } from '../../common/model/space'; -import { SpacesAuditLogger } from '../lib/audit_logger'; - -type RequestFacade = KibanaRequest | Legacy.Request; +import { Space } from '..'; export interface SpacesServiceSetup { - scopedClient(request: RequestFacade): Promise; - - getSpaceId(request: RequestFacade): string; - - getBasePath(spaceId: string): string; + /** + * Retrieves the space id associated with the provided request. + * @param request + * + * @deprecated Use `getSpaceId` from the `SpacesServiceStart` contract instead. + */ + getSpaceId(request: KibanaRequest): string; + + /** + * Converts the provided space id into the corresponding Saved Objects `namespace` id. + * @param spaceId + * + * @deprecated use `spaceIdToNamespace` from the `SpacesServiceStart` contract instead. + */ + spaceIdToNamespace(spaceId: string): string | undefined; - isInDefaultSpace(request: RequestFacade): boolean; + /** + * Converts the provided namespace into the corresponding space id. + * @param namespace + * + * @deprecated use `namespaceToSpaceId` from the `SpacesServiceStart` contract instead. + */ + namespaceToSpaceId(namespace: string | undefined): string; +} +export interface SpacesServiceStart { + /** + * Creates a scoped instance of the SpacesClient. + */ + createSpacesClient: SpacesClientServiceStart['createSpacesClient']; + + /** + * Retrieves the space id associated with the provided request. + * @param request + */ + getSpaceId(request: KibanaRequest): string; + + /** + * Indicates if the provided request is executing within the context of the `default` space. + * @param request + */ + isInDefaultSpace(request: KibanaRequest): boolean; + + /** + * Retrieves the Space associated with the provided request. + * @param request + */ + getActiveSpace(request: KibanaRequest): Promise; + + /** + * Converts the provided space id into the corresponding Saved Objects `namespace` id. + * @param spaceId + */ spaceIdToNamespace(spaceId: string): string | undefined; + /** + * Converts the provided namespace into the corresponding space id. + * @param namespace + */ namespaceToSpaceId(namespace: string | undefined): string; +} - getActiveSpace(request: RequestFacade): Promise; +interface SpacesServiceSetupDeps { + basePath: IBasePath; } -interface SpacesServiceDeps { - http: CoreSetup['http']; - getStartServices: CoreSetup['getStartServices']; - authorization: SecurityPluginSetup['authz'] | null; - config$: Observable; - auditLogger: SpacesAuditLogger; +interface SpacesServiceStartDeps { + basePath: IBasePath; + spacesClientService: SpacesClientServiceStart; } export class SpacesService { - private configSubscription$?: Subscription; - - constructor(private readonly log: Logger) {} - - public async setup({ - http, - getStartServices, - authorization, - config$, - auditLogger, - }: SpacesServiceDeps): Promise { - const getSpaceId = (request: RequestFacade) => { - // Currently utilized by reporting - const isFakeRequest = typeof (request as any).getBasePath === 'function'; - - const basePath = isFakeRequest - ? (request as Record).getBasePath() - : http.basePath.get(request); - - const { spaceId } = getSpaceIdFromPath(basePath, http.basePath.serverBasePath); - - return spaceId; - }; - - const internalRepositoryPromise = getStartServices().then(([coreStart]) => - coreStart.savedObjects.createInternalRepository(['space']) - ); - - const getScopedClient = async (request: KibanaRequest) => { - const [coreStart] = await getStartServices(); - const internalRepository = await internalRepositoryPromise; - - return config$ - .pipe( - take(1), - map((config) => { - const callWithRequestRepository = coreStart.savedObjects.createScopedRepository( - request, - ['space'] - ); - - return new SpacesClient( - auditLogger, - (message: string) => { - this.log.debug(message); - }, - authorization, - callWithRequestRepository, - config, - internalRepository, - request - ); - }) - ) - .toPromise(); + public setup({ basePath }: SpacesServiceSetupDeps): SpacesServiceSetup { + return { + getSpaceId: (request: KibanaRequest) => { + return this.getSpaceId(request, basePath); + }, + spaceIdToNamespace, + namespaceToSpaceId, }; + } + public start({ basePath, spacesClientService }: SpacesServiceStartDeps) { return { - getSpaceId, - getBasePath: (spaceId: string) => { - if (!spaceId) { - throw new TypeError(`spaceId is required to retrieve base path`); - } - return addSpaceIdToPath(http.basePath.serverBasePath, spaceId); + getSpaceId: (request: KibanaRequest) => { + return this.getSpaceId(request, basePath); + }, + + getActiveSpace: (request: KibanaRequest) => { + const spaceId = this.getSpaceId(request, basePath); + return spacesClientService.createSpacesClient(request).get(spaceId); }, - isInDefaultSpace: (request: RequestFacade) => { - const spaceId = getSpaceId(request); + + isInDefaultSpace: (request: KibanaRequest) => { + const spaceId = this.getSpaceId(request, basePath); return spaceId === DEFAULT_SPACE_ID; }, + + createSpacesClient: (request: KibanaRequest) => + spacesClientService.createSpacesClient(request), + spaceIdToNamespace, namespaceToSpaceId, - scopedClient: getScopedClient, - getActiveSpace: async (request: RequestFacade) => { - const spaceId = getSpaceId(request); - const spacesClient = await getScopedClient( - request instanceof KibanaRequest ? request : KibanaRequest.from(request) - ); - return spacesClient.get(spaceId); - }, }; } - public async stop() { - if (this.configSubscription$) { - this.configSubscription$.unsubscribe(); - this.configSubscription$ = undefined; - } + public stop() {} + + private getSpaceId(request: KibanaRequest, basePathService: IBasePath) { + const basePath = basePathService.get(request); + + const { spaceId } = getSpaceIdFromPath(basePath, basePathService.serverBasePath); + + return spaceId; } } diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx index e5e43210d1e6b..0a722734ffc5a 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx @@ -8,7 +8,11 @@ import React, { Fragment, FunctionComponent, useEffect, useRef } from 'react'; import { EuiFormRow } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { IErrorObject, AlertsContextValue } from '../../../../../../triggers_actions_ui/public'; +import { + IErrorObject, + AlertsContextValue, + AlertTypeParamsExpressionProps, +} from '../../../../../../triggers_actions_ui/public'; import { ES_GEO_FIELD_TYPES } from '../../types'; import { GeoIndexPatternSelect } from '../util_components/geo_index_pattern_select'; import { SingleFieldSelect } from '../util_components/single_field_select'; @@ -23,7 +27,7 @@ interface Props { errors: IErrorObject; setAlertParamsDate: (date: string) => void; setAlertParamsGeoField: (geoField: string) => void; - setAlertProperty: (alertProp: string, alertParams: unknown) => void; + setAlertProperty: AlertTypeParamsExpressionProps['setAlertProperty']; setIndexPattern: (indexPattern: IIndexPattern) => void; indexPattern: IIndexPattern; isInvalid: boolean; diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts index c2e62b6e1898b..3470ee4d76486 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts @@ -477,6 +477,41 @@ describe('Workload Statistics Aggregator', () => { }, reject); }); }); + + test('recovery after errors occurrs at the next interval', async () => { + const refreshInterval = 1000; + + const taskStore = taskStoreMock.create({}); + const logger = loggingSystemMock.create().get(); + const workloadAggregator = createWorkloadAggregator( + taskStore, + of(true), + refreshInterval, + 3000, + logger + ); + + return new Promise((resolve, reject) => { + let errorWasThrowAt = 0; + taskStore.aggregate.mockImplementation(async () => { + if (errorWasThrowAt === 0) { + errorWasThrowAt = Date.now(); + throw new Error(`Elasticsearch has gone poof`); + } else if (Date.now() - errorWasThrowAt < refreshInterval) { + reject(new Error(`Elasticsearch is still poof`)); + } + + return setTaskTypeCount(mockAggregatedResult(), 'alerting_telemetry', { + idle: 2, + }); + }); + + workloadAggregator.pipe(take(2), bufferCount(2)).subscribe((results) => { + expect(results.length).toEqual(2); + resolve(); + }, reject); + }); + }); }); describe('estimateRecurringTaskScheduling', () => { diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts index a27b5e2282e32..8002ee44d01ff 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts @@ -5,7 +5,7 @@ */ import { combineLatest, Observable, timer } from 'rxjs'; -import { mergeMap, map, filter, catchError } from 'rxjs/operators'; +import { mergeMap, map, filter, switchMap, catchError } from 'rxjs/operators'; import { Logger } from 'src/core/server'; import { JsonObject } from 'src/plugins/kibana_utils/common'; import { keyBy, mapValues } from 'lodash'; @@ -222,8 +222,8 @@ export function createWorkloadAggregator( }), catchError((ex: Error, caught) => { logger.error(`[WorkloadAggregator]: ${ex}`); - // continue to pull values from the same observable - return caught; + // continue to pull values from the same observable but only on the next refreshInterval + return timer(refreshInterval).pipe(switchMap(() => caught)); }) ); } diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index c965623ebfc17..8936cdafa3827 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -1778,30 +1778,6 @@ } } }, - "infraops": { - "properties": { - "last_24_hours": { - "properties": { - "hits": { - "properties": { - "infraops_hosts": { - "type": "long" - }, - "infraops_docker": { - "type": "long" - }, - "infraops_kubernetes": { - "type": "long" - }, - "logs": { - "type": "long" - } - } - } - } - } - } - }, "ingest_manager": { "properties": { "fleet_enabled": { @@ -1841,6 +1817,30 @@ } } }, + "infraops": { + "properties": { + "last_24_hours": { + "properties": { + "hits": { + "properties": { + "infraops_hosts": { + "type": "long" + }, + "infraops_docker": { + "type": "long" + }, + "infraops_kubernetes": { + "type": "long" + }, + "logs": { + "type": "long" + } + } + } + } + } + } + }, "lens": { "properties": { "events_30_days": { @@ -3136,6 +3136,50 @@ } } }, + "saved_objects_tagging": { + "properties": { + "usedTags": { + "type": "integer" + }, + "taggedObjects": { + "type": "integer" + }, + "types": { + "properties": { + "dashboard": { + "properties": { + "usedTags": { + "type": "integer" + }, + "taggedObjects": { + "type": "integer" + } + } + }, + "visualization": { + "properties": { + "usedTags": { + "type": "integer" + }, + "taggedObjects": { + "type": "integer" + } + } + }, + "map": { + "properties": { + "usedTags": { + "type": "integer" + }, + "taggedObjects": { + "type": "integer" + } + } + } + } + } + } + }, "security_solution": { "properties": { "detections": { diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index ef81065608ad4..3e5e95996c80f 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -25,6 +25,7 @@ Table of Contents - [GROUPED BY expression component](#grouped-by-expression-component) - [FOR THE LAST expression component](#for-the-last-expression-component) - [THRESHOLD expression component](#threshold-expression-component) + - [Alert Conditions Components](#alert-conditions-components) - [Embed the Create Alert flyout within any Kibana plugin](#embed-the-create-alert-flyout-within-any-kibana-plugin) - [Build and register Action Types](#build-and-register-action-types) - [Built-in Action Types](#built-in-action-types) @@ -634,6 +635,155 @@ interface ThresholdExpressionProps { |customComparators|(Optional) List of comparators that replaces the default options defined in constants `x-pack/plugins/triggers_actions_ui/public/common/constants/comparators.ts`.| |popupPosition|(Optional) expression popup position. Default is `downLeft`. Recommend changing it for a small parent window space.| +## Alert Conditions Components +To aid in creating a uniform UX across Alert Types, we provide two components for specifying the conditions for detection of a certain alert under within any specific Action Groups: +1. `AlertConditions`: A component that generates a container which renders custom component for each Action Group which has had its _conditions_ specified. +2. `AlertConditionsGroup`: A component that provides a unified container for the Action Group with its name and a button for resetting its condition. + +These can be used by any Alert Type to easily create the UI for adding action groups along with an Alert Type specific component. + +For Example: +Given an Alert Type which requires different thresholds for each detected Action Group (for example), you might have a `ThresholdSpecifier` component for specifying the threshold for a specific Action Group. + +``` +const ThresholdSpecifier = ( + { + actionGroup, + setThreshold + } : { + actionGroup?: ActionGroupWithCondition; + setThreshold: (actionGroup: ActionGroupWithCondition) => void; +}) => { + if (!actionGroup) { + // render empty if no condition action group is specified + return ; + } + + return ( + { + const conditions = parseInt(e.target.value, 10); + if (e.target.value && !isNaN(conditions)) { + setThreshold({ + ...actionGroup, + conditions, + }); + } + }} + /> + ); +}; + +``` + +This component takes two props, one which is required (`actionGroup`) and one which is alert type specific (`setThreshold`). +The `actionGroup` will be populated by the `AlertConditions` component, but `setThreshold` will have to be provided by the AlertType itself. + +To understand how this is used, lets take a closer look at `actionGroup`: + +``` +type ActionGroupWithCondition = ActionGroup & + ( + | // allow isRequired=false with or without conditions + { + conditions?: T; + isRequired?: false; + } + // but if isRequired=true then conditions must be specified + | { + conditions: T; + isRequired: true; + } + ) +``` + +The `condition` field is Alert Type specific, and holds whichever type an Alert Type needs for specifying the condition under which a certain detection falls under that specific Action Group. +In our example, this is a `number` as that's all we need to speciufy the threshold which dictates whether an alert falls into one actio ngroup rather than another. + +The `isRequired` field specifies whether this specific action group is _required_, that is, you can't reset its condition and _have_ to specify a some condition for it. + +Using this `ThresholdSpecifier` component, we can now use `AlertConditionsGroup` & `AlertConditions` to enable the user to specify these thresholds for each action group in the alert type. + +Like so: +``` +interface ThresholdAlertTypeParams { + thresholds?: { + alert?: number; + warning?: number; + error?: number; + }; +} + +const DEFAULT_THRESHOLDS: ThresholdAlertTypeParams['threshold] = { + alert: 50, + warning: 80, + error: 90, +}; +``` + +``` + { + setAlertParams('thresholds', { + ...thresholds, + ...pick(DEFAULT_THRESHOLDS, actionGroup.id), + }); + }} +> + { + setAlertParams('thresholds', omit(thresholds, actionGroup.id)); + }} + > + { + setAlertParams('thresholds', { + ...thresholds, + [actionGroup.id]: actionGroup.conditions, + }); + }} + /> + + +``` + +### The AlertConditions component + +This component will render the `Conditions` header & headline, along with the selectors for adding every Action Group you specity. +Additionally it will clone its `children` for _each_ action group which has a `condition` specified for it, passing in the appropriate `actionGroup` prop for each one. + +|Property|Description| +|---|---| +|headline|The headline title displayed above the fields | +|actionGroups|A list of `ActionGroupWithCondition` which includes all the action group you wish to offer the user and what conditions they are already configured to follow| +|onInitializeConditionsFor|A callback which is called when the user ask for a certain actionGroup to be initialized with an initial default condition. If you have no specific default, that's fine, as the component will render the action group's field even if the condition is empty (using a `null` or an `undefined`) and determines whether to render these fields by _the very presence_ of a `condition` field| + +### The AlertConditionsGroup component + +This component renders a standard EuiTitle foe each action group, wrapping the Alert Type specific component, in addition to a "reset" button which allows the user to reset the condition for that action group. The definition of what a _reset_ actually means is Alert Type specific, and up to the implementor to decide. In some case it might mean removing the condition, in others it might mean to reset it to some default value on the server side. In either case, it should _delete_ the `condition` field from the appropriate `actionGroup` as per the above example. + +|Property|Description| +|---|---| +|onResetConditionsFor|A callback which is called when the user clicks the _reset_ button besides the action group's title. The implementor should use this to remove the `condition` from the specified actionGroup| + + ## Embed the Create Alert flyout within any Kibana plugin Follow the instructions bellow to embed the Create Alert flyout within any Kibana plugin: diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts index 9e89a38377a4d..7fb50eaab7d7d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Alert, AlertType } from '../../types'; +import { AlertType } from '../../types'; +import { InitialAlert } from '../sections/alert_form/alert_reducer'; /** * NOTE: Applications that want to show the alerting UIs will need to add @@ -21,9 +22,9 @@ export const hasExecuteActionsCapability = (capabilities: Capabilities) => export const hasDeleteActionsCapability = (capabilities: Capabilities) => capabilities?.actions?.delete; -export function hasAllPrivilege(alert: Alert, alertType?: AlertType): boolean { +export function hasAllPrivilege(alert: InitialAlert, alertType?: AlertType): boolean { return alertType?.authorizedConsumers[alert.consumer]?.all ?? false; } -export function hasReadPrivilege(alert: Alert, alertType?: AlertType): boolean { +export function hasReadPrivilege(alert: InitialAlert, alertType?: AlertType): boolean { return alertType?.authorizedConsumers[alert.consumer]?.read ?? false; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 50f5167b9e5c2..83e6386122eb2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -36,7 +36,7 @@ import { AddConnectorInline } from './connector_add_inline'; import { actionTypeCompare } from '../../lib/action_type_compare'; import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled'; import { VIEW_LICENSE_OPTIONS_LINK, DEFAULT_HIDDEN_ACTION_TYPES } from '../../../common/constants'; -import { ActionGroup } from '../../../../../alerts/common'; +import { ActionGroup, AlertActionParam } from '../../../../../alerts/common'; export interface ActionAccordionFormProps { actions: AlertAction[]; @@ -45,7 +45,7 @@ export interface ActionAccordionFormProps { setActionIdByIndex: (id: string, index: number) => void; setActionGroupIdByIndex?: (group: string, index: number) => void; setAlertProperty: (actions: AlertAction[]) => void; - setActionParamsProperty: (key: string, value: any, index: number) => void; + setActionParamsProperty: (key: string, value: AlertActionParam, index: number) => void; http: HttpSetup; actionTypeRegistry: ActionTypeRegistryContract; toastNotifications: ToastsSetup; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx index 149ea88291066..10c8498b181dc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx @@ -26,7 +26,7 @@ import { EuiBadge, EuiErrorBoundary, } from '@elastic/eui'; -import { ResolvedActionGroup } from '../../../../../alerts/common'; +import { AlertActionParam, ResolvedActionGroup } from '../../../../../alerts/common'; import { IErrorObject, AlertAction, @@ -51,7 +51,7 @@ export type ActionTypeFormProps = { onAddConnector: () => void; onConnectorSelected: (id: string) => void; onDeleteAction: () => void; - setActionParamsProperty: (key: string, value: any, index: number) => void; + setActionParamsProperty: (key: string, value: AlertActionParam, index: number) => void; actionTypesIndex: ActionTypeIndex; connectors: ActionConnector[]; } & Pick< diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index b38f0e749a28d..d7de7e0a82c1e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -75,8 +75,8 @@ export const AlertDetails: React.FunctionComponent = ({ chrome, } = useAppDependencies(); const [{}, dispatch] = useReducer(alertReducer, { alert }); - const setInitialAlert = (key: string, value: any) => { - dispatch({ command: { type: 'setAlert' }, payload: { key, value } }); + const setInitialAlert = (value: Alert) => { + dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } }); }; // Set breadcrumb and page title @@ -172,7 +172,7 @@ export const AlertDetails: React.FunctionComponent = ({ { - setInitialAlert('alert', alert); + setInitialAlert(alert); setEditFlyoutVisibility(false); }} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index 741cbadb07070..34a4c909c65a9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -3,15 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useReducer, useState, useEffect } from 'react'; -import { isObject } from 'lodash'; +import React, { useCallback, useReducer, useMemo, useState, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, EuiFlyoutHeader, EuiFlyout, EuiFlyoutBody, EuiPortal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useAlertsContext } from '../../context/alerts_context'; import { Alert, AlertAction, IErrorObject } from '../../../types'; -import { AlertForm, validateBaseProperties } from './alert_form'; -import { alertReducer } from './alert_reducer'; +import { AlertForm, isValidAlert, validateBaseProperties } from './alert_form'; +import { alertReducer, InitialAlert, InitialAlertReducer } from './alert_reducer'; import { createAlert } from '../../lib/alert_api'; import { HealthCheck } from '../../components/health_check'; import { ConfirmAlertSave } from './confirm_alert_save'; @@ -36,27 +35,32 @@ export const AlertAdd = ({ alertTypeId, initialValues, }: AlertAddProps) => { - // eslint-disable-next-line react-hooks/exhaustive-deps - const initialAlert = ({ - params: {}, - consumer, - alertTypeId, - schedule: { - interval: '1m', - }, - actions: [], - tags: [], - ...(initialValues ? initialValues : {}), - } as unknown) as Alert; - - const [{ alert }, dispatch] = useReducer(alertReducer, { alert: initialAlert }); + const initialAlert: InitialAlert = useMemo( + () => ({ + params: {}, + consumer, + alertTypeId, + schedule: { + interval: '1m', + }, + actions: [], + tags: [], + ...(initialValues ? initialValues : {}), + }), + [alertTypeId, consumer, initialValues] + ); + + const [{ alert }, dispatch] = useReducer(alertReducer as InitialAlertReducer, { + alert: initialAlert, + }); const [isSaving, setIsSaving] = useState(false); const [isConfirmAlertSaveModalOpen, setIsConfirmAlertSaveModalOpen] = useState(false); - const setAlert = (value: any) => { + const setAlert = (value: InitialAlert) => { dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } }); }; - const setAlertProperty = (key: string, value: any) => { + + const setAlertProperty = (key: Key, value: Alert[Key] | null) => { dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); }; @@ -73,7 +77,7 @@ export const AlertAdd = ({ const canShowActions = hasShowActionsCapability(capabilities); useEffect(() => { - setAlertProperty('alertTypeId', alertTypeId); + setAlertProperty('alertTypeId', alertTypeId ?? null); }, [alertTypeId]); const closeFlyout = useCallback(() => { @@ -101,7 +105,7 @@ export const AlertAdd = ({ ...(alertType ? alertType.validate(alert.params).errors : []), ...validateBaseProperties(alert).errors, } as IErrorObject; - const hasErrors = parseErrors(errors); + const hasErrors = !isValidAlert(alert, errors); const actionsErrors: Array<{ errors: IErrorObject; @@ -121,16 +125,18 @@ export const AlertAdd = ({ async function onSaveAlert(): Promise { try { - const newAlert = await createAlert({ http, alert }); - toastNotifications.addSuccess( - i18n.translate('xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText', { - defaultMessage: 'Created alert "{alertName}"', - values: { - alertName: newAlert.name, - }, - }) - ); - return newAlert; + if (isValidAlert(alert, errors)) { + const newAlert = await createAlert({ http, alert }); + toastNotifications.addSuccess( + i18n.translate('xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText', { + defaultMessage: 'Created alert "{alertName}"', + values: { + alertName: newAlert.name, + }, + }) + ); + return newAlert; + } } catch (errorRes) { toastNotifications.addDanger( errorRes.body?.message ?? @@ -207,11 +213,5 @@ export const AlertAdd = ({ ); }; -const parseErrors: (errors: IErrorObject) => boolean = (errors) => - !!Object.values(errors).find((errorList) => { - if (isObject(errorList)) return parseErrors(errorList as IErrorObject); - return errorList.length >= 1; - }); - // eslint-disable-next-line import/no-default-export export { AlertAdd as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx new file mode 100644 index 0000000000000..8029b43a2cf53 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx @@ -0,0 +1,260 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 React from 'react'; +import { mountWithIntl, nextTick } from '@kbn/test/jest'; +import { act } from 'react-dom/test-utils'; +import { ReactWrapper } from 'enzyme'; +import { AlertConditions, ActionGroupWithCondition } from './alert_conditions'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiTitle, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiButtonEmpty, +} from '@elastic/eui'; + +describe('alert_conditions', () => { + async function setup(element: React.ReactElement): Promise> { + const wrapper = mountWithIntl(element); + + // Wait for active space to resolve before requesting the component to update + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + return wrapper; + } + + it('renders with custom headline', async () => { + const wrapper = await setup( + + ); + + expect(wrapper.find(EuiTitle).find(FormattedMessage).prop('id')).toMatchInlineSnapshot( + `"xpack.triggersActionsUI.sections.alertForm.conditions.title"` + ); + expect( + wrapper.find(EuiTitle).find(FormattedMessage).prop('defaultMessage') + ).toMatchInlineSnapshot(`"Conditions:"`); + + expect(wrapper.find('[data-test-subj="alertConditionsHeadline"]').get(0)) + .toMatchInlineSnapshot(` + + Set different threshold with their own status + + `); + }); + + it('renders any action group with conditions on it', async () => { + const ConditionForm = ({ + actionGroup, + }: { + actionGroup?: ActionGroupWithCondition<{ someProp: string }>; + }) => { + return ( + + ID + {actionGroup?.id} + Name + {actionGroup?.name} + SomeProp + + {actionGroup?.conditions?.someProp} + + + ); + }; + + const wrapper = await setup( + + + + ); + + expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(0)) + .toMatchInlineSnapshot(` + + default + + `); + + expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(1)) + .toMatchInlineSnapshot(` + + Default + + `); + + expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(2)) + .toMatchInlineSnapshot(` + + my prop value + + `); + }); + + it('doesnt render action group without conditions', async () => { + const ConditionForm = ({ + actionGroup, + }: { + actionGroup?: ActionGroupWithCondition<{ someProp: string }>; + }) => { + return ( + + ID + {actionGroup?.id} + + ); + }; + + const wrapper = await setup( + + + + ); + + expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(0)) + .toMatchInlineSnapshot(` + + default + + `); + + expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(1)) + .toMatchInlineSnapshot(` + + shouldRender + + `); + + expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).length).toEqual(2); + }); + + it('render add buttons for action group without conditions', async () => { + const onInitializeConditionsFor = jest.fn(); + + const ConditionForm = ({ + actionGroup, + }: { + actionGroup?: ActionGroupWithCondition<{ someProp: string }>; + }) => { + return ( + + ID + {actionGroup?.id} + + ); + }; + + const wrapper = await setup( + + + + ); + + expect(wrapper.find(EuiButtonEmpty).get(0)).toMatchInlineSnapshot(` + + Should Render A Link + + `); + wrapper.find(EuiButtonEmpty).simulate('click'); + + expect(onInitializeConditionsFor).toHaveBeenCalledWith({ + id: 'shouldRenderLink', + name: 'Should Render A Link', + }); + }); + + it('passes in any additional props the container passes in', async () => { + const callbackProp = jest.fn(); + + const ConditionForm = ({ + actionGroup, + someCallbackProp, + }: { + actionGroup?: ActionGroupWithCondition<{ someProp: string }>; + someCallbackProp: (actionGroup: ActionGroupWithCondition<{ someProp: string }>) => void; + }) => { + if (!actionGroup) { + return
; + } + + // call callback when the actionGroup is available + someCallbackProp(actionGroup); + return ( + + ID + {actionGroup?.id} + Name + {actionGroup?.name} + SomeProp + + {actionGroup?.conditions?.someProp} + + + ); + }; + + await setup( + + + + ); + + expect(callbackProp).toHaveBeenCalledWith({ + id: 'default', + name: 'Default', + conditions: { someProp: 'my prop value' }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.tsx new file mode 100644 index 0000000000000..1eb086dd6a2c5 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.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 React, { PropsWithChildren } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexItem, EuiText, EuiFlexGroup, EuiTitle, EuiButtonEmpty } from '@elastic/eui'; +import { partition } from 'lodash'; +import { ActionGroup, getBuiltinActionGroups } from '../../../../../alerts/common'; + +const BUILT_IN_ACTION_GROUPS: Set = new Set(getBuiltinActionGroups().map(({ id }) => id)); + +export type ActionGroupWithCondition = ActionGroup & + ( + | // allow isRequired=false with or without conditions + { + conditions?: T; + isRequired?: false; + } + // but if isRequired=true then conditions must be specified + | { + conditions: T; + isRequired: true; + } + ); + +export interface AlertConditionsProps { + headline?: string; + actionGroups: Array>; + onInitializeConditionsFor?: (actionGroup: ActionGroupWithCondition) => void; + onResetConditionsFor?: (actionGroup: ActionGroupWithCondition) => void; + includeBuiltInActionGroups?: boolean; +} + +export const AlertConditions = ({ + headline, + actionGroups, + onInitializeConditionsFor, + onResetConditionsFor, + includeBuiltInActionGroups = false, + children, +}: PropsWithChildren>) => { + const [withConditions, withoutConditions] = partition( + includeBuiltInActionGroups + ? actionGroups + : actionGroups.filter(({ id }) => !BUILT_IN_ACTION_GROUPS.has(id)), + (actionGroup) => actionGroup.hasOwnProperty('conditions') + ); + + return ( + + + + + +
+ +
+
+ {headline && ( + + + {headline} + + + )} +
+
+
+ + + {withConditions.map((actionGroup) => ( + + {React.isValidElement(children) && + React.cloneElement( + React.Children.only(children), + onResetConditionsFor + ? { + actionGroup, + onResetConditionsFor, + } + : { actionGroup } + )} + + ))} + {onInitializeConditionsFor && withoutConditions.length > 0 && ( + + + + + + {withoutConditions.map((actionGroup) => ( + + onInitializeConditionsFor(actionGroup)} + > + {actionGroup.name} + + + ))} + + + )} + + +
+ ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.test.tsx new file mode 100644 index 0000000000000..dd12af4ae9e62 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.test.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 React from 'react'; +import { mountWithIntl, nextTick } from '@kbn/test/jest'; +import { act } from 'react-dom/test-utils'; +import { ReactWrapper } from 'enzyme'; +import { AlertConditionsGroup } from './alert_conditions_group'; +import { EuiFormRow, EuiButtonIcon } from '@elastic/eui'; + +describe('alert_conditions_group', () => { + async function setup(element: React.ReactElement): Promise> { + const wrapper = mountWithIntl(element); + + // Wait for active space to resolve before requesting the component to update + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + return wrapper; + } + + it('renders with actionGroup name as label', async () => { + const InnerComponent = () =>
{'inner component'}
; + const wrapper = await setup( + + + + ); + + expect(wrapper.find(EuiFormRow).prop('label')).toMatchInlineSnapshot(` + + + My Group + + + `); + expect(wrapper.find(InnerComponent).prop('actionGroup')).toMatchInlineSnapshot(` + Object { + "id": "myGroup", + "name": "My Group", + } + `); + }); + + it('renders a reset button when onResetConditionsFor is specified', async () => { + const onResetConditionsFor = jest.fn(); + const wrapper = await setup( + +
{'inner component'}
+
+ ); + + expect(wrapper.find(EuiButtonIcon).prop('aria-label')).toMatchInlineSnapshot(`"Remove"`); + + wrapper.find(EuiButtonIcon).simulate('click'); + + expect(onResetConditionsFor).toHaveBeenCalledWith({ + id: 'myGroup', + name: 'My Group', + }); + }); + + it('shouldnt render a reset button when isRequired is true', async () => { + const onResetConditionsFor = jest.fn(); + const wrapper = await setup( + +
{'inner component'}
+
+ ); + + expect(wrapper.find(EuiButtonIcon).length).toEqual(0); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx new file mode 100644 index 0000000000000..879f276317503 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx @@ -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 React, { Fragment, PropsWithChildren } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFormRow, EuiButtonIcon, EuiTitle } from '@elastic/eui'; +import { AlertConditionsProps, ActionGroupWithCondition } from './alert_conditions'; + +export type AlertConditionsGroupProps = { + actionGroup?: ActionGroupWithCondition; +} & Pick, 'onResetConditionsFor'>; + +export const AlertConditionsGroup = ({ + actionGroup, + onResetConditionsFor, + children, + ...otherProps +}: PropsWithChildren>) => { + if (!actionGroup) { + return null; + } + + return ( + + {actionGroup.name} + + } + fullWidth + labelAppend={ + onResetConditionsFor && + !actionGroup.isRequired && ( + onResetConditionsFor(actionGroup)} + /> + ) + } + > + {React.isValidElement(children) ? ( + React.cloneElement(React.Children.only(children), { + actionGroup, + ...otherProps, + }) + ) : ( + + )} + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index d5ae701546c64..2e2a77fa6afc3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { useAlertsContext } from '../../context/alerts_context'; import { Alert, AlertAction, IErrorObject } from '../../../types'; import { AlertForm, validateBaseProperties } from './alert_form'; -import { alertReducer } from './alert_reducer'; +import { alertReducer, ConcreteAlertReducer } from './alert_reducer'; import { updateAlert } from '../../lib/alert_api'; import { HealthCheck } from '../../components/health_check'; import { HealthContextProvider } from '../../context/health_context'; @@ -34,7 +34,9 @@ interface AlertEditProps { } export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { - const [{ alert }, dispatch] = useReducer(alertReducer, { alert: initialAlert }); + const [{ alert }, dispatch] = useReducer(alertReducer as ConcreteAlertReducer, { + alert: initialAlert, + }); const [isSaving, setIsSaving] = useState(false); const [hasActionsDisabled, setHasActionsDisabled] = useState(false); const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] = useState( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index ebb1fffeb1b84..7fd5bdc8d8707 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -34,14 +34,14 @@ import { } from '@elastic/eui'; import { some, filter, map, fold } from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; -import { capitalize } from 'lodash'; +import { capitalize, isObject } from 'lodash'; import { KibanaFeature } from '../../../../../features/public'; import { getDurationNumberInItsUnit, getDurationUnitValue, } from '../../../../../alerts/common/parse_duration'; import { loadAlertTypes } from '../../lib/alert_api'; -import { AlertReducerAction } from './alert_reducer'; +import { AlertReducerAction, InitialAlert } from './alert_reducer'; import { AlertTypeModel, Alert, @@ -49,18 +49,19 @@ import { AlertAction, AlertTypeIndex, AlertType, + ValidationResult, } from '../../../types'; import { getTimeOptions } from '../../../common/lib/get_time_options'; import { useAlertsContext } from '../../context/alerts_context'; import { ActionForm } from '../action_connector_form'; -import { ALERTS_FEATURE_ID } from '../../../../../alerts/common'; +import { AlertActionParam, ALERTS_FEATURE_ID } from '../../../../../alerts/common'; import { hasAllPrivilege, hasShowActionsCapability } from '../../lib/capabilities'; import { SolutionFilter } from './solution_filter'; import './alert_form.scss'; const ENTER_KEY = 13; -export function validateBaseProperties(alertObject: Alert) { +export function validateBaseProperties(alertObject: InitialAlert): ValidationResult { const validationResult = { errors: {} }; const errors = { name: new Array(), @@ -93,12 +94,25 @@ export function validateBaseProperties(alertObject: Alert) { return validationResult; } +const hasErrors: (errors: IErrorObject) => boolean = (errors) => + !!Object.values(errors).find((errorList) => { + if (isObject(errorList)) return hasErrors(errorList as IErrorObject); + return errorList.length >= 1; + }); + +export function isValidAlert( + alertObject: InitialAlert | Alert, + validationResult: IErrorObject +): alertObject is Alert { + return !hasErrors(validationResult); +} + function getProducerFeatureName(producer: string, kibanaFeatures: KibanaFeature[]) { return kibanaFeatures.find((featureItem) => featureItem.id === producer)?.name; } interface AlertFormProps { - alert: Alert; + alert: InitialAlert; dispatch: React.Dispatch; errors: IErrorObject; canChangeTrigger?: boolean; // to hide Change trigger button @@ -204,10 +218,13 @@ export const AlertForm = ({ useEffect(() => { setAlertTypeModel(alert.alertTypeId ? alertTypeRegistry.get(alert.alertTypeId) : null); - }, [alert, alertTypeRegistry]); + if (alert.alertTypeId && alertTypesIndex && alertTypesIndex.has(alert.alertTypeId)) { + setDefaultActionGroupId(alertTypesIndex.get(alert.alertTypeId)!.defaultActionGroupId); + } + }, [alert, alert.alertTypeId, alertTypesIndex, alertTypeRegistry]); const setAlertProperty = useCallback( - (key: string, value: any) => { + (key: Key, value: Alert[Key] | null) => { dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); }, [dispatch] @@ -226,12 +243,16 @@ export const AlertForm = ({ dispatch({ command: { type: 'setScheduleProperty' }, payload: { key, value } }); }; - const setActionProperty = (key: string, value: any, index: number) => { + const setActionProperty = ( + key: Key, + value: AlertAction[Key] | null, + index: number + ) => { dispatch({ command: { type: 'setAlertActionProperty' }, payload: { key, value, index } }); }; const setActionParamsProperty = useCallback( - (key: string, value: any, index: number) => { + (key: string, value: AlertActionParam, index: number) => { dispatch({ command: { type: 'setAlertActionParams' }, payload: { key, value, index } }); }, [dispatch] @@ -437,7 +458,10 @@ export const AlertForm = ({ )} - {AlertParamsExpressionComponent ? ( + {AlertParamsExpressionComponent && + defaultActionGroupId && + alert.alertTypeId && + alertTypesIndex?.has(alert.alertTypeId) ? ( }> @@ -455,6 +481,7 @@ export const AlertForm = ({ {canShowActions && defaultActionGroupId && alertTypeModel && + alert.alertTypeId && alertTypesIndex?.has(alert.alertTypeId) ? ( & + Pick; + +interface CommandType< + T extends | 'setAlert' | 'setProperty' | 'setScheduleProperty' | 'setAlertParams' | 'setAlertActionParams' - | 'setAlertActionProperty'; + | 'setAlertActionProperty' +> { + type: T; } export interface AlertState { - alert: any; + alert: InitialAlert; +} + +interface Payload { + key: Keys; + value: Value; + index?: number; +} + +interface AlertPayload { + key: Key; + value: Alert[Key] | null; + index?: number; +} + +interface AlertActionPayload { + key: Key; + value: AlertAction[Key] | null; + index?: number; } -export interface AlertReducerAction { - command: CommandType; - payload: { - key: string; - value: {}; - index?: number; - }; +interface AlertSchedulePayload { + key: Key; + value: IntervalSchedule[Key]; + index?: number; } -export const alertReducer = (state: any, action: AlertReducerAction) => { - const { command, payload } = action; +export type AlertReducerAction = + | { + command: CommandType<'setAlert'>; + payload: Payload<'alert', InitialAlert>; + } + | { + command: CommandType<'setProperty'>; + payload: AlertPayload; + } + | { + command: CommandType<'setScheduleProperty'>; + payload: AlertSchedulePayload; + } + | { + command: CommandType<'setAlertParams'>; + payload: Payload; + } + | { + command: CommandType<'setAlertActionParams'>; + payload: Payload; + } + | { + command: CommandType<'setAlertActionProperty'>; + payload: AlertActionPayload; + }; + +export type InitialAlertReducer = Reducer<{ alert: InitialAlert }, AlertReducerAction>; +export type ConcreteAlertReducer = Reducer<{ alert: Alert }, AlertReducerAction>; + +export const alertReducer = ( + state: { alert: AlertPhase }, + action: AlertReducerAction +) => { const { alert } = state; - switch (command.type) { + switch (action.command.type) { case 'setAlert': { - const { key, value } = payload; + const { key, value } = action.payload as Payload<'alert', AlertPhase>; if (key === 'alert') { return { ...state, @@ -45,7 +100,7 @@ export const alertReducer = (state: any, action: AlertReducerAction) => { } } case 'setProperty': { - const { key, value } = payload; + const { key, value } = action.payload as AlertPayload; if (isEqual(alert[key], value)) { return state; } else { @@ -59,8 +114,8 @@ export const alertReducer = (state: any, action: AlertReducerAction) => { } } case 'setScheduleProperty': { - const { key, value } = payload; - if (isEqual(alert.schedule[key], value)) { + const { key, value } = action.payload as AlertSchedulePayload; + if (alert.schedule && isEqual(alert.schedule[key], value)) { return state; } else { return { @@ -76,7 +131,7 @@ export const alertReducer = (state: any, action: AlertReducerAction) => { } } case 'setAlertParams': { - const { key, value } = payload; + const { key, value } = action.payload as Payload>; if (isEqual(alert.params[key], value)) { return state; } else { @@ -93,7 +148,10 @@ export const alertReducer = (state: any, action: AlertReducerAction) => { } } case 'setAlertActionParams': { - const { key, value, index } = payload; + const { key, value, index } = action.payload as Payload< + keyof AlertAction, + SavedObjectAttribute + >; if (index === undefined || isEqual(alert.actions[index][key], value)) { return state; } else { @@ -116,7 +174,7 @@ export const alertReducer = (state: any, action: AlertReducerAction) => { } } case 'setAlertActionProperty': { - const { key, value, index } = payload; + const { key, value, index } = action.payload as AlertActionPayload; if (index === undefined || isEqual(alert.actions[index][key], value)) { return state; } else { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx index 79720edc4672e..421f0fc26dd68 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx @@ -5,6 +5,12 @@ */ import { lazy } from 'react'; import { suspendedComponentWithProps } from '../../lib/suspended_component_with_props'; +export { + AlertConditions, + ActionGroupWithCondition, + AlertConditionsProps, +} from './alert_conditions'; +export { AlertConditionsGroup } from './alert_conditions_group'; export const AlertAdd = suspendedComponentWithProps(lazy(() => import('./alert_add'))); export const AlertEdit = suspendedComponentWithProps(lazy(() => import('./alert_edit'))); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx index 677ee139271c0..490aeb5be8bd3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx @@ -6,6 +6,12 @@ import { lazy } from 'react'; import { suspendedComponentWithProps } from '../lib/suspended_component_with_props'; +export { + ActionGroupWithCondition, + AlertConditionsProps, + AlertConditions, + AlertConditionsGroup, +} from './alert_form'; export const AlertAdd = suspendedComponentWithProps(lazy(() => import('./alert_form/alert_add'))); export const AlertEdit = suspendedComponentWithProps(lazy(() => import('./alert_form/alert_edit'))); diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index c479359ff7e6e..025741aa7f9bd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -9,7 +9,12 @@ import { Plugin } from './plugin'; export { AlertsContextProvider, AlertsContextValue } from './application/context/alerts_context'; export { ActionsConnectorsContextProvider } from './application/context/actions_connectors_context'; export { AlertAdd } from './application/sections/alert_form'; -export { AlertEdit } from './application/sections'; +export { + AlertEdit, + AlertConditions, + AlertConditionsGroup, + ActionGroupWithCondition, +} from './application/sections'; export { ActionForm } from './application/sections/action_connector_form'; export { AlertAction, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 16c6bbc215ddc..cc0522eeb52a1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -6,7 +6,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import type { HttpSetup, DocLinksStart, ToastsSetup } from 'kibana/public'; import { ComponentType } from 'react'; -import { ActionGroup } from '../../alerts/common'; +import { ActionGroup, AlertActionParam } from '../../alerts/common'; import { ActionType } from '../../actions/common'; import { TypeRegistry } from './application/type_registry'; import { @@ -52,7 +52,7 @@ export interface ActionConnectorFieldsProps { export interface ActionParamsProps { actionParams: TParams; index: number; - editAction: (property: string, value: any, index: number) => void; + editAction: (key: string, value: AlertActionParam, index: number) => void; errors: IErrorObject; messageVariables?: ActionVariable[]; defaultMessage?: string; @@ -166,9 +166,11 @@ export interface AlertTypeParamsExpressionProps< alertInterval: string; alertThrottle: string; setAlertParams: (property: string, value: any) => void; - setAlertProperty: (key: string, value: any) => void; + setAlertProperty: (key: Key, value: Alert[Key] | null) => void; errors: IErrorObject; alertsContext: AlertsContextValue; + defaultActionGroupId: string; + actionGroups: ActionGroup[]; } export interface AlertTypeModel { diff --git a/x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx b/x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx index 35335f9868978..5195eef6e9a3b 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx @@ -27,6 +27,7 @@ import { ChartEmptyState } from './chart_empty_state'; import { DurationAnomaliesBar } from './duration_line_bar_list'; import { AnomalyRecords } from '../../../state/actions'; import { UptimeThemeContext } from '../../../contexts'; +import { MONITOR_CHART_HEIGHT } from '../../monitor'; interface DurationChartProps { /** @@ -86,7 +87,7 @@ export const DurationChartComponent = ({ }; return ( - + {hasLines ? ( { return ( @@ -20,7 +21,7 @@ export const MonitorCharts = ({ monitorId }: MonitorChartsProps) => { - + ); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index 41f6b66c30aaf..cf7fc9edd9529 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -91,6 +91,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { }); expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0); expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0); + expect(Date.parse(response.body.updatedAt)).to.eql(Date.parse(response.body.createdAt)); expect(typeof response.body.scheduledTaskId).to.be('string'); const { _source: taskRecord } = await getScheduledTask(response.body.scheduledTaskId); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/execution_status.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/execution_status.ts index 5ebce8edf6fb7..642173a7c2c6c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/execution_status.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/execution_status.ts @@ -63,6 +63,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon ); expect(response.status).to.eql(200); const alertId = response.body.id; + const alertUpdatedAt = response.body.updatedAt; dates.push(response.body.executionStatus.lastExecutionDate); objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts'); @@ -70,6 +71,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon dates.push(executionStatus.lastExecutionDate); dates.push(Date.now()); ensureDatetimesAreOrdered(dates); + ensureAlertUpdatedAtHasNotChanged(alertId, alertUpdatedAt); // Ensure AAD isn't broken await checkAAD({ @@ -97,6 +99,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon ); expect(response.status).to.eql(200); const alertId = response.body.id; + const alertUpdatedAt = response.body.updatedAt; dates.push(response.body.executionStatus.lastExecutionDate); objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts'); @@ -104,6 +107,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon dates.push(executionStatus.lastExecutionDate); dates.push(Date.now()); ensureDatetimesAreOrdered(dates); + ensureAlertUpdatedAtHasNotChanged(alertId, alertUpdatedAt); // Ensure AAD isn't broken await checkAAD({ @@ -128,6 +132,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon ); expect(response.status).to.eql(200); const alertId = response.body.id; + const alertUpdatedAt = response.body.updatedAt; dates.push(response.body.executionStatus.lastExecutionDate); objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts'); @@ -135,6 +140,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon dates.push(executionStatus.lastExecutionDate); dates.push(Date.now()); ensureDatetimesAreOrdered(dates); + ensureAlertUpdatedAtHasNotChanged(alertId, alertUpdatedAt); // Ensure AAD isn't broken await checkAAD({ @@ -162,12 +168,14 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon ); expect(response.status).to.eql(200); const alertId = response.body.id; + const alertUpdatedAt = response.body.updatedAt; objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts'); const executionStatus = await waitForStatus(alertId, new Set(['error'])); expect(executionStatus.error).to.be.ok(); expect(executionStatus.error.reason).to.be('execute'); expect(executionStatus.error.message).to.be('this alert is intended to fail'); + ensureAlertUpdatedAtHasNotChanged(alertId, alertUpdatedAt); }); it('should eventually have error reason "unknown" when appropriate', async () => { @@ -183,6 +191,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon ); expect(response.status).to.eql(200); const alertId = response.body.id; + const alertUpdatedAt = response.body.updatedAt; objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts'); let executionStatus = await waitForStatus(alertId, new Set(['ok'])); @@ -201,6 +210,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon executionStatus = await waitForStatus(alertId, new Set(['error'])); expect(executionStatus.error).to.be.ok(); expect(executionStatus.error.reason).to.be('unknown'); + ensureAlertUpdatedAtHasNotChanged(alertId, alertUpdatedAt); const message = 'params invalid: [param1]: expected value of type [string] but got [number]'; expect(executionStatus.error.message).to.be(message); @@ -306,6 +316,18 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon await delay(WaitForStatusIncrement); return await waitForStatus(id, statuses, waitMillis - WaitForStatusIncrement); } + + async function ensureAlertUpdatedAtHasNotChanged(alertId: string, originalUpdatedAt: string) { + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${alertId}` + ); + const { updatedAt, executionStatus } = response.body; + expect(Date.parse(updatedAt)).to.be.greaterThan(0); + expect(Date.parse(updatedAt)).to.eql(Date.parse(originalUpdatedAt)); + expect(Date.parse(executionStatus.lastExecutionDate)).to.be.greaterThan( + Date.parse(originalUpdatedAt) + ); + } } function expectErrorExecutionStatus(executionStatus: Record, startDate: number) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts index 17070a14069ce..bd6afacf206d9 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts @@ -82,5 +82,14 @@ export default function createGetTests({ getService }: FtrProviderContext) { }, ]); }); + + it('7.11.0 migrates alerts to contain `updatedAt` field', async () => { + const response = await supertest.get( + `${getUrlPrefix(``)}/api/alerts/alert/74f3e6d7-b7bb-477d-ac28-92ee22728e6e` + ); + + expect(response.status).to.eql(200); + expect(response.body.updatedAt).to.eql('2020-06-17T15:35:39.839Z'); + }); }); } diff --git a/x-pack/test/api_integration/services/usage_api.ts b/x-pack/test/api_integration/services/usage_api.ts index c56de5127f743..b4adc6c61b664 100644 --- a/x-pack/test/api_integration/services/usage_api.ts +++ b/x-pack/test/api_integration/services/usage_api.ts @@ -40,7 +40,7 @@ export function UsageAPIProvider({ getService }: FtrProviderContext) { async getTelemetryStats(payload: { unencrypted?: boolean; timestamp: number | string; - }): Promise { + }): Promise> { const { body } = await supertest .post('/api/telemetry/v2/clusters/_stats') .set('kbn-xsrf', 'xxx') diff --git a/x-pack/test/apm_api_integration/basic/tests/correlations/ranges.ts b/x-pack/test/apm_api_integration/basic/tests/correlations/ranges.ts deleted file mode 100644 index 751ee8753c449..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/correlations/ranges.ts +++ /dev/null @@ -1,95 +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 { format } from 'url'; -import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; -import archives_metadata from '../../../common/archives_metadata'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - const archiveName = 'apm_8.0.0'; - const range = archives_metadata[archiveName]; - - // url parameters - const start = '2020-09-29T14:45:00.000Z'; - const end = range.end; - const fieldNames = - 'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,url.domain,container.id,service.node.name'; - - describe('Ranges', () => { - const url = format({ - pathname: `/api/apm/correlations/ranges`, - query: { start, end, fieldNames }, - }); - - describe('when data is not loaded ', () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - expect(response.status).to.be(200); - expect(response.body.response).to.be(undefined); - }); - }); - - describe('when data is loaded', () => { - let response: PromiseReturnType; - before(async () => { - await esArchiver.load(archiveName); - response = await supertest.get(url); - }); - - after(() => esArchiver.unload(archiveName)); - - it('returns successfully', () => { - expect(response.status).to.eql(200); - }); - - it('returns fields in response', () => { - expectSnapshot(Object.keys(response.body.response)).toMatchInline(` - Array [ - "service.node.name", - "host.ip", - "user.id", - "user_agent.name", - "container.id", - "url.domain", - ] - `); - }); - - it('returns cardinality for each field', () => { - const cardinalitys = Object.values(response.body.response).map( - (field: any) => field.cardinality - ); - - expectSnapshot(cardinalitys).toMatchInline(` - Array [ - 5, - 6, - 20, - 6, - 5, - 4, - ] - `); - }); - - it('returns buckets', () => { - const { buckets } = response.body.response['user.id'].value; - expectSnapshot(buckets[0]).toMatchInline(` - Object { - "bg_count": 2, - "doc_count": 7, - "key": "20", - "score": 3.5, - } - `); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts b/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts deleted file mode 100644 index 3cf1c2cecb42b..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts +++ /dev/null @@ -1,115 +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 { format } from 'url'; -import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; -import archives_metadata from '../../../common/archives_metadata'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - const archiveName = 'apm_8.0.0'; - const range = archives_metadata[archiveName]; - - // url parameters - const start = range.start; - const end = range.end; - const durationPercentile = 95; - const fieldNames = - 'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,url.domain,container.id,service.node.name'; - - // Failing: See https://github.com/elastic/kibana/issues/81264 - describe('Slow durations', () => { - const url = format({ - pathname: `/api/apm/correlations/slow_durations`, - query: { start, end, durationPercentile, fieldNames }, - }); - - describe('when data is not loaded ', () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect(response.body.response).to.be(undefined); - }); - }); - - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - describe('making request with default args', () => { - let response: PromiseReturnType; - before(async () => { - response = await supertest.get(url); - - it('returns successfully', () => { - expect(response.status).to.eql(200); - }); - - it('returns fields in response', () => { - expectSnapshot(Object.keys(response.body.response)).toMatchInline(` - Array [ - "service.node.name", - "host.ip", - "user.id", - "user_agent.name", - "container.id", - "url.domain", - ] - `); - }); - - it('returns cardinality for each field', () => { - const cardinalitys = Object.values(response.body.response).map( - (field: any) => field.cardinality - ); - - expectSnapshot(cardinalitys).toMatchInline(` - Array [ - 5, - 6, - 3, - 5, - 5, - 4, - ] - `); - }); - - it('returns buckets', () => { - const { buckets } = response.body.response['user.id'].value; - expectSnapshot(buckets[0]).toMatchInline(` - Object { - "bg_count": 32, - "doc_count": 6, - "key": "2", - "score": 0.1875, - } - `); - }); - }); - }); - - describe('making a request for each "scoring"', () => { - ['percentage', 'jlh', 'chi_square', 'gnd'].map(async (scoring) => { - it(`returns response for scoring "${scoring}"`, async () => { - const response = await supertest.get( - format({ - pathname: `/api/apm/correlations/slow_durations`, - query: { start, end, durationPercentile, fieldNames, scoring }, - }) - ); - - expect(response.status).to.be(200); - }); - }); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/basic/tests/correlations/slow_transactions.ts b/x-pack/test/apm_api_integration/basic/tests/correlations/slow_transactions.ts new file mode 100644 index 0000000000000..c0978db69a3c9 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/correlations/slow_transactions.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { format } from 'url'; +import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/createCallApmApi'; +import archives_metadata from '../../../common/archives_metadata'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const archiveName = 'apm_8.0.0'; + const range = archives_metadata[archiveName]; + + describe('Slow durations', () => { + const url = format({ + pathname: `/api/apm/correlations/slow_transactions`, + query: { + start: range.start, + end: range.end, + durationPercentile: 95, + fieldNames: + 'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,url.domain,container.id,service.node.name', + }, + }); + + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + expect(response.body.response).to.be(undefined); + }); + }); + + describe('when data is loaded', () => { + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); + + describe('making request with default args', () => { + type ResponseBody = APIReturnType<'GET /api/apm/correlations/slow_transactions'>; + let response: { + status: number; + body: NonNullable; + }; + + before(async () => { + response = await supertest.get(url); + }); + + it('returns successfully', () => { + expect(response.status).to.eql(200); + }); + + it('returns significant terms', () => { + expectSnapshot(response.body?.significantTerms?.map((term) => term.fieldName)) + .toMatchInline(` + Array [ + "host.ip", + "service.node.name", + "container.id", + "url.domain", + "user_agent.name", + "user.id", + "host.ip", + "service.node.name", + "container.id", + "user.id", + ] + `); + }); + + it('returns a timeseries per term', () => { + // @ts-ignore + expectSnapshot(response.body?.significantTerms[0].timeseries.length).toMatchInline(`31`); + }); + + it('returns a distribution per term', () => { + // @ts-ignore + expectSnapshot(response.body?.significantTerms[0].distribution.length).toMatchInline( + `11` + ); + }); + + it('returns overall timeseries', () => { + // @ts-ignore + expectSnapshot(response.body?.overall.timeseries.length).toMatchInline(`31`); + }); + + it('returns overall distribution', () => { + // @ts-ignore + expectSnapshot(response.body?.overall.distribution.length).toMatchInline(`11`); + }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts index 0381e5f51bb9b..e9bc59df96108 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -24,6 +24,7 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont describe('Service overview', function () { loadTestFile(require.resolve('./service_overview/error_groups')); + loadTestFile(require.resolve('./service_overview/transaction_groups')); }); describe('Settings', function () { @@ -59,8 +60,7 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont }); describe('Correlations', function () { - loadTestFile(require.resolve('./correlations/slow_durations')); - loadTestFile(require.resolve('./correlations/ranges')); + loadTestFile(require.resolve('./correlations/slow_transactions')); }); }); } diff --git a/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts b/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts index 088b7cb8bb568..6d0d1e4b52bec 100644 --- a/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts +++ b/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts @@ -99,9 +99,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { } `); - expectSnapshot( - firstItem.occurrences.timeseries.filter(({ y }: any) => y > 0).length - ).toMatchInline(`7`); + const visibleDataPoints = firstItem.occurrences.timeseries.filter(({ y }: any) => y > 0); + expectSnapshot(visibleDataPoints.length).toMatchInline(`7`); }); it('sorts items in the correct order', async () => { diff --git a/x-pack/test/apm_api_integration/basic/tests/service_overview/transaction_groups.ts b/x-pack/test/apm_api_integration/basic/tests/service_overview/transaction_groups.ts new file mode 100644 index 0000000000000..f9ae8cc9a1976 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/service_overview/transaction_groups.ts @@ -0,0 +1,255 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { pick, uniqBy } from 'lodash'; +import url from 'url'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import archives from '../../../common/archives_metadata'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const archiveName = 'apm_8.0.0'; + const { start, end } = archives[archiveName]; + + describe('Service overview transaction groups', () => { + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/overview_transaction_groups`, + query: { + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'impact', + }, + }) + ); + + expect(response.status).to.be(200); + expect(response.body).to.eql({ + totalTransactionGroups: 0, + transactionGroups: [], + isAggregationAccurate: true, + }); + }); + }); + + describe('when data is loaded', () => { + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); + + it('returns the correct data', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/overview_transaction_groups`, + query: { + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'impact', + }, + }) + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body.totalTransactionGroups).toMatchInline(`12`); + + expectSnapshot(response.body.transactionGroups.map((group: any) => group.name)) + .toMatchInline(` + Array [ + "DispatcherServlet#doGet", + "APIRestController#stats", + "APIRestController#topProducts", + "APIRestController#order", + "APIRestController#customer", + ] + `); + + expectSnapshot(response.body.transactionGroups.map((group: any) => group.impact)) + .toMatchInline(` + Array [ + 100, + 0.794579770440557, + 0.298214689777379, + 0.290932594821871, + 0.270655974123907, + ] + `); + + const firstItem = response.body.transactionGroups[0]; + + expectSnapshot( + pick(firstItem, 'name', 'latency.value', 'throughput.value', 'errorRate.value', 'impact') + ).toMatchInline(` + Object { + "errorRate": Object { + "value": 0.107142857142857, + }, + "impact": 100, + "latency": Object { + "value": 996636.214285714, + }, + "name": "DispatcherServlet#doGet", + "throughput": Object { + "value": 28, + }, + } + `); + + expectSnapshot( + firstItem.latency.timeseries.filter(({ y }: any) => y > 0).length + ).toMatchInline(`15`); + + expectSnapshot( + firstItem.throughput.timeseries.filter(({ y }: any) => y > 0).length + ).toMatchInline(`15`); + + expectSnapshot( + firstItem.errorRate.timeseries.filter(({ y }: any) => y > 0).length + ).toMatchInline(`3`); + }); + + it('sorts items in the correct order', async () => { + const descendingResponse = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/overview_transaction_groups`, + query: { + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'impact', + }, + }) + ); + + expect(descendingResponse.status).to.be(200); + + const descendingOccurrences = descendingResponse.body.transactionGroups.map( + (item: any) => item.impact + ); + + expect(descendingOccurrences).to.eql(descendingOccurrences.concat().sort().reverse()); + + const ascendingResponse = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/overview_transaction_groups`, + query: { + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'impact', + }, + }) + ); + + const ascendingOccurrences = ascendingResponse.body.transactionGroups.map( + (item: any) => item.impact + ); + + expect(ascendingOccurrences).to.eql(ascendingOccurrences.concat().sort().reverse()); + }); + + it('sorts items by the correct field', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/overview_transaction_groups`, + query: { + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'latency', + }, + }) + ); + + expect(response.status).to.be(200); + + const latencies = response.body.transactionGroups.map((group: any) => group.latency.value); + + expect(latencies).to.eql(latencies.concat().sort().reverse()); + }); + + it('paginates through the items', async () => { + const size = 1; + + const firstPage = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/overview_transaction_groups`, + query: { + start, + end, + uiFilters: '{}', + size, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'impact', + }, + }) + ); + + expect(firstPage.status).to.eql(200); + + const totalItems = firstPage.body.totalTransactionGroups; + + const pages = Math.floor(totalItems / size); + + const items = await new Array(pages) + .fill(undefined) + .reduce(async (prevItemsPromise, _, pageIndex) => { + const prevItems = await prevItemsPromise; + + const thisPage = await supertest.get( + url.format({ + pathname: '/api/apm/services/opbeans-java/overview_transaction_groups', + query: { + start, + end, + uiFilters: '{}', + size, + numBuckets: 20, + pageIndex, + sortDirection: 'desc', + sortField: 'impact', + }, + }) + ); + + return prevItems.concat(thisPage.body.transactionGroups); + }, Promise.resolve([])); + + expect(items.length).to.eql(totalItems); + + expect(uniqBy(items, 'name').length).to.eql(totalItems); + }); + }); + }); +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/roles_users_utils/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/roles_users_utils/index.ts new file mode 100644 index 0000000000000..5098ff157b116 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/roles_users_utils/index.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 t1AnalystUser from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_user.json'; +import * as t2AnalystUser from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_user.json'; +import * as hunterUser from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_user.json'; +import * as ruleAuthorUser from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_user.json'; +import * as socManagerUser from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_user.json'; +import * as platformEngineerUser from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_user.json'; +import * as detectionsAdminUser from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_user.json'; + +import * as t1AnalystRole from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json'; +import * as t2AnalystRole from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json'; +import * as hunterRole from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json'; +import * as ruleAuthorRole from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json'; +import * as socManagerRole from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json'; +import * as platformEngineerRole from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json'; +import * as detectionsAdminRole from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json'; + +import { ROLES } from '../../../../plugins/security_solution/common/test'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +export const createUserAndRole = async ( + securityService: ReturnType, + role: keyof typeof ROLES +) => { + switch (role) { + case ROLES.detections_admin: + await postRoleAndUser( + ROLES.detections_admin, + detectionsAdminRole, + detectionsAdminUser, + securityService + ); + break; + case ROLES.t1_analyst: + await postRoleAndUser(ROLES.t1_analyst, t1AnalystRole, t1AnalystUser, securityService); + break; + case ROLES.t2_analyst: + await postRoleAndUser(ROLES.t2_analyst, t2AnalystRole, t2AnalystUser, securityService); + break; + case ROLES.hunter: + await postRoleAndUser(ROLES.hunter, hunterRole, hunterUser, securityService); + break; + case ROLES.rule_author: + await postRoleAndUser(ROLES.rule_author, ruleAuthorRole, ruleAuthorUser, securityService); + break; + case ROLES.soc_manager: + await postRoleAndUser(ROLES.soc_manager, socManagerRole, socManagerUser, securityService); + break; + case ROLES.platform_engineer: + await postRoleAndUser( + ROLES.platform_engineer, + platformEngineerRole, + platformEngineerUser, + securityService + ); + break; + default: + break; + } +}; + +interface UserInterface { + password: string; + roles: string[]; + full_name: string; + email: string; +} + +interface RoleInterface { + elasticsearch: { + cluster: string[]; + indices: Array<{ + names: string[]; + privileges: string[]; + }>; + }; + kibana: Array<{ + feature: { + ml: string[]; + siem: string[]; + actions: string[]; + builtInAlerts: string[]; + savedObjectsManagement: string[]; + }; + spaces: string[]; + }>; +} + +export const postRoleAndUser = async ( + roleName: string, + role: RoleInterface, + user: UserInterface, + securityService: ReturnType +) => { + await securityService.role.create(roleName, { + kibana: role.kibana, + elasticsearch: role.elasticsearch, + }); + await securityService.user.create(roleName, { + password: 'changeme', + full_name: user.full_name, + roles: user.roles, + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts index d2a3e86526db4..bbc3943b75955 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts @@ -25,12 +25,16 @@ import { waitForSignalsToBePresent, getAllSignals, } from '../../utils'; +import { createUserAndRole } from '../roles_users_utils'; +import { ROLES } from '../../../../plugins/security_solution/common/test'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const es = getService('es'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const securityService = getService('security'); describe('open_close_signals', () => { describe('validation checks', () => { @@ -157,6 +161,79 @@ export default ({ getService }: FtrProviderContext) => { ); expect(everySignalClosed).to.eql(true); }); + + it('should NOT be able to close signals with t1 analyst user', async () => { + const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; + await createRule(supertest, rule); + await waitForSignalsToBePresent(supertest); + await createUserAndRole(securityService, ROLES.t1_analyst); + const signalsOpen = await getAllSignals(supertest); + const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); + + // Try to set all of the signals to the state of closed. + // This should not be possible with the given user. + await supertestWithoutAuth + .post(DETECTION_ENGINE_SIGNALS_STATUS_URL) + .set('kbn-xsrf', 'true') + .auth(ROLES.t1_analyst, 'changeme') + .send(setSignalStatus({ signalIds, status: 'closed' })) + .expect(403); + + // query for the signals with the superuser + // to allow a check that the signals were NOT closed with t1 analyst + const { + body: signalsClosed, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds(signalIds)) + .expect(200); + + const everySignalOpen = signalsClosed.hits.hits.every( + ({ + _source: { + signal: { status }, + }, + }) => status === 'open' + ); + expect(everySignalOpen).to.eql(true); + }); + + it('should be able to close signals with soc_manager user', async () => { + const rule = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: '*:*' }; + await createRule(supertest, rule); + await waitForSignalsToBePresent(supertest); + const userAndRole = ROLES.soc_manager; + await createUserAndRole(securityService, userAndRole); + const signalsOpen = await getAllSignals(supertest); + const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); + + // Try to set all of the signals to the state of closed. + // This should not be possible with the given user. + await supertestWithoutAuth + .post(DETECTION_ENGINE_SIGNALS_STATUS_URL) + .set('kbn-xsrf', 'true') + .auth(userAndRole, 'changeme') // each user has the same password + .send(setSignalStatus({ signalIds, status: 'closed' })) + .expect(200); + + const { + body: signalsClosed, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds(signalIds)) + .expect(200); + + const everySignalClosed = signalsClosed.hits.hits.every( + ({ + _source: { + signal: { status }, + }, + }) => status === 'closed' + ); + expect(everySignalClosed).to.eql(true); + }); }); }); }); diff --git a/x-pack/test/fleet_api_integration/apis/agents/actions.ts b/x-pack/test/fleet_api_integration/apis/agents/actions.ts index 01f69328388db..d97ac6f7daa6e 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/actions.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/actions.ts @@ -36,6 +36,39 @@ export default function (providerContext: FtrProviderContext) { expect(apiResponse.item.data).to.eql({ data: 'action_data' }); }); + it('should return a 200 if this a valid SETTINGS action request', async () => { + const { body: apiResponse } = await supertest + .post(`/api/fleet/agents/agent1/actions`) + .set('kbn-xsrf', 'xx') + .send({ + action: { + type: 'SETTINGS', + data: { log_level: 'debug' }, + }, + }) + .expect(200); + + expect(apiResponse.item.type).to.eql('SETTINGS'); + expect(apiResponse.item.data).to.eql({ log_level: 'debug' }); + }); + + it('should return a 400 if this a invalid SETTINGS action request', async () => { + const { body: apiResponse } = await supertest + .post(`/api/fleet/agents/agent1/actions`) + .set('kbn-xsrf', 'xx') + .send({ + action: { + type: 'SETTINGS', + data: { log_level: 'thisnotavalidloglevel' }, + }, + }) + .expect(400); + + expect(apiResponse.message).to.match( + /\[request body.action\.[0-9]*\.data\.log_level]: types that failed validation/ + ); + }); + it('should return a 400 when request does not have type information', async () => { const { body: apiResponse } = await supertest .post(`/api/fleet/agents/agent1/actions`) @@ -43,12 +76,11 @@ export default function (providerContext: FtrProviderContext) { .send({ action: { data: { data: 'action_data' }, - sent_at: '2020-03-18T19:45:02.620Z', }, }) .expect(400); - expect(apiResponse.message).to.eql( - '[request body.action.type]: expected at least one defined value but got [undefined]' + expect(apiResponse.message).to.match( + /\[request body.action\.[0-9]*\.type]: expected at least one defined value but got \[undefined]/ ); }); @@ -60,7 +92,6 @@ export default function (providerContext: FtrProviderContext) { action: { type: 'POLICY_CHANGE', data: { data: 'action_data' }, - sent_at: '2020-03-18T19:45:02.620Z', }, }) .expect(404); diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts index 12de29c4fde10..d44a373f43040 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts @@ -25,7 +25,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.preserveCrossAppState(); }); - it('should create dashboard to URL drilldown and use it to navigate to discover', async () => { + it.skip('should create dashboard to URL drilldown and use it to navigate to discover', async () => { await PageObjects.dashboard.gotoDashboardEditMode( dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME ); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 1d86d95b7a796..fdcb456493dab 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -9,6 +9,7 @@ import uuid from 'uuid'; import { omit, mapValues, range, flatten } from 'lodash'; import moment from 'moment'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { alwaysFiringAlertType } from '../../fixtures/plugins/alerts/server/plugin'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); @@ -306,8 +307,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/57426 - describe.skip('Alert Instances', function () { + describe('Alert Instances', function () { const testRunUuid = uuid.v4(); let alert: any; @@ -373,16 +373,31 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // refresh to ensure Api call and UI are looking at freshest output await browser.refresh(); + // Get action groups + const { actionGroups } = alwaysFiringAlertType; + // Verify content await testSubjects.existOrFail('alertInstancesList'); - const summary = await alerting.alerts.getAlertInstanceSummary(alert.id); + const actionGroupNameFromId = (actionGroupId: string) => + actionGroups.find( + (actionGroup: { id: string; name: string }) => actionGroup.id === actionGroupId + )?.name; + const summary = await alerting.alerts.getAlertInstanceSummary(alert.id); const dateOnAllInstancesFromApiResponse = mapValues( summary.instances, (instance) => instance.activeStartDate ); + const actionGroupNameOnAllInstancesFromApiResponse = mapValues( + summary.instances, + (instance) => { + const name = actionGroupNameFromId(instance.actionGroupId); + return name ? ` (${name})` : ''; + } + ); + log.debug( `API RESULT: ${Object.entries(dateOnAllInstancesFromApiResponse) .map(([id, date]) => `${id}: ${moment(date).utc()}`) @@ -393,21 +408,21 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(instancesList.map((instance) => omit(instance, 'duration'))).to.eql([ { instance: 'us-central', - status: 'Active (Default)', + status: `Active${actionGroupNameOnAllInstancesFromApiResponse['us-central']}`, start: moment(dateOnAllInstancesFromApiResponse['us-central']) .utc() .format('D MMM YYYY @ HH:mm:ss'), }, { instance: 'us-east', - status: 'Active (Default)', + status: `Active${actionGroupNameOnAllInstancesFromApiResponse['us-east']}`, start: moment(dateOnAllInstancesFromApiResponse['us-east']) .utc() .format('D MMM YYYY @ HH:mm:ss'), }, { instance: 'us-west', - status: 'Active (Default)', + status: `Active${actionGroupNameOnAllInstancesFromApiResponse['us-west']}`, start: moment(dateOnAllInstancesFromApiResponse['us-west']) .utc() .format('D MMM YYYY @ HH:mm:ss'), diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts index 6f9d010378624..6584c5891a8b9 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts @@ -17,11 +17,62 @@ export interface AlertingExampleDeps { features: FeaturesPluginSetup; } +export const noopAlertType: AlertType = { + id: 'test.noop', + name: 'Test: Noop', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + async executor() {}, + producer: 'alerts', +}; + +export const alwaysFiringAlertType: any = { + id: 'test.always-firing', + name: 'Always Firing', + actionGroups: [ + { id: 'default', name: 'Default' }, + { id: 'other', name: 'Other' }, + ], + defaultActionGroupId: 'default', + producer: 'alerts', + async executor(alertExecutorOptions: any) { + const { services, state, params } = alertExecutorOptions; + + (params.instances || []).forEach((instance: { id: string; state: any }) => { + services + .alertInstanceFactory(instance.id) + .replaceState({ instanceStateValue: true, ...(instance.state || {}) }) + .scheduleActions('default'); + }); + + return { + globalStateValue: true, + groupInSeriesIndex: (state.groupInSeriesIndex || 0) + 1, + }; + }, +}; + +export const failingAlertType: any = { + id: 'test.failing', + name: 'Test: Failing', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + producer: 'alerts', + defaultActionGroupId: 'default', + async executor() { + throw new Error('Failed to execute alert type'); + }, +}; + export class AlertingFixturePlugin implements Plugin { public setup(core: CoreSetup, { alerts, features }: AlertingExampleDeps) { - createNoopAlertType(alerts); - createAlwaysFiringAlertType(alerts); - createFailingAlertType(alerts); + alerts.registerType(noopAlertType); + alerts.registerType(alwaysFiringAlertType); + alerts.registerType(failingAlertType); features.registerKibanaFeature({ id: 'alerting_fixture', name: 'alerting_fixture', @@ -56,64 +107,3 @@ export class AlertingFixturePlugin implements Plugin { - services - .alertInstanceFactory(instance.id) - .replaceState({ instanceStateValue: true, ...(instance.state || {}) }) - .scheduleActions('default'); - }); - - return { - globalStateValue: true, - groupInSeriesIndex: (state.groupInSeriesIndex || 0) + 1, - }; - }, - }; - alerts.registerType(alwaysFiringAlertType); -} - -function createFailingAlertType(alerts: AlertingSetup) { - const failingAlertType: any = { - id: 'test.failing', - name: 'Test: Failing', - actionGroups: [ - { - id: 'default', - name: 'Default', - }, - ], - producer: 'alerts', - defaultActionGroupId: 'default', - async executor() { - throw new Error('Failed to execute alert type'); - }, - }; - alerts.registerType(failingAlertType); -} diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts index 942b352b4afd3..5ab07aa00412b 100644 --- a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts @@ -20,6 +20,7 @@ export interface AlertInstanceSummary { export interface AlertInstanceStatus { status: string; muted: boolean; + actionGroupId: string; activeStartDate?: string; } diff --git a/x-pack/test/reporting_api_integration/fixtures.ts b/x-pack/test/reporting_api_integration/fixtures.ts index 6d76a158acf1d..afd6ea5582acf 100644 --- a/x-pack/test/reporting_api_integration/fixtures.ts +++ b/x-pack/test/reporting_api_integration/fixtures.ts @@ -251,17 +251,18 @@ export const CSV_RESULT_NANOS_CUSTOM = `date,message,"_id" `; export const CSV_RESULT_DOCVALUE = `"order_date",category,currency,"customer_id","order_id","day_of_week_i","order_date","products.created_on",sku -"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes""]",EUR,12,570552,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0216402164"",""ZO0666306663""]" -"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing""]",EUR,34,570520,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0618906189"",""ZO0289502895""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing""]",EUR,42,570569,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0643506435"",""ZO0646406464""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Accessories"",""Women's Clothing""]",EUR,45,570133,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0320503205"",""ZO0049500495""]" -"Jun 26, 2019 @ 00:00:00.000","[""Men's Accessories""]",EUR,4,570161,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0606606066"",""ZO0596305963""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,17,570200,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0025100251"",""ZO0101901019""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Shoes""]",EUR,27,732050,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0101201012"",""ZO0230902309"",""ZO0325603256"",""ZO0056400564""]" -"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing"",""Men's Shoes""]",EUR,52,719675,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0448604486"",""ZO0686206862"",""ZO0395403954"",""ZO0528505285""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Accessories""]",EUR,26,570396,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0495604956"",""ZO0208802088""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Accessories""]",EUR,17,570037,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0321503215"",""ZO0200102001""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,26,569309,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0364103641"",""ZO0708807088""]" "Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,24,569311,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0024600246"",""ZO0660706607""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing"",""Men's Shoes""]",EUR,31,569312,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0425104251"",""ZO0107901079""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Shoes""]",EUR,14,569336,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0512505125"",""ZO0384103841""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing""]",EUR,28,569337,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0634106341"",""ZO0066900669""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Accessories"",""Men's Clothing""]",EUR,31,569338,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0702507025"",""ZO0528105281""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,27,569356,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0010500105"",""ZO0172201722""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing"",""Men's Shoes""]",EUR,19,569362,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0292402924"",""ZO0681006810""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Accessories"",""Women's Clothing""]",EUR,42,569370,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0358603586"",""ZO0641106411""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Accessories""]",EUR,20,569371,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0225702257"",""ZO0186601866""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Shoes""]",EUR,43,569375,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0347603476"",""ZO0668806688""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing""]",EUR,48,569387,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0593805938"",""ZO0125201252""]" `; // This concatenates lines of multi-line string into a single line. diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/csv_saved_search.ts b/x-pack/test/reporting_api_integration/reporting_and_security/csv_saved_search.ts index ca3172807139c..20df601f2ff5c 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/csv_saved_search.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/csv_saved_search.ts @@ -355,7 +355,10 @@ export default function ({ getService }: FtrProviderContext) { timezone: 'UTC', }, state: { - sort: [{ order_date: { order: 'desc', unmapped_type: 'boolean' } }], + sort: [ + { order_date: { order: 'desc', unmapped_type: 'boolean' } }, + { order_id: { order: 'asc', unmapped_type: 'boolean' } }, + ], docvalue_fields: [ { field: 'customer_birth_date', format: 'date_time' }, { field: 'order_date', format: 'date_time' }, diff --git a/x-pack/test/reporting_api_integration/services.ts b/x-pack/test/reporting_api_integration/services.ts index 2c0252fde7693..3b908ecdd2b6e 100644 --- a/x-pack/test/reporting_api_integration/services.ts +++ b/x-pack/test/reporting_api_integration/services.ts @@ -5,8 +5,6 @@ */ import expect from '@kbn/expect'; -import * as Rx from 'rxjs'; -import { filter, first, mapTo, switchMap, timeout } from 'rxjs/operators'; import { indexTimestamp } from '../../plugins/reporting/server/lib/store/index_timestamp'; import { services as xpackServices } from '../functional/services'; import { services as apiIntegrationServices } from '../api_integration/services'; @@ -47,6 +45,7 @@ export function ReportingAPIProvider({ getService }: FtrProviderContext) { const log = getService('log'); const supertest = getService('supertest'); const esSupertest = getService('esSupertest'); + const retry = getService('retry'); return { async waitForJobToFinish(downloadReportPath: string) { @@ -139,21 +138,12 @@ export function ReportingAPIProvider({ getService }: FtrProviderContext) { log.debug('ReportingAPI.deleteAllReports'); // ignores 409 errs and keeps retrying - const deleted$ = Rx.interval(100).pipe( - switchMap(() => - esSupertest - .post('/.reporting*/_delete_by_query') - .send({ query: { match_all: {} } }) - .then(({ status }) => status) - ), - filter((status) => status === 200), - mapTo(true), - first(), - timeout(5000) - ); - - const reportsDeleted = await deleted$.toPromise(); - expect(reportsDeleted).to.be(true); + await retry.tryForTime(5000, async () => { + await esSupertest + .post('/.reporting*/_delete_by_query') + .send({ query: { match_all: {} } }) + .expect(200); + }); }, expectRecentPdfAppStats(stats: UsageStats, app: string, count: number) { diff --git a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/index.ts b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/index.ts index d78513ca06206..6bacd5a625a15 100644 --- a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/index.ts +++ b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/index.ts @@ -14,5 +14,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./update')); + loadTestFile(require.resolve('./usage_collection')); }); } diff --git a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/usage_collection.ts b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/usage_collection.ts new file mode 100644 index 0000000000000..8804c2cd2ad59 --- /dev/null +++ b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/usage_collection.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../services'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const usageAPI = getService('usageAPI'); + + describe('saved_object_tagging usage collector data', () => { + beforeEach(async () => { + await esArchiver.load('usage_collection'); + }); + + afterEach(async () => { + await esArchiver.unload('usage_collection'); + }); + + /* + * Dataset description: + * + * 5 tags: tag-1 tag-2 tag-3 tag-4 ununsed-tag + * 3 dashboard: + * - dash-1: ref to tag-1 + tag-2 + * - dash-2: ref to tag-2 + tag 4 + * - dash-3: no ref to any tag + * 3 visualization: + * - vis-1: ref to tag-1 + * - vis-2: ref to tag-1 + tag-3 + * - vis-3: ref to tag-3 + */ + it('collects the expected data', async () => { + const telemetryStats = (await usageAPI.getTelemetryStats({ + unencrypted: true, + timestamp: Date.now(), + })) as any; + + const taggingStats = telemetryStats[0].stack_stats.kibana.plugins.saved_objects_tagging; + expect(taggingStats).to.eql({ + usedTags: 4, + taggedObjects: 5, + types: { + dashboard: { + taggedObjects: 2, + usedTags: 3, + }, + visualization: { + taggedObjects: 3, + usedTags: 2, + }, + }, + }); + }); + }); +} diff --git a/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/usage_collection/data.json b/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/usage_collection/data.json new file mode 100644 index 0000000000000..a9535ae9e40b2 --- /dev/null +++ b/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/usage_collection/data.json @@ -0,0 +1,313 @@ +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana", + "source": { + "space": { + "_reserved": true, + "description": "This is the default space", + "name": "Default Space" + }, + "type": "space", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "tag:tag-1", + "index": ".kibana", + "source": { + "tag": { + "name": "tag-1", + "description": "My first tag!", + "color": "#FF00FF" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "tag:tag-2", + "index": ".kibana", + "source": { + "tag": { + "name": "tag-2", + "description": "Another awesome tag", + "color": "#FFFFFF" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "tag:tag-3", + "index": ".kibana", + "source": { + "tag": { + "name": "tag-3", + "description": "Last but not least", + "color": "#000000" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "tag:tag-4", + "index": ".kibana", + "source": { + "tag": { + "name": "tag-4", + "description": "Last", + "color": "#000000" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "tag:unused-tag", + "index": ".kibana", + "source": { + "tag": { + "name": "unused-tag", + "description": "This tag is unused and should only appear in totalTags", + "color": "#123456" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "visualization:ref-to-tag-1", + "source": { + "type": "visualization", + "updated_at": "2017-09-21T18:51:23.794Z", + "visualization": { + "title": "Vis with ref to tag-1", + "visState": "{}", + "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + } + }, + "references": [ + { + "type": "tag", + "id": "tag-1", + "name": "tag-1" + } + ] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "visualization:ref-to-tag-1-and-tag-3", + "source": { + "type": "visualization", + "updated_at": "2017-09-21T18:51:23.794Z", + "visualization": { + "title": "Vis with ref to tag-1 and tag-2", + "visState": "{}", + "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + } + }, + "references": [ + { + "type": "tag", + "id": "tag-1", + "name": "tag-1" + }, + { + "type": "tag", + "id": "tag-3", + "name": "tag-3" + } + ] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "visualization:ref-to-tag-3", + "source": { + "type": "visualization", + "updated_at": "2017-09-21T18:51:23.794Z", + "visualization": { + "title": "Vis with ref to tag-2", + "visState": "{}", + "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + } + }, + "references": [ + { + "type": "tag", + "id": "tag-3", + "name": "tag-3" + } + ] + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:ref-to-tag-1-and-tag-2", + "index": ".kibana", + "source": { + "dashboard": { + "title": "dashboard 1 (tag-2)", + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"animal\",\"value\":\"dog\",\"params\":{\"query\":\"dog\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"animal\":{\"query\":\"dog\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeFrom": "Mon Apr 09 2018 17:56:08 GMT-0400", + "timeRestore": true, + "timeTo": "Wed Apr 11 2018 17:56:08 GMT-0400", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "tag-1", + "name": "tag-1-ref", + "type": "tag" + }, + { + "id": "tag-2", + "name": "tag-2-ref", + "type": "tag" + } + ], + "type": "dashboard", + "updated_at": "2018-04-11T21:57:52.253Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:ref-to-tag-2-and-tag-4", + "index": ".kibana", + "source": { + "dashboard": { + "title": "dashboard 2 (tag-2 and tag-4)", + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"animal\",\"value\":\"dog\",\"params\":{\"query\":\"dog\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"animal\":{\"query\":\"dog\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeFrom": "Mon Apr 09 2018 17:56:08 GMT-0400", + "timeRestore": true, + "timeTo": "Wed Apr 11 2018 17:56:08 GMT-0400", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "tag-2", + "name": "tag-2-ref", + "type": "tag" + }, + { + "id": "tag-4", + "name": "tag-4-ref", + "type": "tag" + } + ], + "type": "dashboard", + "updated_at": "2018-04-11T21:57:52.253Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:no-tag-reference", + "index": ".kibana", + "source": { + "dashboard": { + "title": "dashboard 2 (tag-2 and tag-4)", + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"animal\",\"value\":\"dog\",\"params\":{\"query\":\"dog\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"animal\":{\"query\":\"dog\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeFrom": "Mon Apr 09 2018 17:56:08 GMT-0400", + "timeRestore": true, + "timeTo": "Wed Apr 11 2018 17:56:08 GMT-0400", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T21:57:52.253Z" + } + } +} + diff --git a/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/usage_collection/mappings.json b/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/usage_collection/mappings.json new file mode 100644 index 0000000000000..9cf628bef4767 --- /dev/null +++ b/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/usage_collection/mappings.json @@ -0,0 +1,266 @@ +{ + "type": "index", + "value": { + "aliases": {}, + "index": ".kibana", + "mappings": { + "dynamic": "strict", + "properties": { + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "defaultIndex": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "dashboard": { + "dynamic": "strict", + "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" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "tag": { + "properties": { + "name": { + "type": "text" + }, + "description": { + "type": "text" + }, + "color": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "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/security_solution_cypress/es_archives/custom_rules/data.json.gz b/x-pack/test/security_solution_cypress/es_archives/custom_rules/data.json.gz index 4a8fdf53fa9a1..fb262155ea03a 100644 Binary files a/x-pack/test/security_solution_cypress/es_archives/custom_rules/data.json.gz and b/x-pack/test/security_solution_cypress/es_archives/custom_rules/data.json.gz differ diff --git a/x-pack/test/security_solution_cypress/es_archives/custom_rules/mappings.json b/x-pack/test/security_solution_cypress/es_archives/custom_rules/mappings.json index 5869964991ba7..d416926a40fa6 100644 --- a/x-pack/test/security_solution_cypress/es_archives/custom_rules/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/custom_rules/mappings.json @@ -321,6 +321,9 @@ "throttle": { "type": "keyword" }, + "updatedAt": { + "type": "date" + }, "updatedBy": { "type": "keyword" }, diff --git a/x-pack/test/security_solution_cypress/es_archives/export_rule/data.json.gz b/x-pack/test/security_solution_cypress/es_archives/export_rule/data.json.gz index aad07a0bf6d53..c9739a7725293 100644 Binary files a/x-pack/test/security_solution_cypress/es_archives/export_rule/data.json.gz and b/x-pack/test/security_solution_cypress/es_archives/export_rule/data.json.gz differ diff --git a/x-pack/test/security_solution_cypress/es_archives/export_rule/mappings.json b/x-pack/test/security_solution_cypress/es_archives/export_rule/mappings.json index 5eec03ca3d11a..757121df53d44 100644 --- a/x-pack/test/security_solution_cypress/es_archives/export_rule/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/export_rule/mappings.json @@ -191,6 +191,9 @@ "throttle": { "type": "keyword" }, + "updatedAt": { + "type": "date" + }, "updatedBy": { "type": "keyword" } diff --git a/x-pack/test/security_solution_cypress/es_archives/prebuilt_rules_loaded/data.json.gz b/x-pack/test/security_solution_cypress/es_archives/prebuilt_rules_loaded/data.json.gz index cac63ed9c585f..0bec997503146 100644 Binary files a/x-pack/test/security_solution_cypress/es_archives/prebuilt_rules_loaded/data.json.gz and b/x-pack/test/security_solution_cypress/es_archives/prebuilt_rules_loaded/data.json.gz differ diff --git a/x-pack/test/security_solution_cypress/es_archives/prebuilt_rules_loaded/mappings.json b/x-pack/test/security_solution_cypress/es_archives/prebuilt_rules_loaded/mappings.json index f4278c4d4318f..7ef00495390ee 100644 --- a/x-pack/test/security_solution_cypress/es_archives/prebuilt_rules_loaded/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/prebuilt_rules_loaded/mappings.json @@ -322,6 +322,9 @@ "throttle": { "type": "keyword" }, + "updatedAt": { + "type": "date" + }, "updatedBy": { "type": "keyword" } diff --git a/x-pack/test/security_solution_cypress/runner.ts b/x-pack/test/security_solution_cypress/runner.ts index ccdc2fa4424ac..a1a1a3916ef7f 100644 --- a/x-pack/test/security_solution_cypress/runner.ts +++ b/x-pack/test/security_solution_cypress/runner.ts @@ -28,9 +28,20 @@ export async function SecuritySolutionCypressCliTestRunner({ getService }: FtrPr FORCE_COLOR: '1', // eslint-disable-next-line @typescript-eslint/naming-convention CYPRESS_baseUrl: Url.format(config.get('servers.kibana')), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_protocol: config.get('servers.kibana.protocol'), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_hostname: config.get('servers.kibana.hostname'), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_configport: config.get('servers.kibana.port'), CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')), CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'), CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'), + CYPRESS_KIBANA_URL: Url.format({ + protocol: config.get('servers.kibana.protocol'), + hostname: config.get('servers.kibana.hostname'), + port: config.get('servers.kibana.port'), + }), ...process.env, }, wait: true, @@ -55,9 +66,20 @@ export async function SecuritySolutionCypressVisualTestRunner({ getService }: Ft FORCE_COLOR: '1', // eslint-disable-next-line @typescript-eslint/naming-convention CYPRESS_baseUrl: Url.format(config.get('servers.kibana')), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_protocol: config.get('servers.kibana.protocol'), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_hostname: config.get('servers.kibana.hostname'), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_configport: config.get('servers.kibana.port'), CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')), CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'), CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'), + CYPRESS_KIBANA_URL: Url.format({ + protocol: config.get('servers.kibana.protocol'), + hostname: config.get('servers.kibana.hostname'), + port: config.get('servers.kibana.port'), + }), ...process.env, }, wait: true, 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 f032416d2e7bb..b3c130ea1e5dc 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 @@ -52,6 +52,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { policyInfo.packagePolicy.name ); }); + + it('and the show advanced settings button is clicked', async () => { + await testSubjects.missingOrFail('advancedPolicyPanel'); + + let advancedPolicyButton = await pageObjects.policy.findAdvancedPolicyButton(); + await advancedPolicyButton.click(); + + await testSubjects.existOrFail('advancedPolicyPanel'); + + advancedPolicyButton = await pageObjects.policy.findAdvancedPolicyButton(); + await advancedPolicyButton.click(); + await testSubjects.missingOrFail('advancedPolicyPanel'); + }); }); describe('and the save button is clicked', () => { @@ -98,7 +111,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyLinuxEvent_file'), pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyMacEvent_file'), ]); + + const advancedPolicyButton = await pageObjects.policy.findAdvancedPolicyButton(); + await advancedPolicyButton.click(); + + const advancedPolicyField = await pageObjects.policy.findAdvancedPolicyField(); + await advancedPolicyField.clearValue(); + await advancedPolicyField.click(); + await advancedPolicyField.type('true'); await pageObjects.policy.confirmAndSave(); + await testSubjects.existOrFail('policyDetailsSuccessMessage'); const agentFullPolicy = await policyTestResources.getFullAgentPolicy( @@ -191,6 +213,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { linux: { events: { file: false, network: true, process: true }, logging: { file: 'info' }, + advanced: { agent: { connection_delay: 'true' } }, }, mac: { events: { file: false, network: true, process: true }, diff --git a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts index 92571e5c27566..8bfbdc32452ee 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts @@ -77,6 +77,22 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr return await testSubjects.find('policyDetailsCancelButton'); }, + /** + * Finds and returns the Advanced Policy Show/Hide Button + */ + async findAdvancedPolicyButton() { + await this.ensureIsOnDetailsPage(); + return await testSubjects.find('advancedPolicyButton'); + }, + + /** + * Finds and returns the linux connection_delay Advanced Policy field + */ + async findAdvancedPolicyField() { + await this.ensureIsOnDetailsPage(); + return await testSubjects.find('linux.advanced.agent.connection_delay'); + }, + /** * ensures that the Details Page is the currently display view */ 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 2a6b2c0e69d1d..849e91a785048 100644 --- a/x-pack/test/spaces_api_integration/common/suites/delete.ts +++ b/x-pack/test/spaces_api_integration/common/suites/delete.ts @@ -167,7 +167,7 @@ export function deleteTestSuiteFactory(es: any, esArchiver: any, supertest: Supe expect(resp.body).to.eql({ error: 'Bad Request', statusCode: 400, - message: `This Space cannot be deleted because it is reserved.`, + message: `The default space cannot be deleted because it is reserved.`, }); }; diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 804268fbf5dac..12782e6bdd5ea 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -22,17 +22,21 @@ }, "references": [ { "path": "../src/core/tsconfig.json" }, + { "path": "../src/plugins/dev_tools/tsconfig.json" }, { "path": "../src/plugins/inspector/tsconfig.json" }, { "path": "../src/plugins/kibana_legacy/tsconfig.json" }, { "path": "../src/plugins/kibana_react/tsconfig.json" }, { "path": "../src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../src/plugins/newsfeed/tsconfig.json" }, + { "path": "../src/plugins/security_oss/tsconfig.json" }, { "path": "../src/plugins/share/tsconfig.json" }, - { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "../src/plugins/telemetry/tsconfig.json" }, + { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "../src/plugins/url_forwarding/tsconfig.json" }, { "path": "../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../src/test_utils/tsconfig.json" }, + { "path": "./plugins/global_search/tsconfig.json" }, { "path": "./plugins/licensing/tsconfig.json" }, { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" } diff --git a/x-pack/typings/elasticsearch/aggregations.d.ts b/x-pack/typings/elasticsearch/aggregations.d.ts index bc9ed447c8717..f471b83fbbc6b 100644 --- a/x-pack/typings/elasticsearch/aggregations.d.ts +++ b/x-pack/typings/elasticsearch/aggregations.d.ts @@ -354,6 +354,7 @@ interface AggregationResponsePart